Skip to content

Commit

Permalink
feat(app): create well selection component (#15207)
Browse files Browse the repository at this point in the history
creates ODD well selection component for use in quick transfer.
implements basic source and destination quick transfer well selection.
based on SelectableLabware component in protocol designer.

closes PLAT-172, PLAT-175
  • Loading branch information
brenthagen authored and Carlos-fernandez committed Jun 3, 2024
1 parent 4727726 commit f4ecce4
Show file tree
Hide file tree
Showing 10 changed files with 489 additions and 50 deletions.
1 change: 1 addition & 0 deletions app/src/assets/localization/en/shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"refresh": "refresh",
"remember_my_selection_and_do_not_ask_again": "Remember my selection and don't ask again",
"reset_all": "Reset all",
"reset": "Reset",
"restart": "restart",
"resume": "resume",
"return": "return",
Expand Down
61 changes: 42 additions & 19 deletions app/src/organisms/QuickTransferFlow/SelectDestWells.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { Flex, SPACING } from '@opentrons/components'
import { Flex, POSITION_FIXED, SPACING } from '@opentrons/components'

import { ChildNavigation } from '../ChildNavigation'
import { WellSelection } from '../../organisms/WellSelection'

import type { SmallButton } from '../../atoms/buttons'
import type {
Expand All @@ -13,47 +14,69 @@ import type {
interface SelectDestWellsProps {
onNext: () => void
onBack: () => void
exitButtonProps: React.ComponentProps<typeof SmallButton>
state: QuickTransferWizardState
dispatch: React.Dispatch<QuickTransferWizardAction>
}

export function SelectDestWells(props: SelectDestWellsProps): JSX.Element {
const { onNext, onBack, exitButtonProps, state, dispatch } = props
const { onNext, onBack, state, dispatch } = props
const { i18n, t } = useTranslation(['quick_transfer', 'shared'])

const destinationWells = state.destinationWells ?? []
const destinationWellGroup = destinationWells.reduce((acc, well) => {
return { ...acc, [well]: null }
}, {})

const [selectedWells, setSelectedWells] = React.useState(destinationWellGroup)

const handleClickNext = (): void => {
// until well selection is implemented, select all wells and proceed to the next step
if (state.destination === 'source' && state.source != null) {
dispatch({
type: 'SET_DEST_WELLS',
wells: Object.keys(state.source.wells),
})
} else if (state.destination !== 'source' && state.destination != null) {
dispatch({
type: 'SET_DEST_WELLS',
wells: Object.keys(state.destination.wells),
})
}
dispatch({
type: 'SET_DEST_WELLS',
wells: Object.keys(selectedWells),
})
onNext()
}

const resetButtonProps: React.ComponentProps<typeof SmallButton> = {
buttonType: 'tertiaryLowLight',
buttonText: t('shared:reset'),
onClick: () => {
setSelectedWells({})
},
}

return (
<Flex>
<>
<ChildNavigation
header={t('select_dest_wells')}
onClickBack={onBack}
buttonText={i18n.format(t('shared:continue'), 'capitalize')}
onClickButton={handleClickNext}
buttonIsDisabled={false}
secondaryButtonProps={exitButtonProps}
secondaryButtonProps={resetButtonProps}
top={SPACING.spacing8}
/>
<Flex
marginTop={SPACING.spacing120}
padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
position={POSITION_FIXED}
top="0"
left="0"
width="100%"
>
TODO: Add destination well selection deck map
{state.destination != null && state.source != null ? (
<WellSelection
definition={
state.destination === 'source' ? state.source : state.destination
}
selectedPrimaryWells={selectedWells}
selectWells={wellGroup => {
setSelectedWells(prevWells => ({ ...prevWells, ...wellGroup }))
}}
channels={state.pipette?.channels ?? 1}
/>
) : null}
</Flex>
</Flex>
</>
)
}
58 changes: 42 additions & 16 deletions app/src/organisms/QuickTransferFlow/SelectSourceWells.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { Flex, SPACING } from '@opentrons/components'
import { Flex, POSITION_FIXED, SPACING } from '@opentrons/components'

import { ChildNavigation } from '../ChildNavigation'
import { ChildNavigation } from '../../organisms/ChildNavigation'
import { WellSelection } from '../../organisms/WellSelection'

import type { SmallButton } from '../../atoms/buttons'
import type {
Expand All @@ -13,42 +14,67 @@ import type {
interface SelectSourceWellsProps {
onNext: () => void
onBack: () => void
exitButtonProps: React.ComponentProps<typeof SmallButton>
state: QuickTransferWizardState
dispatch: React.Dispatch<QuickTransferWizardAction>
}

export function SelectSourceWells(props: SelectSourceWellsProps): JSX.Element {
const { onNext, onBack, exitButtonProps, state, dispatch } = props
const { onNext, onBack, state, dispatch } = props
const { i18n, t } = useTranslation(['quick_transfer', 'shared'])

const sourceWells = state.sourceWells ?? []
const sourceWellGroup = sourceWells.reduce((acc, well) => {
return { ...acc, [well]: null }
}, {})

const [selectedWells, setSelectedWells] = React.useState(sourceWellGroup)

const handleClickNext = (): void => {
// until well selection is implemented, select all wells and proceed to the next step
if (state.source?.wells != null) {
dispatch({
type: 'SET_SOURCE_WELLS',
wells: Object.keys(state.source.wells),
})
onNext()
}
dispatch({
type: 'SET_SOURCE_WELLS',
wells: Object.keys(selectedWells),
})
onNext()
}

const resetButtonProps: React.ComponentProps<typeof SmallButton> = {
buttonType: 'tertiaryLowLight',
buttonText: t('shared:reset'),
onClick: () => {
setSelectedWells({})
},
}

return (
<Flex>
<>
<ChildNavigation
header={t('select_source_wells')}
onClickBack={onBack}
buttonText={i18n.format(t('shared:continue'), 'capitalize')}
onClickButton={handleClickNext}
buttonIsDisabled={false}
secondaryButtonProps={exitButtonProps}
secondaryButtonProps={resetButtonProps}
top={SPACING.spacing8}
/>
<Flex
marginTop={SPACING.spacing120}
padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
position={POSITION_FIXED}
top="0"
left="0"
width="100%"
>
TODO: Add source well selection deck map
{state.source != null ? (
<WellSelection
definition={state.source}
selectedPrimaryWells={selectedWells}
selectWells={wellGroup => {
setSelectedWells(prevWells => ({ ...prevWells, ...wellGroup }))
}}
channels={state.pipette?.channels ?? 1}
/>
) : null}
</Flex>
</Flex>
</>
)
}
119 changes: 119 additions & 0 deletions app/src/organisms/WellSelection/SelectionRect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as React from 'react'
import { Flex, JUSTIFY_CENTER } from '@opentrons/components'

import type { DragRect, GenericRect } from './types'

interface SelectionRectProps {
onSelectionMove?: (rect: GenericRect) => void
onSelectionDone?: (rect: GenericRect) => void
children?: React.ReactNode
}

export function SelectionRect(props: SelectionRectProps): JSX.Element {
const { onSelectionMove, onSelectionDone, children } = props

const [positions, setPositions] = React.useState<DragRect | null>(null)
const parentRef = React.useRef<HTMLElement | SVGElement | null>(null)

const getRect = (args: DragRect): GenericRect => {
const { xStart, yStart, xDynamic, yDynamic } = args
return {
x0: Math.min(xStart, xDynamic),
x1: Math.max(xStart, xDynamic),
y0: Math.min(yStart, yDynamic),
y1: Math.max(yStart, yDynamic),
}
}

const handleDrag = React.useCallback(
(e: TouchEvent | MouseEvent): void => {
let xDynamic: number
let yDynamic: number
if (e instanceof TouchEvent) {
const touch = e.touches[0]
xDynamic = touch.clientX
yDynamic = touch.clientY
} else {
xDynamic = e.clientX
yDynamic = e.clientY
}
setPositions(prevPositions => {
if (prevPositions != null) {
const nextRect = {
...prevPositions,
xDynamic,
yDynamic,
}
const rect = getRect(nextRect)
onSelectionMove != null && onSelectionMove(rect)

return nextRect
}
return prevPositions
})
},
[onSelectionMove]
)

const handleDragEnd = React.useCallback(
(e: TouchEvent | MouseEvent): void => {
if (!(e instanceof TouchEvent) && !(e instanceof MouseEvent)) {
return
}
const finalRect = positions != null ? getRect(positions) : null
setPositions(prevPositions => {
return prevPositions === positions ? null : prevPositions
})
// call onSelectionDone callback with {x0, x1, y0, y1} of final selection rectangle
onSelectionDone != null && finalRect != null && onSelectionDone(finalRect)
},
[onSelectionDone, positions]
)

const handleTouchStart: React.TouchEventHandler = e => {
const touch = e.touches[0]
setPositions({
xStart: touch.clientX,
xDynamic: touch.clientX,
yStart: touch.clientY,
yDynamic: touch.clientY,
})
}

const handleMouseDown: React.MouseEventHandler = e => {
setPositions({
xStart: e.clientX,
xDynamic: e.clientX,
yStart: e.clientY,
yDynamic: e.clientY,
})
}

React.useEffect(() => {
document.addEventListener('touchmove', handleDrag)
document.addEventListener('touchend', handleDragEnd)
document.addEventListener('mousemove', handleDrag)
document.addEventListener('mouseup', handleDragEnd)
return () => {
document.removeEventListener('touchmove', handleDrag)
document.removeEventListener('touchend', handleDragEnd)
document.removeEventListener('mousemove', handleDrag)
document.removeEventListener('mouseup', handleDragEnd)
}
}, [handleDrag, handleDragEnd])

return (
<Flex
// mouse events to enable local development
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
ref={(ref: HTMLDivElement | SVGAElement | null) => {
parentRef.current = ref
}}
justifyContent={JUSTIFY_CENTER}
width="100%"
>
<Flex width="75%">{children}</Flex>
</Flex>
)
}
Loading

0 comments on commit f4ecce4

Please sign in to comment.