Skip to content

Commit

Permalink
feat(protocol-designer, components): wire up pause form in PD redesign
Browse files Browse the repository at this point in the history
This PR adds form functionality and UI for pause step in PD redesign. I consolidate pause hours,
minutes, and seconds into a single colon-delimited time field, add errors and masking, and parse the
time string when creating command arguments. I also touch several components that require styling
refactors, including `Toolbox`, `DropdownMenu`, and `RadioButton`. Note that migrating separate
pause time fields to a single field will be addressed in a future PR.

Closes AUTH-809
  • Loading branch information
ncdiehl11 committed Sep 23, 2024
1 parent 44a395a commit f01f5c6
Show file tree
Hide file tree
Showing 14 changed files with 322 additions and 15 deletions.
1 change: 1 addition & 0 deletions components/src/atoms/MenuList/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const MenuItem = styled.button<ButtonProps>`
color: ${COLORS.black90};
padding: ${SPACING.spacing8} ${SPACING.spacing12} ${SPACING.spacing8}
${SPACING.spacing12};
border: ${props => (props.border != null ? props.border : 'inherit')};
&:hover {
background-color: ${COLORS.blue10};
Expand Down
4 changes: 2 additions & 2 deletions components/src/atoms/buttons/RadioButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function RadioButton(props: RadioButtonProps): JSX.Element {
&:hover,
&:active {
background-color: ${COLORS.blue40};
background-color: ${disabled ? COLORS.grey35 : COLORS.blue40};
}
`

Expand All @@ -76,7 +76,7 @@ export function RadioButton(props: RadioButtonProps): JSX.Element {
&:hover,
&:active {
background-color: ${COLORS.blue55};
background-color: ${disabled ? COLORS.grey35 : COLORS.blue60};
}
`

Expand Down
11 changes: 8 additions & 3 deletions components/src/molecules/DropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { css } from 'styled-components'
import { BORDERS, COLORS } from '../../helix-design-system'
import {
ALIGN_CENTER,
CURSOR_DEFAULT,
CURSOR_POINTER,
DIRECTION_COLUMN,
DIRECTION_ROW,
Expand Down Expand Up @@ -131,13 +132,15 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
}, [filterOptions.length, dropDownMenuWrapperRef])

const toggleSetShowDropdownMenu = (): void => {
setShowDropdownMenu(!showDropdownMenu)
isDisabled ? null : setShowDropdownMenu(!showDropdownMenu)

Check failure on line 135 in components/src/molecules/DropdownMenu/index.tsx

View workflow job for this annotation

GitHub Actions / js checks

Expected an assignment or function call and instead saw an expression

Check failure on line 135 in components/src/molecules/DropdownMenu/index.tsx

View workflow job for this annotation

GitHub Actions / js checks

Expected an assignment or function call and instead saw an expression
}

const isDisabled = filterOptions.length === 0

const DROPDOWN_STYLE = css`
flex-direction: ${DIRECTION_ROW};
background-color: ${COLORS.white};
cursor: ${CURSOR_POINTER};
cursor: ${isDisabled ? CURSOR_DEFAULT : CURSOR_POINTER};
padding: ${SPACING.spacing8} ${SPACING.spacing12};
border: 1px ${BORDERS.styleSolid}
${showDropdownMenu ? COLORS.blue50 : COLORS.grey50};
Expand All @@ -155,7 +158,8 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
}
&:active {
border: 1px ${BORDERS.styleSolid} ${COLORS.blue50};
border: 1px ${BORDERS.styleSolid}
${isDisabled ? COLORS.grey55 : COLORS.blue50};
}
&:focus-visible {
Expand Down Expand Up @@ -249,6 +253,7 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
onClick(option.value)
setShowDropdownMenu(false)
}}
border="none"
>
<Flex gridGap={SPACING.spacing8} alignItems={ALIGN_CENTER}>
{option.liquidColor != null ? (
Expand Down
4 changes: 3 additions & 1 deletion components/src/organisms/Toolbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ToolboxProps {
closeButtonText?: string
side?: 'left' | 'right'
horizontalSide?: 'top' | 'bottom'
padding?: string
}

export function Toolbox(props: ToolboxProps): JSX.Element {
Expand All @@ -41,6 +42,7 @@ export function Toolbox(props: ToolboxProps): JSX.Element {
confirmButton,
side = 'right',
horizontalSide = 'bottom',
padding = SPACING.spacing16,
} = props

const slideOutRef = React.useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -108,7 +110,7 @@ export function Toolbox(props: ToolboxProps): JSX.Element {
) : null}
</Flex>
<Box
padding={SPACING.spacing16}
padding={padding}
flex="1 1 auto"
overflowY="auto"
ref={slideOutRef}
Expand Down
2 changes: 2 additions & 0 deletions protocol-designer/src/assets/localization/en/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"profile_steps": "profile steps",
"ending_hold": "ending hold"
},
"temperature": "Temperature (˚C)",
"time": "Time (hh:mm:ss)",
"units": {
"millimeter": "mm",
"microliter": "μL",
Expand Down
1 change: 1 addition & 0 deletions protocol-designer/src/form-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export type PauseForm = AnnotationFields & {
pauseSecond?: string
pauseMessage?: string
pauseTemperature?: string
pauseTime?: string
}
// TODO: separate field values from from metadata
export interface FormData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
</PrimaryButton>
}
height="calc(100vh - 64px)"
padding="0"
title={
<Flex gridGap={SPACING.spacing8} alignItems={ALIGN_CENTER}>
<Icon size="1rem" name={icon} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,256 @@
import * as React from 'react'
import styled from 'styled-components'
import {
COLORS,
DIRECTION_COLUMN,
Divider,
DropdownMenu,
Flex,
RadioButton,
SPACING,
StyledText,
TYPOGRAPHY,
} from '@opentrons/components'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'

export function PauseTools(): JSX.Element {
return <div>TODO: wire this up</div>
import { selectors as uiModuleSelectors } from '../../../../../../ui/modules'
import {
PAUSE_UNTIL_RESUME,
PAUSE_UNTIL_TEMP,
PAUSE_UNTIL_TIME,
} from '../../../../../../constants'

import type { StepFormProps } from '../../types'
import { getInitialDeckSetup } from '../../../../../../step-forms/selectors'
import {
HEATERSHAKER_MODULE_TYPE,
TEMPERATURE_MODULE_TYPE,
getModuleDisplayName,
} from '@opentrons/shared-data'

export function PauseTools(props: StepFormProps): JSX.Element {
const { propsForFields } = props

const tempModuleLabwareOptions = useSelector(
uiModuleSelectors.getTemperatureLabwareOptions
)
const { i18n, t } = useTranslation(['tooltip', 'application', 'form'])

const heaterShakerModuleLabwareOptions = useSelector(
uiModuleSelectors.getHeaterShakerLabwareOptions
)

const { modules } = useSelector(getInitialDeckSetup)
interface ModuleOption {
name: string
value: string
}
const modulesOnDeck = Object.values(modules)
const moduleOptions = modulesOnDeck.reduce<ModuleOption[]>((acc, module) => {
if (
[
TEMPERATURE_MODULE_TYPE as string,
HEATERSHAKER_MODULE_TYPE as string,
].includes(module.type)
) {
const moduleName = getModuleDisplayName(module.model)
return [
...acc,
{ value: module.id, name: `${moduleName} in ${module.slot}` },
]
}
return acc
}, [])

const moduleLabwareOptions = [
...tempModuleLabwareOptions,
...heaterShakerModuleLabwareOptions,
]

const pauseUntilModuleEnabled = moduleLabwareOptions.length > 0

const { pauseAction } = props.formData

return (
<>
<Flex flexDirection={DIRECTION_COLUMN}>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing12}>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
width="100%"
padding={`${SPACING.spacing16} ${SPACING.spacing16} 0 ${SPACING.spacing16}`}
>
<RadioButton
onChange={(e: React.ChangeEvent<any>) => {
propsForFields.pauseAction.updateValue(e.currentTarget.value)
}}
buttonLabel={t(
'form:step_edit_form.field.pauseAction.options.untilResume'
)}
buttonValue={PAUSE_UNTIL_RESUME}
isSelected={
propsForFields.pauseAction.value === PAUSE_UNTIL_RESUME
}
largeDesktopBorderRadius
/>
<RadioButton
onChange={(e: React.ChangeEvent<any>) => {
propsForFields.pauseAction.updateValue(e.currentTarget.value)
}}
buttonLabel={t(
'form:step_edit_form.field.pauseAction.options.untilTime'
)}
buttonValue={PAUSE_UNTIL_TIME}
isSelected={propsForFields.pauseAction.value === PAUSE_UNTIL_TIME}
largeDesktopBorderRadius
/>
<RadioButton
onChange={(e: React.ChangeEvent<any>) => {
propsForFields.pauseAction.updateValue(e.currentTarget.value)
}}
buttonLabel={t(
'form:step_edit_form.field.pauseAction.options.untilTemperature'
)}
buttonValue={PAUSE_UNTIL_TEMP}
isSelected={propsForFields.pauseAction.value === PAUSE_UNTIL_TEMP}
largeDesktopBorderRadius
disabled={!pauseUntilModuleEnabled}
/>
</Flex>
<Divider marginY="0" />
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing12}
paddingX={SPACING.spacing16}
>
{pauseAction === PAUSE_UNTIL_TIME ? (
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing12}
>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
>
<StyledText desktopStyle="captionRegular">
{t('application:time')}
</StyledText>
<StyledTextArea
value={propsForFields.pauseTime.value as string}
onChange={(e: React.ChangeEvent<any>) => {
propsForFields.pauseTime.updateValue(
e.currentTarget.value
)
}}
error={
propsForFields.pauseTime.errorToShow != null &&
propsForFields.pauseTime.value != null &&
propsForFields.pauseTime.value !== ''
}
/>
{propsForFields.pauseTime.value !== '' &&
propsForFields.pauseTime.value != null &&
propsForFields.pauseTime.errorToShow != null ? (
<StyledText
desktopStyle="captionRegular"
color={COLORS.red50}
>
{propsForFields.pauseTime.errorToShow}
</StyledText>
) : null}
</Flex>
</Flex>
) : null}
{pauseAction === PAUSE_UNTIL_TEMP ? (
<>
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText desktopStyle="captionRegular">
{i18n.format(
t('form:step_edit_form.field.moduleActionLabware.label'),
'capitalize'
)}
</StyledText>
<DropdownMenu
filterOptions={moduleOptions}
onClick={value => {
propsForFields.moduleId.updateValue(value)
}}
currentOption={
moduleOptions.find(
option => option.value === propsForFields.moduleId.value
) ?? { name: '', value: '' }
}
dropdownType="neutral"
width="100%"
/>
</Flex>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
>
<StyledText desktopStyle="captionRegular">
{t('application:temperature')}
</StyledText>
<StyledTextArea
value={propsForFields.pauseTemperature.value as string}
onChange={(e: React.ChangeEvent<any>) => {
propsForFields.pauseTemperature.updateValue(
e.currentTarget.value
)
}}
error={propsForFields.pauseTemperature.errorToShow != null}
/>
{propsForFields.pauseTemperature.value !== '' &&
propsForFields.pauseTemperature.errorToShow != null ? (
<StyledText
desktopStyle="captionRegular"
color={COLORS.red50}
>
{propsForFields.pauseTemperature.errorToShow}
</StyledText>
) : null}
</Flex>
</>
) : null}
</Flex>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing4}
paddingX={SPACING.spacing16}
>
<StyledText desktopStyle="captionRegular">
{i18n.format(
t('form:step_edit_form.field.pauseMessage.label'),
'capitalize'
)}
</StyledText>
<StyledTextArea
value={propsForFields.pauseMessage.value as string}
onChange={(e: React.ChangeEvent<any>) => {
propsForFields.pauseMessage.updateValue(e.currentTarget.value)
}}
height="7rem"
/>
</Flex>
</Flex>
</Flex>
</>
)
}

const StyledTextArea = styled.textarea<{ height?: string; error?: boolean }>`
width: 100%;
height: ${props => (props.height != null ? props.height : '2rem')};
box-sizing: border-box;
border: 1px solid
${props =>
props.error != null && props.error ? COLORS.red50 : COLORS.grey50};
border-radius: 4px;
padding: 8px;
font-size: ${TYPOGRAPHY.fontSizeH4};
line-height: ${TYPOGRAPHY.lineHeight16};
font-weight: ${TYPOGRAPHY.fontWeightRegular};
resize: none;
`
Loading

0 comments on commit f01f5c6

Please sign in to comment.