Skip to content

Commit

Permalink
feat(protocol-designer,-shared-data): add liquid class scaffolding to…
Browse files Browse the repository at this point in the history
… PD (#17126)

This PR introduces initial work for liquid class support in protocol
designer. I create a full liquid class interface based on the API's
Pydantic model. I also add utilities for getting all liquid class
definitions and a feature flag to hide liquid classes from the UI.
Lastly, I add some of the basic functionality for assigning a liquid
class to a defined liquid, and displaying that liquid class in
LiquidCard. A liquid class assigned to a defined liquid will be
accessible through extended existing selectors (allIngredientNamesIds)
for step forms.

closes AUTH-965
closes AUTH-968
  • Loading branch information
ncdiehl11 authored Dec 18, 2024
1 parent c752197 commit f8def77
Show file tree
Hide file tree
Showing 19 changed files with 312 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,9 @@
"OT_PD_ENABLE_REACT_SCAN": {
"title": "Enable React Scan",
"description": "Enable React Scan support for components rendering check"
},
"OT_PD_ENABLE_LIQUID_CLASSES": {
"title": "Enable liquid classes",
"description": "Enable liquid classes support"
}
}
4 changes: 4 additions & 0 deletions protocol-designer/src/assets/localization/en/liquids.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"display_color": "Color",
"liquid_volume": "Liquid volume by well",
"liquid": "Liquid",
"liquid_class": {
"title": "Liquid class",
"tooltip": "Applies predefined pipetting settings to transfer and mix steps using this liquid"
},
"liquids_added": "Liquids added",
"liquids": "Liquids",
"microliters": "µL",
Expand Down
2 changes: 2 additions & 0 deletions protocol-designer/src/feature-flags/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const initialFlags: Flags = {
OT_PD_ENABLE_HOT_KEYS_DISPLAY:
process.env.OT_PD_ENABLE_HOT_KEYS_DISPLAY === '1' || true,
OT_PD_ENABLE_REACT_SCAN: process.env.OT_PD_ENABLE_REACT_SCAN === '1' || false,
OT_PD_ENABLE_LIQUID_CLASSES:
process.env.OT_PD_ENABLE_REACT_SCAN === '1' || false,
}
// @ts-expect-error(sa, 2021-6-10): cannot use string literals as action type
// TODO IMMEDIATELY: refactor this to the old fashioned way if we cannot have type safety: https://github.com/redux-utilities/redux-actions/issues/282#issuecomment-595163081
Expand Down
4 changes: 4 additions & 0 deletions protocol-designer/src/feature-flags/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ export const getEnableReactScan: Selector<boolean> = createSelector(
getFeatureFlagData,
flags => flags.OT_PD_ENABLE_REACT_SCAN ?? false
)
export const getEnableLiquidClasses: Selector<boolean> = createSelector(
getFeatureFlagData,
flags => flags.OT_PD_ENABLE_LIQUID_CLASSES ?? false
)
2 changes: 2 additions & 0 deletions protocol-designer/src/feature-flags/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type FlagTypes =
| 'OT_PD_ENABLE_RETURN_TIP'
| 'OT_PD_ENABLE_HOT_KEYS_DISPLAY'
| 'OT_PD_ENABLE_REACT_SCAN'
| 'OT_PD_ENABLE_LIQUID_CLASSES'
// flags that are not in this list only show in prerelease mode
export const userFacingFlags: FlagTypes[] = [
'OT_PD_DISABLE_MODULE_RESTRICTIONS',
Expand All @@ -49,5 +50,6 @@ export const allFlags: FlagTypes[] = [
'OT_PD_ENABLE_COMMENT',
'OT_PD_ENABLE_RETURN_TIP',
'OT_PD_ENABLE_REACT_SCAN',
'OT_PD_ENABLE_LIQUID_CLASSES',
]
export type Flags = Partial<Record<FlagTypes, boolean | null | undefined>>
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('DUPLICATE_LABWARE action', () => {
wellDetailsByLocation: null,
concentration: '50 mol/ng',
description: '',
liquidClass: null,
displayColor: '#b925ff',
serialize: false,
},
Expand All @@ -27,6 +28,7 @@ describe('DUPLICATE_LABWARE action', () => {
wellDetailsByLocation: null,
concentration: '100%',
description: '',
liquidClass: null,
displayColor: '#ffd600',
serialize: false,
},
Expand Down
1 change: 1 addition & 0 deletions protocol-designer/src/labware-ingred/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const allIngredientNamesIds: Selector<
ingredientId: ingredId,
name: ingreds[ingredId].name,
displayColor: ingreds[ingredId].displayColor,
liquidClass: ingreds[ingredId].liquidClass,
}))
})
const getLabwareSelectionMode: Selector<RootSlice, boolean> = createSelector(
Expand Down
19 changes: 9 additions & 10 deletions protocol-designer/src/labware-ingred/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,22 @@ export interface WellContents {
selected?: boolean
maxVolume?: number
}
export type ContentsByWell = {
[wellName: string]: WellContents
} | null
export interface WellContentsByLabware {
[labwareId: string]: ContentsByWell
}
export type ContentsByWell = Record<string, WellContents> | null
export type WellContentsByLabware = Record<string, ContentsByWell>
// ==== INGREDIENTS ====
// TODO(ND: 12/17/2024): add migration for liquids in >8.3.0
export type OrderedLiquids = Array<{
ingredientId: string
name: string | null | undefined
displayColor: string | null | undefined
name?: string | null
displayColor?: string | null
liquidClass?: string | null
}>
// TODO: Ian 2018-10-15 audit & rename these confusing types
export interface LiquidGroup {
name: string | null | undefined
description: string | null | undefined
name: string | null
description: string | null
displayColor: string
liquidClass: string | null
serialize: boolean
}
export type IngredInputs = LiquidGroup & {
Expand Down
15 changes: 15 additions & 0 deletions protocol-designer/src/liquid-defs/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getAllLiquidClassDefs } from '@opentrons/shared-data'

const liquidClassDefs = getAllLiquidClassDefs()
export const getLiquidClassDisplayName = (
liquidClass: string | null
): string | null => {
if (liquidClass == null) {
return null
}
if (!(liquidClass in liquidClassDefs)) {
console.warn(`Liquid class ${liquidClass} not found`)
return null
}
return liquidClassDefs[liquidClass].displayName
}
17 changes: 15 additions & 2 deletions protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '@opentrons/components'

import { LINE_CLAMP_TEXT_STYLE } from '../../atoms'
import { getEnableLiquidClasses } from '../../feature-flags/selectors'
import { removeWellsContents } from '../../labware-ingred/actions'
import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
import { getLabwareEntities } from '../../step-forms/selectors'
Expand All @@ -34,7 +35,7 @@ interface LiquidCardProps {

export function LiquidCard(props: LiquidCardProps): JSX.Element {
const { info } = props
const { name, color, liquidIndex } = info
const { name, color, liquidClassDisplayName, liquidIndex } = info
const { t } = useTranslation('liquids')
const dispatch = useDispatch()
const [isExpanded, setIsExpanded] = useState<boolean>(false)
Expand All @@ -49,6 +50,7 @@ export function LiquidCard(props: LiquidCardProps): JSX.Element {
const allWellContentsForActiveItem = useSelector(
wellContentsSelectors.getAllWellContentsForActiveItem
)
const enableLiquidClasses = useSelector(getEnableLiquidClasses)
const wellContents =
allWellContentsForActiveItem != null && labwareId != null
? allWellContentsForActiveItem[labwareId]
Expand Down Expand Up @@ -104,13 +106,24 @@ export function LiquidCard(props: LiquidCardProps): JSX.Element {
>
<Flex alignItems={ALIGN_CENTER} gridGap={SPACING.spacing16}>
<LiquidIcon color={color} size="medium" />
<Flex flexDirection={DIRECTION_COLUMN} width="12.375rem">
<Flex
flexDirection={DIRECTION_COLUMN}
width="12.375rem"
gridGap={SPACING.spacing4}
>
<StyledText
desktopStyle="bodyDefaultSemiBold"
css={LINE_CLAMP_TEXT_STYLE(3)}
>
{name}
</StyledText>
{liquidClassDisplayName != null && enableLiquidClasses ? (
<Tag
text={liquidClassDisplayName}
type="default"
shrinkToContent
/>
) : null}
<StyledText
desktopStyle="bodyDefaultRegular"
css={LINE_CLAMP_TEXT_STYLE(3)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { selectors as labwareIngredSelectors } from '../../labware-ingred/select
import * as wellContentsSelectors from '../../top-selectors/well-contents'
import * as fieldProcessors from '../../steplist/fieldLevel/processing'
import * as labwareIngredActions from '../../labware-ingred/actions'
import { getLiquidClassDisplayName } from '../../liquid-defs/utils'
import { getSelectedWells } from '../../well-selection/selectors'
import { getLabwareNicknamesById } from '../../ui/labware/selectors'
import {
Expand All @@ -38,6 +39,7 @@ export interface LiquidInfo {
name: string
color: string
liquidIndex: string
liquidClassDisplayName: string | null
}

interface ValidFormValues {
Expand Down Expand Up @@ -214,6 +216,9 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element {
liquidIndex: liquid,
name: foundLiquid?.name ?? '',
color: foundLiquid?.displayColor ?? '',
liquidClassDisplayName: getLiquidClassDisplayName(
foundLiquid?.liquidClass ?? null
),
}
})
.filter(Boolean)
Expand Down
65 changes: 59 additions & 6 deletions protocol-designer/src/organisms/DefineLiquidsModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import { yupResolver } from '@hookform/resolvers/yup'
import * as Yup from 'yup'
import { Controller, useForm } from 'react-hook-form'
import styled from 'styled-components'
import { DEFAULT_LIQUID_COLORS } from '@opentrons/shared-data'
import {
DEFAULT_LIQUID_COLORS,
getAllLiquidClassDefs,
} from '@opentrons/shared-data'
import {
BORDERS,
Btn,
COLORS,
DIRECTION_COLUMN,
DropdownMenu,
Flex,
InputField,
JUSTIFY_END,
Expand All @@ -30,6 +34,7 @@ import * as labwareIngredActions from '../../labware-ingred/actions'
import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
import { HandleEnter } from '../../atoms/HandleEnter'
import { LINE_CLAMP_TEXT_STYLE } from '../../atoms'
import { getEnableLiquidClasses } from '../../feature-flags/selectors'
import { swatchColors } from './swatchColors'

import type { ColorResult, RGBColor } from 'react-color'
Expand All @@ -40,15 +45,17 @@ import type { LiquidGroup } from '../../labware-ingred/types'
interface LiquidEditFormValues {
name: string
displayColor: string
description?: string | null
serialize?: boolean
description: string
liquidClass: string
serialize: boolean
[key: string]: unknown
}

const liquidEditFormSchema: any = Yup.object().shape({
name: Yup.string().required('liquid name is required'),
displayColor: Yup.string(),
description: Yup.string(),
liquidClass: Yup.string(),
serialize: Yup.boolean(),
})

Expand Down Expand Up @@ -77,6 +84,9 @@ export function DefineLiquidsModal(
const allIngredientGroupFields = useSelector(
labwareIngredSelectors.allIngredientGroupFields
)
const enableLiquidClasses = useSelector(getEnableLiquidClasses)
const liquidClassDefs = getAllLiquidClassDefs()

const liquidGroupId = selectedLiquidGroupState.liquidGroupId
const deleteLiquidGroup = (): void => {
if (liquidGroupId != null) {
Expand Down Expand Up @@ -107,13 +117,14 @@ export function DefineLiquidsModal(
const initialValues: LiquidEditFormValues = {
name: selectedIngredFields?.name ?? '',
displayColor: selectedIngredFields?.displayColor ?? swatchColors(liquidId),
liquidClass: selectedIngredFields?.liquidClass ?? '',
description: selectedIngredFields?.description ?? '',
serialize: selectedIngredFields?.serialize ?? false,
}

const {
handleSubmit,
formState: { errors, touchedFields },
formState,
control,
watch,
setValue,
Expand All @@ -125,12 +136,15 @@ export function DefineLiquidsModal(
})
const name = watch('name')
const color = watch('displayColor')
const liquidClass = watch('liquidClass')
const { errors, touchedFields } = formState

const handleLiquidEdits = (values: LiquidEditFormValues): void => {
saveForm({
name: values.name,
displayColor: values.displayColor,
description: values.description ?? null,
liquidClass: values.liquidClass ? values.liquidClass : null,
description: values.description ? values.description : null,
serialize: values.serialize ?? false,
})
}
Expand All @@ -142,6 +156,15 @@ export function DefineLiquidsModal(
return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alpha)}`
}

const liquidClassOptions = [
{ name: 'Choose an option', value: '' },
...Object.entries(liquidClassDefs).map(
([liquidClassDefName, { displayName }]) => {
return { name: displayName, value: liquidClassDefName }
}
),
]

return (
<HandleEnter
onEnter={() => {
Expand Down Expand Up @@ -182,6 +205,7 @@ export function DefineLiquidsModal(
left="4.375rem"
top="4.6875rem"
ref={chooseColorWrapperRef}
zIndex={2}
>
<Controller
name="displayColor"
Expand All @@ -202,7 +226,10 @@ export function DefineLiquidsModal(
) : null}

<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing32}>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing8}>
<Flex
flexDirection={DIRECTION_COLUMN}
gridGap={SPACING.spacing12}
>
<Flex
flexDirection={DIRECTION_COLUMN}
color={COLORS.grey60}
Expand Down Expand Up @@ -239,6 +266,32 @@ export function DefineLiquidsModal(
</StyledText>
<DescriptionField {...register('description')} />
</Flex>
{enableLiquidClasses ? (
<Flex flexDirection={DIRECTION_COLUMN} color={COLORS.grey60}>
<Controller
control={control}
name="liquidClass"
render={({ field }) => (
<DropdownMenu
title={t('liquid_class.title')}
tooltipText={t('liquid_class.tooltip')}
dropdownType="neutral"
width="100%"
filterOptions={liquidClassOptions}
currentOption={
liquidClassOptions.find(
({ value }) => value === liquidClass
) ?? liquidClassOptions[0]
}
onClick={value => {
field.onChange(value)
setValue('liquidClass', value)
}}
/>
)}
/>
</Flex>
) : null}
<Flex
flexDirection={DIRECTION_COLUMN}
color={COLORS.grey60}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ describe('MaterialsListModal', () => {
ingredientId: mockId,
name: 'mockName',
displayColor: 'mockDisplayColor',
liquidClass: null,
},
],
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('SlotOverflowMenu', () => {
displayColor: 'mockColor',
name: 'mockname',
ingredientId: '0',
liquidClass: null,
},
])
})
Expand Down
Loading

0 comments on commit f8def77

Please sign in to comment.