Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app): create well selection component #15207

Merged
merged 17 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading