Skip to content

Commit

Permalink
feat(protocol-designer): edit multiple modules modal + row (#14933)
Browse files Browse the repository at this point in the history
closes AUTH-16
  • Loading branch information
jerader authored Apr 18, 2024
1 parent 58973c6 commit 585f69e
Show file tree
Hide file tree
Showing 11 changed files with 655 additions and 22 deletions.
41 changes: 31 additions & 10 deletions protocol-designer/src/components/EditModules.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import * as React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
FLEX_ROBOT_TYPE,
TEMPERATURE_MODULE_TYPE,
} from '@opentrons/shared-data'
import {
selectors as stepFormSelectors,
actions as stepFormActions,
} from '../step-forms'
import { moveDeckItem } from '../labware-ingred/actions/actions'
import { getRobotType } from '../file-data/selectors'
import { getEnableMoam } from '../feature-flags/selectors'
import { EditMultipleModulesModal } from './modals/EditModulesModal/EditMultipleModulesModal'
import { useBlockingHint } from './Hints/useBlockingHint'
import { MagneticModuleWarningModalContent } from './modals/EditModulesModal/MagneticModuleWarningModalContent'
import { EditModulesModal } from './modals/EditModulesModal'
import { ModuleModel, ModuleType } from '@opentrons/shared-data'
import type { ModuleModel, ModuleType } from '@opentrons/shared-data'

export interface EditModulesProps {
moduleToEdit: {
Expand All @@ -27,6 +34,12 @@ export const EditModules = (props: EditModulesProps): JSX.Element => {
const { onCloseClick, moduleToEdit } = props
const { moduleId, moduleType } = moduleToEdit
const _initialDeckSetup = useSelector(stepFormSelectors.getInitialDeckSetup)
const robotType = useSelector(getRobotType)
const moamFf = useSelector(getEnableMoam)
const showMultipleModuleModal =
robotType === FLEX_ROBOT_TYPE &&
moduleType === TEMPERATURE_MODULE_TYPE &&
moamFf

const moduleOnDeck = moduleId ? _initialDeckSetup.modules[moduleId] : null
const [
Expand Down Expand Up @@ -74,16 +87,24 @@ export const EditModules = (props: EditModulesProps): JSX.Element => {
enabled: changeModuleWarningInfo !== null,
})

return (
changeModuleWarning ?? (
<EditModulesModal
moduleType={moduleType}
moduleOnDeck={moduleOnDeck}
let modal = (
<EditModulesModal
moduleType={moduleType}
moduleOnDeck={moduleOnDeck}
onCloseClick={onCloseClick}
editModuleSlot={editModuleSlot}
editModuleModel={editModuleModel}
displayModuleWarning={displayModuleWarning}
/>
)
if (showMultipleModuleModal) {
modal = (
<EditMultipleModulesModal
onCloseClick={onCloseClick}
editModuleSlot={editModuleSlot}
editModuleModel={editModuleModel}
displayModuleWarning={displayModuleWarning}
allModulesOnDeck={Object.values(_initialDeckSetup.modules)}
moduleType={moduleType}
/>
)
)
}
return changeModuleWarning ?? modal
}
25 changes: 23 additions & 2 deletions protocol-designer/src/components/__tests__/EditModules.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import * as React from 'react'
import { screen } from '@testing-library/react'
import { vi, beforeEach, describe, it } from 'vitest'
import {
FLEX_ROBOT_TYPE,
OT2_ROBOT_TYPE,
TEMPERATURE_MODULE_TYPE,
} from '@opentrons/shared-data'
import { i18n } from '../../localization'
import { getInitialDeckSetup } from '../../step-forms/selectors'
import { getDismissedHints } from '../../tutorial/selectors'
import { EditModules } from '../EditModules'
import { EditModulesModal } from '../modals/EditModulesModal'
import { renderWithProviders } from '../../__testing-utils__'
import { getEnableMoam } from '../../feature-flags/selectors'
import { getRobotType } from '../../file-data/selectors'
import { EditMultipleModulesModal } from '../modals/EditModulesModal/EditMultipleModulesModal'

import type { HintKey } from '../../tutorial'

vi.mock('../../step-forms/selectors')
vi.mock('../modals/EditModulesModal/EditMultipleModulesModal')
vi.mock('../modals/EditModulesModal')
vi.mock('../../tutorial/selectors')

vi.mock('../../file-data/selectors')
vi.mock('../../feature-flags/selectors')
const render = (props: React.ComponentProps<typeof EditModules>) => {
return renderWithProviders(<EditModules {...props} />, {
i18nInstance: i18n,
Expand Down Expand Up @@ -51,11 +61,22 @@ describe('EditModules', () => {
vi.mocked(EditModulesModal).mockReturnValue(
<div>mock EditModulesModal</div>
)
vi.mocked(EditMultipleModulesModal).mockReturnValue(
<div>mock EditMultipleModulesModal</div>
)
vi.mocked(getDismissedHints).mockReturnValue([hintKey])
vi.mocked(getRobotType).mockReturnValue(OT2_ROBOT_TYPE)
vi.mocked(getEnableMoam).mockReturnValue(true)
})

it('renders the edit modules modal', () => {
it('renders the edit modules modal for single modules', () => {
render(props)
screen.getByText('mock EditModulesModal')
})
it('renders multiple edit modules modal', () => {
props.moduleToEdit.moduleType = TEMPERATURE_MODULE_TYPE
vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE)
render(props)
screen.getByText('mock EditMultipleModulesModal')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector, useDispatch } from 'react-redux'
import { Controller, useForm, useWatch } from 'react-hook-form'
import {
BUTTON_TYPE_SUBMIT,
OutlineButton,
ModalShell,
Flex,
SPACING,
DIRECTION_ROW,
Box,
Text,
ALIGN_CENTER,
JUSTIFY_FLEX_END,
JUSTIFY_END,
DeckConfigurator,
DIRECTION_COLUMN,
} from '@opentrons/components'
import {
DeckConfiguration,
SINGLE_RIGHT_SLOT_FIXTURE,
TEMPERATURE_MODULE_CUTOUTS,
TEMPERATURE_MODULE_TYPE,
TEMPERATURE_MODULE_V2,
TEMPERATURE_MODULE_V2_FIXTURE,
} from '@opentrons/shared-data'
import { createModule, deleteModule } from '../../../step-forms/actions'
import { getLabwareOnSlot, getSlotIsEmpty } from '../../../step-forms'
import { getInitialDeckSetup } from '../../../step-forms/selectors'
import { getLabwareIsCompatible } from '../../../utils/labwareModuleCompatibility'
import { PDAlert } from '../../alerts/PDAlert'
import type { Control, ControllerRenderProps } from 'react-hook-form'
import type { CutoutId, ModuleType } from '@opentrons/shared-data'
import type { ModuleOnDeck } from '../../../step-forms'

export interface EditMultipleModulesModalValues {
selectedAddressableAreas: string[]
}

interface EditMultipleModulesModalComponentProps
extends EditMultipleModulesModalProps {
control: Control<EditMultipleModulesModalValues, 'selectedAddressableAreas'>
moduleLocations: string[] | null
}

const EditMultipleModulesModalComponent = (
props: EditMultipleModulesModalComponentProps
): JSX.Element => {
const { t } = useTranslation(['button', 'alert'])
const {
onCloseClick,
allModulesOnDeck,
control,
moduleLocations,
moduleType,
} = props
const initialDeckSetup = useSelector(getInitialDeckSetup)

const selectedSlots = useWatch({
control,
name: 'selectedAddressableAreas',
defaultValue: moduleLocations ?? [],
})
const occupiedCutoutIds = selectedSlots
.map(slot => {
const hasModSlot =
allModulesOnDeck.find(
module =>
module.type === moduleType && slot === `cutout${module.slot}`
) != null
const labwareOnSlot = getLabwareOnSlot(initialDeckSetup, slot)
const isLabwareCompatible =
(labwareOnSlot &&
getLabwareIsCompatible(labwareOnSlot.def, moduleType)) ??
true
const isEmpty =
(getSlotIsEmpty(initialDeckSetup, slot, true) || hasModSlot) &&
isLabwareCompatible

return { slot, isEmpty }
})
.filter(slot => !slot.isEmpty)
const hasConflictedSlot = occupiedCutoutIds.length > 0
const mappedModules: DeckConfiguration =
moduleLocations != null
? moduleLocations.flatMap(location => {
return [
{
cutoutId: location as CutoutId,
cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE,
},
]
})
: []
const STANDARD_EMPTY_SLOTS: DeckConfiguration = TEMPERATURE_MODULE_CUTOUTS.map(
cutoutId => ({
cutoutId,
cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE,
})
)

STANDARD_EMPTY_SLOTS.forEach(emptySlot => {
if (
!mappedModules.some(({ cutoutId }) => cutoutId === emptySlot.cutoutId)
) {
mappedModules.push(emptySlot)
}
})

const selectableSlots =
mappedModules.length > 0 ? mappedModules : STANDARD_EMPTY_SLOTS
const [updatedSlots, setUpdatedSlots] = React.useState<DeckConfiguration>(
selectableSlots
)
const handleClickAdd = (
cutoutId: string,
field: ControllerRenderProps<
EditMultipleModulesModalValues,
'selectedAddressableAreas'
>
): void => {
const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => {
if (slot.cutoutId === cutoutId) {
return {
...slot,
cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE,
}
}
return slot
})
setUpdatedSlots(modifiedSlots)
const updatedSelectedSlots = [...selectedSlots, cutoutId]
field.onChange(updatedSelectedSlots)
}

const handleClickRemove = (
cutoutId: string,
field: ControllerRenderProps<
EditMultipleModulesModalValues,
'selectedAddressableAreas'
>
): void => {
const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => {
if (slot.cutoutId === cutoutId) {
return { ...slot, cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE }
}
return slot
})
setUpdatedSlots(modifiedSlots)

field.onChange(selectedSlots.filter(item => item !== cutoutId))
}
const occupiedSlots = occupiedCutoutIds.map(
occupiedCutout => occupiedCutout.slot.split('cutout')[1]
)
const alertDescription = t(
`alert:module_placement.SLOTS_OCCUPIED.${
occupiedSlots.length === 1 ? 'single' : 'multi'
}`,
{
slotName: occupiedSlots,
}
)

return (
<>
<Flex height="23rem" flexDirection={DIRECTION_COLUMN}>
<Flex
justifyContent={JUSTIFY_END}
alignItems={ALIGN_CENTER}
height="1.5rem"
paddingX={SPACING.spacing32}
>
<Box>
{hasConflictedSlot ? (
<PDAlert
alertType="warning"
title={t('alert:module_placement.SLOT_OCCUPIED.title')}
description={alertDescription}
/>
) : null}
</Box>
</Flex>
<Controller
name="selectedAddressableAreas"
control={control}
defaultValue={moduleLocations ?? []}
render={({ field }) => (
<DeckConfigurator
deckConfig={updatedSlots}
handleClickAdd={cutoutId => handleClickAdd(cutoutId, field)}
handleClickRemove={cutoutId => handleClickRemove(cutoutId, field)}
showExpansion={false}
/>
)}
/>
</Flex>
<Flex
flexDirection={DIRECTION_ROW}
justifyContent={JUSTIFY_FLEX_END}
paddingRight={SPACING.spacing32}
paddingBottom={SPACING.spacing32}
gridGap={SPACING.spacing8}
>
<OutlineButton onClick={onCloseClick}>{t('cancel')}</OutlineButton>
<OutlineButton type={BUTTON_TYPE_SUBMIT} disabled={hasConflictedSlot}>
{t('save')}
</OutlineButton>
</Flex>
</>
)
}

export interface EditMultipleModulesModalProps {
onCloseClick: () => void
allModulesOnDeck: ModuleOnDeck[]
moduleType: ModuleType
}
export function EditMultipleModulesModal(
props: EditMultipleModulesModalProps
): JSX.Element {
const { onCloseClick, allModulesOnDeck, moduleType } = props
const { t } = useTranslation('modules')
const dispatch = useDispatch()
const { control, handleSubmit } = useForm<EditMultipleModulesModalValues>()
const moduleLocations = Object.values(allModulesOnDeck)
.filter(module => module.type === moduleType)
.map(temp => `cutout${temp.slot}`)

const onSaveClick = (data: EditMultipleModulesModalValues): void => {
onCloseClick()

data.selectedAddressableAreas.forEach(aa => {
const moduleInSlot = Object.values(allModulesOnDeck).find(module =>
aa.includes(module.slot)
)
if (!moduleInSlot) {
dispatch(
createModule({
slot: aa.split('cutout')[1],
type: TEMPERATURE_MODULE_TYPE,
model: TEMPERATURE_MODULE_V2,
})
)
}
})
Object.values(allModulesOnDeck).forEach(module => {
const moduleCutout = `cutout${module.slot}`
if (!data.selectedAddressableAreas.includes(moduleCutout)) {
dispatch(deleteModule(module.id))
}
})
}

return (
<form onSubmit={handleSubmit(onSaveClick)}>
<ModalShell width="48rem">
<Box marginTop={SPACING.spacing32} paddingX={SPACING.spacing32}>
<Text as="h2">
{t('module_display_names.multipleTemperatureModuleTypes')}
</Text>
</Box>
<EditMultipleModulesModalComponent
onCloseClick={onCloseClick}
allModulesOnDeck={allModulesOnDeck}
control={control}
moduleLocations={moduleLocations}
moduleType={moduleType}
/>
</ModalShell>
</form>
)
}
Loading

0 comments on commit 585f69e

Please sign in to comment.