diff --git a/protocol-designer/src/components/DeckSetup/LabwareOnDeck.tsx b/protocol-designer/src/components/DeckSetup/LabwareOnDeck.tsx
index 390346793b4..1bc83f7e752 100644
--- a/protocol-designer/src/components/DeckSetup/LabwareOnDeck.tsx
+++ b/protocol-designer/src/components/DeckSetup/LabwareOnDeck.tsx
@@ -1,72 +1,48 @@
import * as React from 'react'
-import { connect } from 'react-redux'
-import { LabwareRender, WellGroup } from '@opentrons/components'
+import { useSelector } from 'react-redux'
+import { LabwareRender } from '@opentrons/components'
import { selectors } from '../../labware-ingred/selectors'
import * as wellContentsSelectors from '../../top-selectors/well-contents'
import * as highlightSelectors from '../../top-selectors/substep-highlight'
import * as tipContentsSelectors from '../../top-selectors/tip-contents'
import { LabwareOnDeck as LabwareOnDeckType } from '../../step-forms'
-import { ContentsByWell } from '../../labware-ingred/types'
-import { BaseState } from '../../types'
import { wellFillFromWellContents } from '../labware/utils'
-interface OP {
+interface LabwareOnDeckProps {
className?: string
labwareOnDeck: LabwareOnDeckType
x: number
y: number
}
-interface SP {
- wellContents: ContentsByWell
- liquidDisplayColors: string[]
- missingTips?: WellGroup | null
- highlightedWells?: WellGroup | null
-}
-
-type Props = OP & SP
-
-const LabwareOnDeckComponent = (props: Props): JSX.Element => (
-
-
-
-)
-
-const mapStateToProps = (state: BaseState, ownProps: OP): SP => {
- const { labwareOnDeck } = ownProps
-
- const missingTipsByLabwareId = tipContentsSelectors.getMissingTipsByLabwareId(
- state
+export function LabwareOnDeck(props: LabwareOnDeckProps): JSX.Element {
+ const { labwareOnDeck, x, y, className } = props
+ const missingTipsByLabwareId = useSelector(
+ tipContentsSelectors.getMissingTipsByLabwareId
)
-
- const allWellContentsForActiveItem = wellContentsSelectors.getAllWellContentsForActiveItem(
- state
+ const allWellContentsForActiveItem = useSelector(
+ wellContentsSelectors.getAllWellContentsForActiveItem
+ )
+ const allHighlightedWells = useSelector(
+ highlightSelectors.wellHighlightsByLabwareId
+ )
+ const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors)
+ const wellContents = allWellContentsForActiveItem
+ ? allWellContentsForActiveItem[labwareOnDeck.id]
+ : null
+ const highlightedWells = allHighlightedWells[labwareOnDeck.id]
+ const missingTips = missingTipsByLabwareId
+ ? missingTipsByLabwareId[labwareOnDeck.id]
+ : null
+ return (
+
+
+
)
-
- return {
- wellContents: allWellContentsForActiveItem
- ? allWellContentsForActiveItem[labwareOnDeck.id]
- : null,
- highlightedWells: highlightSelectors.wellHighlightsByLabwareId(state)[
- labwareOnDeck.id
- ],
- missingTips: missingTipsByLabwareId
- ? missingTipsByLabwareId[labwareOnDeck.id]
- : null,
- liquidDisplayColors: selectors.getLiquidDisplayColors(state),
- }
}
-
-export const LabwareOnDeck = connect(mapStateToProps)(LabwareOnDeckComponent)
diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx
index c923df5af15..7b1fb2b7433 100644
--- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx
+++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { connect } from 'react-redux'
+import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { css } from 'styled-components'
import {
@@ -23,7 +23,7 @@ import { NameThisLabware } from './NameThisLabware'
import styles from './LabwareOverlays.css'
import type { LabwareEntity } from '@opentrons/step-generation'
-import type { BaseState, ThunkDispatch } from '../../../types'
+import type { ThunkDispatch } from '../../../types'
const NAME_LABWARE_OVERLAY_STYLE = css`
z-index: 1;
@@ -58,30 +58,26 @@ const REGULAR_OVERLAY_STYLE = css`
}
`
-interface OP {
+interface EditLabwareOffDeckProps {
labwareEntity: LabwareEntity
}
-interface SP {
- isYetUnnamed: boolean
-}
-interface DP {
- editLiquids: () => void
- duplicateLabware: () => void
- deleteLabware: () => void
-}
-
-type Props = OP & SP & DP
-const EditLabwareOffDeckComponent = (props: Props): JSX.Element => {
- const {
- labwareEntity,
- isYetUnnamed,
- editLiquids,
- deleteLabware,
- duplicateLabware,
- } = props
+export function EditLabwareOffDeck(
+ props: EditLabwareOffDeckProps
+): JSX.Element {
+ const { labwareEntity } = props
const { t } = useTranslation('deck')
+ const dispatch = useDispatch>()
+ const allSavedLabware = useSelector(labwareIngredSelectors.getSavedLabware)
+ const hasName = allSavedLabware[labwareEntity.id]
const { isTiprack } = labwareEntity.def.parameters
+
+ const isYetUnnamed = isTiprack && !hasName
+
+ const editLiquids = (): void => {
+ dispatch(openIngredientSelector(labwareEntity.id))
+ }
+
if (isYetUnnamed && !isTiprack) {
return (
@@ -102,11 +98,23 @@ const EditLabwareOffDeckComponent = (props: Props): JSX.Element => {
) : (
)}
-
+ dispatch(duplicateLabware(labwareEntity.id))}
+ >
{t('overlay.edit.duplicate')}
-
+ {
+ window.confirm(
+ t('warning.cancelForSure', {
+ adapterName: getLabwareDisplayName(labwareEntity.def),
+ })
+ ) && dispatch(deleteContainer({ labwareId: labwareEntity.id }))
+ }}
+ >
{t('overlay.edit.delete')}
@@ -114,32 +122,3 @@ const EditLabwareOffDeckComponent = (props: Props): JSX.Element => {
)
}
}
-
-const mapStateToProps = (state: BaseState, ownProps: OP): SP => {
- const { id } = ownProps.labwareEntity
- const hasName = labwareIngredSelectors.getSavedLabware(state)[id]
- return {
- isYetUnnamed: !ownProps.labwareEntity.def.parameters.isTiprack && !hasName,
- }
-}
-
-const mapDispatchToProps = (
- dispatch: ThunkDispatch
,
- ownProps: OP
-): DP => ({
- editLiquids: () =>
- dispatch(openIngredientSelector(ownProps.labwareEntity.id)),
- duplicateLabware: () => dispatch(duplicateLabware(ownProps.labwareEntity.id)),
- deleteLabware: () => {
- window.confirm(
- `Are you sure you want to permanently delete this ${getLabwareDisplayName(
- ownProps.labwareEntity.def
- )}?`
- ) && dispatch(deleteContainer({ labwareId: ownProps.labwareEntity.id }))
- },
-})
-
-export const EditLabwareOffDeck = connect(
- mapStateToProps,
- mapDispatchToProps
-)(EditLabwareOffDeckComponent)
diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareName.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareName.tsx
index 4874c83dba2..7efdc0d2949 100644
--- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareName.tsx
+++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareName.tsx
@@ -1,33 +1,19 @@
import * as React from 'react'
-import { connect } from 'react-redux'
+import { useSelector } from 'react-redux'
import { LabwareNameOverlay, truncateString } from '@opentrons/components'
import { getLabwareDisplayName } from '@opentrons/shared-data'
-import { BaseState } from '../../../types'
import { selectors as uiLabwareSelectors } from '../../../ui/labware'
import { LabwareOnDeck } from '../../../step-forms'
-interface OP {
+interface LabwareNameProps {
labwareOnDeck: LabwareOnDeck
}
-interface SP {
- nickname?: string | null
-}
-
-type Props = OP & SP
-
-const NameOverlay = (props: Props): JSX.Element => {
- const { labwareOnDeck, nickname } = props
+export function LabwareName(props: LabwareNameProps): JSX.Element {
+ const { labwareOnDeck } = props
+ const nicknames = useSelector(uiLabwareSelectors.getLabwareNicknamesById)
+ const nickname = nicknames[labwareOnDeck.id]
const truncatedNickName =
nickname != null ? truncateString(nickname, 75, 25) : null
const title = truncatedNickName ?? getLabwareDisplayName(labwareOnDeck.def)
return
}
-
-const mapStateToProps = (state: BaseState, ownProps: OP): SP => {
- const { id } = ownProps.labwareOnDeck
- return {
- nickname: uiLabwareSelectors.getLabwareNicknamesById(state)[id],
- }
-}
-
-export const LabwareName = connect(mapStateToProps)(NameOverlay)
diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/NameThisLabware.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/NameThisLabware.tsx
index 90c9a623244..0a7966bbb92 100644
--- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/NameThisLabware.tsx
+++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/NameThisLabware.tsx
@@ -1,31 +1,32 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-import { connect } from 'react-redux'
+import { useDispatch } from 'react-redux'
import cx from 'classnames'
import { Icon, useOnClickOutside } from '@opentrons/components'
import { renameLabware } from '../../../labware-ingred/actions'
import styles from './LabwareOverlays.css'
+
import type { LabwareEntity } from '@opentrons/step-generation'
import type { ThunkDispatch } from '../../../types'
import type { LabwareOnDeck } from '../../../step-forms'
-interface OP {
+interface NameThisLabwareProps {
labwareOnDeck: LabwareOnDeck | LabwareEntity
- editLiquids: () => unknown
-}
-
-interface DP {
- setLabwareName: (name: string | null | undefined) => unknown
+ editLiquids: () => void
}
-type Props = OP & DP
-
-const NameThisLabwareComponent = (props: Props): JSX.Element => {
- const [inputValue, setInputValue] = React.useState('')
+export function NameThisLabware(props: NameThisLabwareProps): JSX.Element {
+ const { labwareOnDeck } = props
+ const dispatch: ThunkDispatch = useDispatch()
+ const [inputValue, setInputValue] = React.useState('')
const { t } = useTranslation('deck')
+ const setLabwareName = (name: string | null | undefined): void => {
+ dispatch(renameLabware({ labwareId: labwareOnDeck.id, name }))
+ }
+
const saveNickname = (): void => {
- props.setLabwareName(inputValue || null)
+ setLabwareName(inputValue ?? null)
}
const wrapperRef: React.RefObject = useOnClickOutside({
onClickOutside: saveNickname,
@@ -67,16 +68,3 @@ const NameThisLabwareComponent = (props: Props): JSX.Element => {
)
}
-
-const mapDispatchToProps = (dispatch: ThunkDispatch, ownProps: OP): DP => {
- const { id } = ownProps.labwareOnDeck
- return {
- setLabwareName: (name: string | null | undefined) =>
- dispatch(renameLabware({ labwareId: id, name })),
- }
-}
-
-export const NameThisLabware = connect(
- null,
- mapDispatchToProps
-)(NameThisLabwareComponent)
diff --git a/protocol-designer/src/components/EditableTextField.tsx b/protocol-designer/src/components/EditableTextField.tsx
index a43f61041dc..df5d47ed532 100644
--- a/protocol-designer/src/components/EditableTextField.tsx
+++ b/protocol-designer/src/components/EditableTextField.tsx
@@ -9,80 +9,66 @@ interface Props {
saveEdit: (newValue: string) => unknown
}
-interface State {
- editing: boolean
- transientValue?: string | null
-}
+export function EditableTextField(props: Props): JSX.Element {
+ const { className, value, saveEdit } = props
+ const [editing, setEditing] = React.useState(false)
+ const [transientValue, setTransientValue] = React.useState<
+ string | null | undefined
+ >(value)
-export class EditableTextField extends React.Component {
- constructor(props: Props) {
- super(props)
- this.state = {
- editing: false,
- transientValue: this.props.value,
- }
+ const enterEditMode = (): void => {
+ setEditing(true)
+ setTransientValue(value)
}
-
- enterEditMode: () => void = () =>
- this.setState({ editing: true, transientValue: this.props.value })
-
- handleCancel: () => void = () => {
- this.setState({
- editing: false,
- transientValue: this.props.value,
- })
+ const handleCancel = (): void => {
+ setEditing(false)
+ setTransientValue(value)
}
- handleKeyUp: (e: React.KeyboardEvent) => void = e => {
+ const handleKeyUp = (e: React.KeyboardEvent): void => {
if (e.key === 'Escape') {
- this.handleCancel()
+ handleCancel()
}
}
- handleFormSubmit: (e: React.FormEvent) => void = e => {
- e.preventDefault() // avoid 'form is not connected' warning
- this.handleSubmit()
+ const handleSubmit = (): void => {
+ setEditing(false)
+ saveEdit(transientValue ?? '')
}
-
- handleSubmit: () => void = () => {
- this.setState({ editing: false }, () =>
- this.props.saveEdit(this.state.transientValue || '')
- )
+ const handleFormSubmit = (e: React.FormEvent): void => {
+ e.preventDefault() // avoid 'form is not connected' warning
+ handleSubmit()
}
- updateValue: (e: React.ChangeEvent) => void = e => {
- this.setState({ transientValue: e.currentTarget.value })
+ const updateValue = (e: React.ChangeEvent): void => {
+ setTransientValue(e.currentTarget.value)
}
-
- render(): React.ReactNode {
- const { className, value } = this.props
- if (this.state.editing) {
- return (
-
- {({ ref }) => (
-
- )}
-
- )
- }
-
+ if (editing) {
return (
-
+
+ {({ ref }) => (
+
+ )}
+
)
}
+
+ return (
+
+ )
}
diff --git a/protocol-designer/src/components/IngredientsList/LabwareDetailsCard/LabwareDetailsCard.tsx b/protocol-designer/src/components/IngredientsList/LabwareDetailsCard/LabwareDetailsCard.tsx
index f71634702c8..782a471e55f 100644
--- a/protocol-designer/src/components/IngredientsList/LabwareDetailsCard/LabwareDetailsCard.tsx
+++ b/protocol-designer/src/components/IngredientsList/LabwareDetailsCard/LabwareDetailsCard.tsx
@@ -1,18 +1,53 @@
import * as React from 'react'
+import assert from 'assert'
+import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import cx from 'classnames'
+import { getLabwareDisplayName } from '@opentrons/shared-data'
+import { selectors as stepFormSelectors } from '../../../step-forms'
+import { selectors as uiLabwareSelectors } from '../../../ui/labware'
+import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors'
+import * as labwareIngredActions from '../../../labware-ingred/actions'
import { PDTitledList, PDListItem } from '../../lists'
import { EditableTextField } from '../../EditableTextField'
-import styles from './labwareDetailsCard.css'
+import type { ThunkDispatch } from '../../../types'
-export interface Props {
- labwareDefDisplayName: string
- nickname: string
- renameLabware: (name: string) => unknown
-}
+import styles from './labwareDetailsCard.css'
-export function LabwareDetailsCard(props: Props): JSX.Element {
+export function LabwareDetailsCard(): JSX.Element {
const { t } = useTranslation('form')
+ const dispatch = useDispatch>()
+ const labwareNicknamesById = useSelector(
+ uiLabwareSelectors.getLabwareNicknamesById
+ )
+ const labwareId = useSelector(labwareIngredSelectors.getSelectedLabwareId)
+ const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities)
+ const labwareDefDisplayName =
+ labwareId != null
+ ? getLabwareDisplayName(labwareEntities[labwareId].def)
+ : null
+
+ assert(
+ labwareId,
+ 'Expected labware id to exist in connected labware details card'
+ )
+
+ const renameLabware = (name: string): void => {
+ assert(
+ labwareId,
+ 'renameLabware in LabwareDetailsCard expected a labwareId'
+ )
+
+ if (labwareId) {
+ dispatch(
+ labwareIngredActions.renameLabware({
+ labwareId: labwareId,
+ name,
+ })
+ )
+ }
+ }
+
return (
@@ -20,9 +55,7 @@ export function LabwareDetailsCard(props: Props): JSX.Element {
{t('generic.labware_type')}
-
- {props.labwareDefDisplayName}
-
+ {labwareDefDisplayName}
@@ -31,8 +64,8 @@ export function LabwareDetailsCard(props: Props): JSX.Element {
{t('generic.nickname')}
diff --git a/protocol-designer/src/components/IngredientsList/LabwareDetailsCard/index.ts b/protocol-designer/src/components/IngredientsList/LabwareDetailsCard/index.ts
deleted file mode 100644
index a230372e7aa..00000000000
--- a/protocol-designer/src/components/IngredientsList/LabwareDetailsCard/index.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { connect } from 'react-redux'
-import assert from 'assert'
-import { getLabwareDisplayName } from '@opentrons/shared-data'
-import {
- LabwareDetailsCard as LabwareDetailsCardComponent,
- Props as LabwareDetailsCardProps,
-} from './LabwareDetailsCard'
-import { selectors as stepFormSelectors } from '../../../step-forms'
-import { selectors as uiLabwareSelectors } from '../../../ui/labware'
-import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors'
-import * as labwareIngredActions from '../../../labware-ingred/actions'
-import { BaseState, ThunkDispatch } from '../../../types'
-type SP = Omit & {
- _labwareId?: string
-}
-
-function mapStateToProps(state: BaseState): SP {
- const labwareNicknamesById = uiLabwareSelectors.getLabwareNicknamesById(state)
- const labwareId = labwareIngredSelectors.getSelectedLabwareId(state)
- const labwareDefDisplayName =
- labwareId &&
- getLabwareDisplayName(
- stepFormSelectors.getLabwareEntities(state)[labwareId].def
- )
- assert(
- labwareId,
- 'Expected labware id to exist in connected labware details card'
- )
-
- if (!labwareId || !labwareDefDisplayName) {
- return {
- labwareDefDisplayName: '?',
- nickname: '?',
- }
- }
-
- return {
- labwareDefDisplayName,
- nickname: labwareNicknamesById[labwareId] || 'Unnamed Labware',
- _labwareId: labwareId,
- }
-}
-
-function mergeProps(
- stateProps: SP,
- dispatchProps: {
- dispatch: ThunkDispatch
- }
-): LabwareDetailsCardProps {
- const dispatch = dispatchProps.dispatch
- const { _labwareId, ...passThruProps } = stateProps
-
- const renameLabware = (name: string): void => {
- assert(
- _labwareId,
- 'renameLabware in LabwareDetailsCard expected a labwareId'
- )
-
- if (_labwareId) {
- dispatch(
- labwareIngredActions.renameLabware({
- labwareId: _labwareId,
- name,
- })
- )
- }
- }
-
- return { ...passThruProps, renameLabware }
-}
-
-export const LabwareDetailsCard = connect(
- mapStateToProps,
- null,
- mergeProps
-)(LabwareDetailsCardComponent)
diff --git a/protocol-designer/src/components/IngredientsList/index.tsx b/protocol-designer/src/components/IngredientsList/index.tsx
index 721dfd36b78..e95f1245f6f 100644
--- a/protocol-designer/src/components/IngredientsList/index.tsx
+++ b/protocol-designer/src/components/IngredientsList/index.tsx
@@ -1,18 +1,23 @@
// TODO: Ian 2018-10-09 figure out what belongs in LiquidsSidebar vs IngredientsList after #2427
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-import { useSelector } from 'react-redux'
+import { useDispatch, useSelector } from 'react-redux'
import { SingleLabwareLiquidState } from '@opentrons/step-generation'
import { IconButton, SidePanel, truncateString } from '@opentrons/components'
import { sortWells } from '@opentrons/shared-data'
-import { selectors } from '../../labware-ingred/selectors'
+import * as wellSelectionSelectors from '../../top-selectors/well-contents'
+import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
import { PDTitledList, PDListItem } from '../lists'
import { TitledListNotes } from '../TitledListNotes'
import { swatchColors } from '../swatchColors'
-import { LabwareDetailsCard } from './LabwareDetailsCard'
-import { LiquidGroupsById, LiquidGroup } from '../../labware-ingred/types'
+import { LabwareDetailsCard } from './LabwareDetailsCard/LabwareDetailsCard'
+
import styles from './IngredientsList.css'
+import type { SelectedContainerId } from '../../labware-ingred/reducers'
+import type { LiquidGroup } from '../../labware-ingred/types'
+import type { ThunkDispatch } from '../../types'
+
type RemoveWellsContents = (args: {
liquidGroupId: string
wells: string[]
@@ -48,7 +53,9 @@ const LiquidGroupCard = (props: LiquidGroupCardProps): JSX.Element | null => {
const wellsWithIngred = Object.keys(labwareWellContents)
.sort(sortWells)
.filter(well => labwareWellContents[well][groupId])
- const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors)
+ const liquidDisplayColors = useSelector(
+ labwareIngredSelectors.getLiquidDisplayColors
+ )
if (wellsWithIngred.length < 1) {
// do not show liquid card if it has no instances for this labware
@@ -147,20 +154,33 @@ function IngredIndividual(props: IndividProps): JSX.Element {
)
}
-type Props = CommonProps & {
- liquidGroupsById: LiquidGroupsById
- labwareWellContents: SingleLabwareLiquidState
- selectedIngredientGroupId?: string | null
-}
+export function IngredientsList(): JSX.Element {
+ const selectedLabwareId = useSelector(
+ labwareIngredSelectors.getSelectedLabwareId
+ )
+ const allLabwareWellContents = useSelector(
+ labwareIngredSelectors.getLiquidsByLabwareId
+ )
-export function IngredientsList(props: Props): JSX.Element {
- const {
- labwareWellContents,
- liquidGroupsById,
- removeWellsContents,
- selectedIngredientGroupId,
- } = props
+ const liquidGroupsById = useSelector(
+ labwareIngredSelectors.getLiquidGroupsById
+ )
+ const selectedIngredientGroupId = useSelector(
+ wellSelectionSelectors.getSelectedWellsCommonIngredId
+ )
const { t } = useTranslation('nav')
+ const dispatch = useDispatch>()
+
+ const labwareWellContents =
+ (selectedLabwareId && allLabwareWellContents[selectedLabwareId]) || {}
+
+ const removeWellsContents = (
+ labwareId?: SelectedContainerId | null
+ ): void => {
+ if (labwareId != null) {
+ dispatch(removeWellsContents(selectedLabwareId))
+ }
+ }
return (
@@ -168,7 +188,7 @@ export function IngredientsList(props: Props): JSX.Element {
{Object.keys(liquidGroupsById).map(groupIdForCard => (
removeWellsContents(selectedLabwareId)}
labwareWellContents={labwareWellContents}
ingredGroup={liquidGroupsById[groupIdForCard]}
groupId={groupIdForCard}
diff --git a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx
index 2491108d6e5..134a6fd33ea 100644
--- a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx
+++ b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx
@@ -1,4 +1,5 @@
import * as React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import startCase from 'lodash/startCase'
import reduce from 'lodash/reduce'
@@ -23,13 +24,26 @@ import {
ModuleModel,
getModuleType,
THERMOCYCLER_MODULE_V2,
+ getAreSlotsHorizontallyAdjacent,
} from '@opentrons/shared-data'
+import {
+ closeLabwareSelector,
+ createContainer,
+} from '../../labware-ingred/actions'
+import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
+import {
+ actions as labwareDefActions,
+ selectors as labwareDefSelectors,
+} from '../../labware-defs'
+import { selectors as stepFormSelectors, ModuleOnDeck } from '../../step-forms'
import { SPAN7_8_10_11_SLOT } from '../../constants'
import {
getLabwareIsCompatible as _getLabwareIsCompatible,
getLabwareCompatibleWithAdapter,
ADAPTER_96_CHANNEL,
} from '../../utils/labwareModuleCompatibility'
+import { getPipetteEntities } from '../../step-forms/selectors'
+import { getHas96Channel } from '../../utils'
import { getOnlyLatestDefs } from '../../labware-defs/utils'
import { Portal } from '../portals/TopPortal'
import { PDTitledList } from '../lists'
@@ -39,7 +53,7 @@ import { LabwareItem } from './LabwareItem'
import { LabwarePreview } from './LabwarePreview'
import styles from './styles.css'
-import type { DeckSlot } from '../../types'
+import type { DeckSlot, ThunkDispatch } from '../../types'
import type { LabwareDefByDefURI } from '../../labware-defs'
export interface Props {
@@ -127,21 +141,61 @@ export const getLabwareIsRecommended = (
: false
}
}
-export const LabwareSelectionModal = (props: Props): JSX.Element | null => {
- const {
- customLabwareDefs,
- permittedTipracks,
- onClose,
- onUploadLabware,
- slot,
- parentSlot,
- moduleModel,
- selectLabware,
- isNextToHeaterShaker,
- adapterLoadName,
- has96Channel,
- } = props
+export function LabwareSelectionModal(): JSX.Element | null {
const { t } = useTranslation(['modules', 'modal', 'button', 'alert'])
+ const dispatch = useDispatch>()
+ const selectedLabwareSlot = useSelector(
+ labwareIngredSelectors.selectedAddLabwareSlot
+ )
+ const pipetteEntities = useSelector(getPipetteEntities)
+ const permittedTipracks = useSelector(stepFormSelectors.getPermittedTipracks)
+ const customLabwareDefs = useSelector(
+ labwareDefSelectors.getCustomLabwareDefsByURI
+ )
+ const deckSetup = useSelector(stepFormSelectors.getInitialDeckSetup)
+ const has96Channel = getHas96Channel(pipetteEntities)
+ const modulesById = deckSetup.modules
+ const labwareById = deckSetup.labware
+ const slot = selectedLabwareSlot === false ? null : selectedLabwareSlot
+
+ const onClose = (): void => {
+ dispatch(closeLabwareSelector())
+ }
+ const selectLabware = (labwareDefURI: string): void => {
+ if (slot) {
+ dispatch(
+ createContainer({
+ slot: slot,
+ labwareDefURI,
+ })
+ )
+ }
+ }
+
+ const onUploadLabware = (
+ fileChangeEvent: React.ChangeEvent
+ ): void => {
+ dispatch(labwareDefActions.createCustomLabwareDef(fileChangeEvent))
+ }
+
+ const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map(
+ moduleId => modulesById[moduleId]
+ )
+ const parentModule =
+ (slot != null &&
+ initialModules.find(moduleOnDeck => moduleOnDeck.id === slot)) ||
+ null
+ const parentSlot = parentModule != null ? parentModule.slot : null
+ const moduleModel = parentModule != null ? parentModule.model : null
+ const isNextToHeaterShaker = initialModules.some(
+ hardwareModule =>
+ hardwareModule.type === HEATERSHAKER_MODULE_TYPE &&
+ getAreSlotsHorizontallyAdjacent(hardwareModule.slot, parentSlot ?? slot)
+ )
+ const adapterLoadName = Object.values(labwareById)
+ .filter(labwareOnDeck => slot === labwareOnDeck.id)
+ .map(labwareOnDeck => labwareOnDeck.def.parameters.loadName)[0]
+
const defs = getOnlyLatestDefs()
const moduleType = moduleModel != null ? getModuleType(moduleModel) : null
const URIs = Object.keys(defs)
@@ -154,10 +208,12 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => {
const [filterRecommended, setFilterRecommended] = React.useState(
false
)
+
const [filterHeight, setFilterHeight] = React.useState(false)
const [enqueuedLabwareType, setEnqueuedLabwareType] = React.useState<
string | null
>(null)
+
const blockingCustomLabwareHint = useBlockingHint({
enabled: enqueuedLabwareType !== null,
hintKey: 'custom_labware_with_modules',
diff --git a/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx b/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx
index 64348cc4ba0..c17c2ad42d7 100644
--- a/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx
+++ b/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx
@@ -1,19 +1,31 @@
import * as React from 'react'
-import i18next from 'i18next'
import { fireEvent, screen } from '@testing-library/react'
import { renderWithProviders, nestedTextMatcher } from '@opentrons/components'
import {
getIsLabwareAboveHeight,
MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM,
} from '@opentrons/shared-data'
+import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors'
import {
ADAPTER_96_CHANNEL,
getLabwareCompatibleWithAdapter,
} from '../../../utils/labwareModuleCompatibility'
+import { i18n } from '../../../localization'
import { LabwareSelectionModal } from '../LabwareSelectionModal'
+import {
+ getInitialDeckSetup,
+ getPermittedTipracks,
+ getPipetteEntities,
+} from '../../../step-forms/selectors'
+import { getHas96Channel } from '../../../utils'
+import { getCustomLabwareDefsByURI } from '../../../labware-defs/selectors'
jest.mock('../../../utils/labwareModuleCompatibility')
+jest.mock('../../../step-forms/selectors')
+jest.mock('../../../labware-defs/selectors')
jest.mock('../../Hints/useBlockingHint')
+jest.mock('../../../utils')
+jest.mock('../../../labware-ingred/selectors')
jest.mock('@opentrons/shared-data', () => {
const actualSharedData = jest.requireActual('@opentrons/shared-data')
return {
@@ -28,47 +40,98 @@ const mockGetIsLabwareAboveHeight = getIsLabwareAboveHeight as jest.MockedFuncti
const mockGetLabwareCompatibleWithAdapter = getLabwareCompatibleWithAdapter as jest.MockedFunction<
typeof getLabwareCompatibleWithAdapter
>
-const render = (props: React.ComponentProps) => {
- return renderWithProviders(, {
- i18nInstance: i18next,
+const mockGetInitialDeckSetup = getInitialDeckSetup as jest.MockedFunction<
+ typeof getInitialDeckSetup
+>
+const mockSlot = labwareIngredSelectors.selectedAddLabwareSlot as jest.MockedFunction<
+ typeof labwareIngredSelectors.selectedAddLabwareSlot
+>
+const mockGetHas96Channel = getHas96Channel as jest.MockedFunction<
+ typeof getHas96Channel
+>
+const mockGetPipetteEntities = getPipetteEntities as jest.MockedFunction<
+ typeof getPipetteEntities
+>
+const mockGetPermittedTipracks = getPermittedTipracks as jest.MockedFunction<
+ typeof getPermittedTipracks
+>
+const mockGetCustomLabwareDefsByURI = getCustomLabwareDefsByURI as jest.MockedFunction<
+ typeof getCustomLabwareDefsByURI
+>
+const render = () => {
+ return renderWithProviders(, {
+ i18nInstance: i18n,
})[0]
}
+const mockTipUri = 'fixture/fixture_tiprack_1000_ul/1'
+const mockPermittedTipracks = [mockTipUri]
+
describe('LabwareSelectionModal', () => {
- let props: React.ComponentProps
beforeEach(() => {
- props = {
- onClose: jest.fn(),
- onUploadLabware: jest.fn(),
- selectLabware: jest.fn(),
- customLabwareDefs: {},
- permittedTipracks: [],
- isNextToHeaterShaker: false,
- has96Channel: false,
- }
mockGetLabwareCompatibleWithAdapter.mockReturnValue([])
+ mockGetInitialDeckSetup.mockReturnValue({
+ labware: {},
+ modules: {},
+ pipettes: {},
+ additionalEquipmentOnDeck: {},
+ })
+ mockSlot.mockReturnValue('2')
+ mockGetHas96Channel.mockReturnValue(false)
+ mockGetPermittedTipracks.mockReturnValue(mockPermittedTipracks)
+ mockGetPipetteEntities.mockReturnValue({
+ mockPip: {
+ tiprackLabwareDef: {} as any,
+ spec: {} as any,
+ name: 'p1000_single',
+ id: 'mockId',
+ tiprackDefURI: mockTipUri,
+ },
+ })
+ mockGetCustomLabwareDefsByURI.mockReturnValue({})
})
it('should NOT filter out labware above 57 mm when the slot is NOT next to a heater shaker', () => {
- props.isNextToHeaterShaker = false
- render(props)
+ render()
expect(mockGetIsLabwareAboveHeight).not.toHaveBeenCalled()
})
it('should filter out labware above 57 mm when the slot is next to a heater shaker', () => {
- props.isNextToHeaterShaker = true
- render(props)
+ mockGetInitialDeckSetup.mockReturnValue({
+ labware: {},
+ modules: {
+ heaterShaker: {
+ id: 'mockId',
+ type: 'heaterShakerModuleType',
+ model: 'heaterShakerModuleV1',
+ moduleState: {} as any,
+ slot: '1',
+ } as any,
+ },
+ pipettes: {},
+ additionalEquipmentOnDeck: {},
+ })
+ render()
expect(mockGetIsLabwareAboveHeight).toHaveBeenCalledWith(
expect.any(Object),
MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM
)
})
it('should display only permitted tipracks if the 96-channel is attached', () => {
- const mockTipUri = 'fixture/fixture_tiprack_1000_ul/1'
- const mockPermittedTipracks = [mockTipUri]
- props.slot = 'A2'
- props.has96Channel = true
- props.adapterLoadName = ADAPTER_96_CHANNEL
- props.permittedTipracks = mockPermittedTipracks
- render(props)
+ mockGetHas96Channel.mockReturnValue(true)
+ mockSlot.mockReturnValue('adapter')
+ mockGetInitialDeckSetup.mockReturnValue({
+ labware: {
+ adapter: {
+ id: 'adapter',
+ labwareDefURI: `opentrons/${ADAPTER_96_CHANNEL}/1`,
+ slot: 'A2',
+ def: { parameters: { loadName: ADAPTER_96_CHANNEL } } as any,
+ },
+ },
+ modules: {},
+ pipettes: {},
+ additionalEquipmentOnDeck: {},
+ })
+ render()
fireEvent.click(
screen.getByText(nestedTextMatcher('adapter compatible labware'))
)
diff --git a/protocol-designer/src/components/LabwareSelectionModal/index.ts b/protocol-designer/src/components/LabwareSelectionModal/index.ts
deleted file mode 100644
index f60c2bf95d7..00000000000
--- a/protocol-designer/src/components/LabwareSelectionModal/index.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { connect } from 'react-redux'
-import {
- getAreSlotsHorizontallyAdjacent,
- HEATERSHAKER_MODULE_TYPE,
-} from '@opentrons/shared-data'
-import {
- LabwareSelectionModal as LabwareSelectionModalComponent,
- Props as LabwareSelectionModalProps,
-} from './LabwareSelectionModal'
-import {
- closeLabwareSelector,
- createContainer,
-} from '../../labware-ingred/actions'
-import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
-import {
- actions as labwareDefActions,
- selectors as labwareDefSelectors,
-} from '../../labware-defs'
-import { selectors as stepFormSelectors, ModuleOnDeck } from '../../step-forms'
-import { getHas96Channel } from '../../utils'
-import { getPipetteEntities } from '../../step-forms/selectors'
-import { adapter96ChannelDefUri } from '../modals/CreateFileWizard'
-import type { BaseState, ThunkDispatch } from '../../types'
-interface SP {
- customLabwareDefs: LabwareSelectionModalProps['customLabwareDefs']
- slot: LabwareSelectionModalProps['slot']
- parentSlot: LabwareSelectionModalProps['parentSlot']
- moduleModel: LabwareSelectionModalProps['moduleModel']
- permittedTipracks: LabwareSelectionModalProps['permittedTipracks']
- isNextToHeaterShaker: boolean
- has96Channel: boolean
- adapterDefUri?: string
- adapterLoadName?: string
-}
-
-function mapStateToProps(state: BaseState): SP {
- const slot = labwareIngredSelectors.selectedAddLabwareSlot(state) || null
- const pipettes = getPipetteEntities(state)
- const has96Channel = getHas96Channel(pipettes)
-
- // TODO: Ian 2019-10-29 needs revisit to support multiple manualIntervention steps
- const modulesById = stepFormSelectors.getInitialDeckSetup(state).modules
- const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map(
- moduleId => modulesById[moduleId]
- )
- const labwareById = stepFormSelectors.getInitialDeckSetup(state).labware
- const parentModule =
- (slot != null &&
- initialModules.find(moduleOnDeck => moduleOnDeck.id === slot)) ||
- null
- const parentSlot = parentModule != null ? parentModule.slot : null
- const moduleModel = parentModule != null ? parentModule.model : null
- const isNextToHeaterShaker = initialModules.some(
- hardwareModule =>
- hardwareModule.type === HEATERSHAKER_MODULE_TYPE &&
- getAreSlotsHorizontallyAdjacent(hardwareModule.slot, parentSlot ?? slot)
- )
- const adapterLoadNameOnDeck = Object.values(labwareById)
- .filter(labwareOnDeck => slot === labwareOnDeck.id)
- .map(labwareOnDeck => labwareOnDeck.def.parameters.loadName)[0]
-
- return {
- customLabwareDefs: labwareDefSelectors.getCustomLabwareDefsByURI(state),
- slot,
- parentSlot,
- moduleModel,
- isNextToHeaterShaker,
- has96Channel,
- adapterDefUri: has96Channel ? adapter96ChannelDefUri : undefined,
- permittedTipracks: stepFormSelectors.getPermittedTipracks(state),
- adapterLoadName: adapterLoadNameOnDeck,
- }
-}
-
-function mergeProps(
- stateProps: SP,
- dispatchProps: {
- dispatch: ThunkDispatch
- }
-): LabwareSelectionModalProps {
- const dispatch = dispatchProps.dispatch
- return {
- ...stateProps,
- onClose: () => {
- dispatch(closeLabwareSelector())
- },
- onUploadLabware: fileChangeEvent =>
- dispatch(labwareDefActions.createCustomLabwareDef(fileChangeEvent)),
- selectLabware: labwareDefURI => {
- if (stateProps.slot) {
- dispatch(
- createContainer({
- slot: stateProps.slot,
- labwareDefURI,
- })
- )
- }
- },
- }
-}
-
-export const LabwareSelectionModal = connect(
- mapStateToProps,
- null,
- mergeProps
-)(LabwareSelectionModalComponent)
diff --git a/protocol-designer/src/components/LiquidPlacementModal.tsx b/protocol-designer/src/components/LiquidPlacementModal.tsx
index 0739b6d740e..da6724e5d33 100644
--- a/protocol-designer/src/components/LiquidPlacementModal.tsx
+++ b/protocol-designer/src/components/LiquidPlacementModal.tsx
@@ -1,140 +1,81 @@
-import assert from 'assert'
import * as React from 'react'
-import { connect } from 'react-redux'
+import assert from 'assert'
+import { useDispatch, useSelector } from 'react-redux'
import cx from 'classnames'
import isEmpty from 'lodash/isEmpty'
import { WellGroup, WELL_LABEL_OPTIONS } from '@opentrons/components'
+
import {
wellFillFromWellContents,
SelectableLabware,
} from '../components/labware'
-import { LiquidPlacementForm } from './LiquidPlacementForm/LiquidPlacementForm'
-import { WellSelectionInstructions } from './WellSelectionInstructions'
-
import { selectors } from '../labware-ingred/selectors'
import { selectors as stepFormSelectors } from '../step-forms'
import * as wellContentsSelectors from '../top-selectors/well-contents'
import { getSelectedWells } from '../well-selection/selectors'
import { selectWells, deselectWells } from '../well-selection/actions'
+import { LiquidPlacementForm } from './LiquidPlacementForm/LiquidPlacementForm'
+import { WellSelectionInstructions } from './WellSelectionInstructions'
import styles from './LiquidPlacementModal.css'
-import { Dispatch } from 'redux'
-import { LabwareDefinition2 } from '@opentrons/shared-data'
-import { BaseState } from '../types'
-import { ContentsByWell } from '../labware-ingred/types'
-import { WellIngredientNames } from '../steplist'
-
-interface SP {
- selectedWells: WellGroup
- wellContents: ContentsByWell
- labwareDef?: LabwareDefinition2 | null
- liquidNamesById: WellIngredientNames
- liquidDisplayColors: string[]
-}
-
-interface DP {
- selectWells: (wellGroup: WellGroup) => unknown
- deselectWells: (wellGroup: WellGroup) => unknown
-}
-
-type Props = SP & DP
-
-interface State {
- highlightedWells: WellGroup
-}
-
-class LiquidPlacementModalComponent extends React.Component {
- state = { highlightedWells: {} }
- constructor(props: Props) {
- super(props)
- this.state = { highlightedWells: {} }
- }
-
- updateHighlightedWells = (wells: WellGroup): void => {
- this.setState({ highlightedWells: wells })
- }
-
- render(): JSX.Element {
- const { labwareDef, selectedWells, liquidDisplayColors } = this.props
-
- return (
-
-
-
- {labwareDef && (
-
-
-
- )}
-
-
-
- )
- }
-}
-
-const mapStateToProps = (state: BaseState): SP => {
- const labwareId = selectors.getSelectedLabwareId(state)
- const selectedWells = getSelectedWells(state)
+export function LiquidPlacementModal(): JSX.Element {
+ const [highlightedWells, setHighlightedWells] = React.useState<
+ WellGroup | {}
+ >({})
+ const labwareId = useSelector(selectors.getSelectedLabwareId)
+ const selectedWells = useSelector(getSelectedWells)
+ const dispatch = useDispatch()
+ const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities)
+ const allWellContents = useSelector(
+ wellContentsSelectors.getWellContentsAllLabware
+ )
+ const liquidNamesById = useSelector(selectors.getLiquidNamesById)
+ const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors)
if (labwareId == null) {
assert(
false,
'LiquidPlacementModal: No labware is selected, and no labwareId was given to LiquidPlacementModal'
)
- return {
- selectedWells: {},
- wellContents: null,
- labwareDef: null,
- liquidNamesById: {},
- liquidDisplayColors: [],
- }
}
- const labwareDef = stepFormSelectors.getLabwareEntities(state)[labwareId]?.def
- let wellContents: ContentsByWell = null
-
- // selection for deck setup: shows initial state of liquids
- wellContents = wellContentsSelectors.getWellContentsAllLabware(state)[
- labwareId
- ]
-
- return {
- selectedWells,
- wellContents,
- labwareDef,
- liquidNamesById: selectors.getLiquidNamesById(state),
- liquidDisplayColors: selectors.getLiquidDisplayColors(state),
- }
+ const labwareDef = labwareEntities[labwareId]?.def
+ const wellContents = allWellContents[labwareId]
+
+ return (
+
+
+
+ {labwareDef && (
+
+ dispatch(selectWells(wells))}
+ deselectWells={(wells: WellGroup) => dispatch(deselectWells(wells))}
+ updateHighlightedWells={(wells: WellGroup) =>
+ setHighlightedWells(wells)
+ }
+ ingredNames={liquidNamesById}
+ wellContents={wellContents}
+ nozzleType={null}
+ />
+
+ )}
+
+
+
+ )
}
-
-const mapDispatchToProps = (dispatch: Dispatch): DP => ({
- deselectWells: wells => dispatch(deselectWells(wells)),
- selectWells: wells => dispatch(selectWells(wells)),
-})
-
-export const LiquidPlacementModal = connect(
- mapStateToProps,
- mapDispatchToProps
-)(LiquidPlacementModalComponent)
diff --git a/protocol-designer/src/components/LiquidsPage/index.tsx b/protocol-designer/src/components/LiquidsPage/index.tsx
index 7aa2c33c722..13c8c4392c9 100644
--- a/protocol-designer/src/components/LiquidsPage/index.tsx
+++ b/protocol-designer/src/components/LiquidsPage/index.tsx
@@ -1,104 +1,70 @@
import * as React from 'react'
-import { connect } from 'react-redux'
+import { useDispatch, useSelector } from 'react-redux'
import assert from 'assert'
-import { LiquidEditForm } from './LiquidEditForm'
-import { LiquidsPageInfo } from './LiquidsPageInfo'
import * as labwareIngredActions from '../../labware-ingred/actions'
import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
+import { LiquidEditForm } from './LiquidEditForm'
+import { LiquidsPageInfo } from './LiquidsPageInfo'
-import { LiquidGroup } from '../../labware-ingred/types'
-import { BaseState, ThunkDispatch } from '../../types'
-
-type Props = React.ComponentProps
-interface WrapperProps {
- showForm: boolean
- formKey: string
- formProps: Props
-}
-
-type SP = LiquidGroup & {
- _liquidGroupId?: string | null
- showForm: boolean
- canDelete: Props['canDelete']
-}
+import type { LiquidGroup } from '../../labware-ingred/types'
+import type { ThunkDispatch } from '../../types'
-function LiquidEditFormWrapper(props: WrapperProps): JSX.Element {
- const { showForm, formKey, formProps } = props
- return showForm ? (
-
- ) : (
-
+export function LiquidsPage(): JSX.Element {
+ const dispatch = useDispatch>()
+ const selectedLiquidGroupState = useSelector(
+ labwareIngredSelectors.getSelectedLiquidGroupState
)
-}
-
-function mapStateToProps(state: BaseState): SP {
- const selectedLiquidGroupState = labwareIngredSelectors.getSelectedLiquidGroupState(
- state
+ const allIngredientGroupFields = useSelector(
+ labwareIngredSelectors.allIngredientGroupFields
)
- const _liquidGroupId =
+
+ const liquidGroupId =
selectedLiquidGroupState && selectedLiquidGroupState.liquidGroupId
- const allIngredientGroupFields = labwareIngredSelectors.allIngredientGroupFields(
- state
- )
- const selectedIngredFields = _liquidGroupId
- ? allIngredientGroupFields[_liquidGroupId]
- : {}
+ const selectedIngredFields =
+ liquidGroupId != null ? allIngredientGroupFields[liquidGroupId] : null
const showForm = Boolean(
selectedLiquidGroupState.liquidGroupId ||
selectedLiquidGroupState.newLiquidGroup
)
+ const formKey = liquidGroupId || '__new_form__'
+
+ const deleteLiquidGroup = (): void => {
+ if (liquidGroupId != null)
+ dispatch(labwareIngredActions.deleteLiquidGroup(liquidGroupId))
+ }
+ const cancelForm = (): void => {
+ dispatch(labwareIngredActions.deselectLiquidGroup())
+ }
+
+ const saveForm = (formData: LiquidGroup): void => {
+ dispatch(
+ labwareIngredActions.editLiquidGroup({
+ ...formData,
+ liquidGroupId: liquidGroupId,
+ })
+ )
+ }
assert(
- !(_liquidGroupId && !selectedIngredFields),
+ !(liquidGroupId && !selectedIngredFields),
`Expected selected liquid group "${String(
- _liquidGroupId
+ liquidGroupId
)}" to have fields in allIngredientGroupFields`
)
- return {
- _liquidGroupId,
- canDelete: _liquidGroupId != null,
- showForm,
- // @ts-expect-error(sa, 2021-6-22): name might not exist
- name: selectedIngredFields.name,
- // @ts-expect-error(sa, 2021-6-22): description might not exist
- description: selectedIngredFields.description,
- // @ts-expect-error(sh, 2022-6-28): displayColor might not exist
- displayColor: selectedIngredFields.displayColor,
- // @ts-expect-error(sa, 2021-6-22): serialize might not exist
- serialize: selectedIngredFields.serialize,
- }
-}
-
-function mergeProps(
- stateProps: SP,
- dispatchProps: { dispatch: ThunkDispatch }
-): WrapperProps {
- const { dispatch } = dispatchProps
- const { showForm, _liquidGroupId, ...passThruFormProps } = stateProps
-
- return {
- showForm,
- formKey: _liquidGroupId || '__new_form__',
- formProps: {
- ...passThruFormProps,
- deleteLiquidGroup: () =>
- _liquidGroupId &&
- dispatch(labwareIngredActions.deleteLiquidGroup(_liquidGroupId)),
- cancelForm: () => dispatch(labwareIngredActions.deselectLiquidGroup()),
- saveForm: (formData: LiquidGroup) =>
- dispatch(
- labwareIngredActions.editLiquidGroup({
- ...formData,
- liquidGroupId: _liquidGroupId,
- })
- ),
- },
- }
+ return showForm ? (
+
+ ) : (
+
+ )
}
-
-export const LiquidsPage = connect(
- mapStateToProps,
- null,
- mergeProps
-)(LiquidEditFormWrapper)
diff --git a/protocol-designer/src/components/LiquidsSidebar/index.tsx b/protocol-designer/src/components/LiquidsSidebar/index.tsx
index e36022ba3ba..d8b17b0452a 100644
--- a/protocol-designer/src/components/LiquidsSidebar/index.tsx
+++ b/protocol-designer/src/components/LiquidsSidebar/index.tsx
@@ -1,36 +1,34 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-import { connect } from 'react-redux'
+import { useDispatch, useSelector } from 'react-redux'
import {
DeprecatedPrimaryButton,
SidePanel,
truncateString,
} from '@opentrons/components'
+import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
+import * as labwareIngredActions from '../../labware-ingred/actions'
import { PDTitledList } from '../lists'
import { swatchColors } from '../swatchColors'
-import listButtonStyles from '../listButtons.css'
-import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
-import { OrderedLiquids } from '../../labware-ingred/types'
-import * as labwareIngredActions from '../../labware-ingred/actions'
-import { BaseState, ThunkDispatch } from '../../types'
+import type { ThunkDispatch } from '../../types'
import styles from './styles.css'
-interface SP {
- liquids: OrderedLiquids
- selectedLiquid?: string | null
-}
-
-interface DP {
- createNewLiquid: () => unknown
- selectLiquid: (liquidId: string) => unknown
-}
-
-type Props = SP & DP
+import listButtonStyles from '../listButtons.css'
-function LiquidsSidebarComponent(props: Props): JSX.Element {
- const { liquids, selectedLiquid, createNewLiquid, selectLiquid } = props
+export function LiquidsSidebar(): JSX.Element {
const { t } = useTranslation('button')
+ const selectedLiquidGroup = useSelector(
+ labwareIngredSelectors.getSelectedLiquidGroupState
+ )
+ const liquids = useSelector(labwareIngredSelectors.allIngredientNamesIds)
+ const dispatch: ThunkDispatch = useDispatch()
+
+ const selectLiquid = (liquidGroupId: string): void => {
+ dispatch(labwareIngredActions.selectLiquidGroup(liquidGroupId))
+ }
+ const selectedLiquid =
+ selectedLiquidGroup && selectedLiquidGroup.liquidGroupId
return (
{liquids.map(({ ingredientId, name, displayColor }) => (
@@ -54,34 +52,13 @@ function LiquidsSidebarComponent(props: Props): JSX.Element {
/>
))}
-
+ dispatch(labwareIngredActions.createNewLiquidGroup())}
+ >
{t('new_liquid')}
)
}
-
-function mapStateToProps(state: BaseState): SP {
- const selectedLiquidGroup = labwareIngredSelectors.getSelectedLiquidGroupState(
- state
- )
- return {
- liquids: labwareIngredSelectors.allIngredientNamesIds(state),
- selectedLiquid: selectedLiquidGroup && selectedLiquidGroup.liquidGroupId,
- }
-}
-
-function mapDispatchToProps(dispatch: ThunkDispatch): DP {
- return {
- selectLiquid: liquidGroupId =>
- dispatch(labwareIngredActions.selectLiquidGroup(liquidGroupId)),
- createNewLiquid: () =>
- dispatch(labwareIngredActions.createNewLiquidGroup()),
- }
-}
-
-export const LiquidsSidebar = connect(
- mapStateToProps,
- mapDispatchToProps
-)(LiquidsSidebarComponent)
diff --git a/protocol-designer/src/components/ProtocolEditor.tsx b/protocol-designer/src/components/ProtocolEditor.tsx
index ef963ec03f1..e483d6d448a 100644
--- a/protocol-designer/src/components/ProtocolEditor.tsx
+++ b/protocol-designer/src/components/ProtocolEditor.tsx
@@ -4,15 +4,15 @@ import { DragDropContext } from 'react-dnd'
import MouseBackEnd from 'react-dnd-mouse-backend'
import { ComputingSpinner } from '../components/ComputingSpinner'
import { ConnectedNav } from '../containers/ConnectedNav'
-import { ConnectedSidebar } from '../containers/ConnectedSidebar'
+import { Sidebar } from '../containers/ConnectedSidebar'
import { ConnectedTitleBar } from '../containers/ConnectedTitleBar'
-import { ConnectedMainPanel } from '../containers/ConnectedMainPanel'
+import { MainPanel } from '../containers/ConnectedMainPanel'
import { PortalRoot as MainPageModalPortalRoot } from '../components/portals/MainPageModalPortal'
import { MAIN_CONTENT_FORCED_SCROLL_CLASSNAME } from '../ui/steps/utils'
import { PrereleaseModeIndicator } from './PrereleaseModeIndicator'
import { PortalRoot as TopPortalRoot } from './portals/TopPortal'
-import { FileUploadMessageModal } from './modals/FileUploadMessageModal'
-import { LabwareUploadMessageModal } from './modals/LabwareUploadMessageModal'
+import { FileUploadMessageModal } from './modals/FileUploadMessageModal/FileUploadMessageModal'
+import { LabwareUploadMessageModal } from './modals/LabwareUploadMessageModal/LabwareUploadMessageModal'
import { GateModal } from './modals/GateModal'
import { AnnouncementModal } from './modals/AnnouncementModal'
import styles from './ProtocolEditor.css'
@@ -30,7 +30,7 @@ function ProtocolEditorComponent(): JSX.Element {
-
+
@@ -47,7 +47,7 @@ function ProtocolEditorComponent(): JSX.Element {
-
+
diff --git a/protocol-designer/src/components/SelectionRect.tsx b/protocol-designer/src/components/SelectionRect.tsx
index 251470e135b..3ffe242b17f 100644
--- a/protocol-designer/src/components/SelectionRect.tsx
+++ b/protocol-designer/src/components/SelectionRect.tsx
@@ -1,55 +1,47 @@
import * as React from 'react'
-
import styles from './SelectionRect.css'
-import { DragRect, GenericRect } from '../collision-types'
+import type { DragRect, GenericRect } from '../collision-types'
-interface Props {
- onSelectionMove?: (e: MouseEvent, arg: GenericRect) => unknown
- onSelectionDone?: (e: MouseEvent, arg: GenericRect) => unknown
+interface SelectionRectProps {
+ onSelectionMove?: (e: MouseEvent, arg: GenericRect) => void
+ onSelectionDone?: (e: MouseEvent, arg: GenericRect) => void
svg?: boolean // set true if this is an embedded SVG
children?: React.ReactNode
originXOffset?: number
originYOffset?: number
}
-interface State {
- positions: DragRect | null
-}
-
-export class SelectionRect extends React.Component {
- parentRef?: HTMLElement | SVGElement | null
-
- constructor(props: Props) {
- super(props)
- this.state = { positions: null }
- }
-
- renderRect(args: DragRect): React.ReactNode {
+export function SelectionRect(props: SelectionRectProps): JSX.Element {
+ const {
+ onSelectionMove,
+ onSelectionDone,
+ svg,
+ children,
+ originXOffset = 0,
+ originYOffset = 0,
+ } = props
+ const [positions, setPositions] = React.useState(null)
+ const parentRef = React.useRef(null)
+ const renderRect = (args: DragRect): React.ReactNode => {
const { xStart, yStart, xDynamic, yDynamic } = args
const left = Math.min(xStart, xDynamic)
const top = Math.min(yStart, yDynamic)
const width = Math.abs(xDynamic - xStart)
const height = Math.abs(yDynamic - yStart)
- const { originXOffset = 0, originYOffset = 0 } = this.props
- if (this.props.svg) {
- // calculate ratio btw clientRect bounding box vs svg parent viewBox
- // WARNING: May not work right if you're nesting SVGs!
- const parentRef = this.parentRef
- if (!parentRef) {
+
+ if (svg) {
+ if (!parentRef.current) {
return null
}
- const clientRect: {
+ const clientRect: DOMRect = parentRef.current.getBoundingClientRect()
+ const viewBox: {
width: number
height: number
- left: number
- top: number
- } = parentRef.getBoundingClientRect()
- // @ts-expect-error(sa, 2021-7-1): parentRef.closest might return null
- const viewBox: { width: number; height: number } = parentRef.closest(
- 'svg'
- ).viewBox.baseVal // WARNING: elem.closest() is experiemental
-
+ } = parentRef.current.closest('svg')?.viewBox?.baseVal ?? {
+ width: 0,
+ height: 0,
+ }
const xScale = viewBox.width / clientRect.width
const yScale = viewBox.height / clientRect.height
@@ -77,10 +69,8 @@ export class SelectionRect extends React.Component {
)
}
- getRect(args: DragRect): GenericRect {
+ const getRect = (args: DragRect): GenericRect => {
const { xStart, yStart, xDynamic, yDynamic } = args
- // convert internal rect position to more generic form
- // TODO should this be used in renderRect?
return {
x0: Math.min(xStart, xDynamic),
x1: Math.max(xStart, xDynamic),
@@ -89,74 +79,72 @@ export class SelectionRect extends React.Component {
}
}
- handleMouseDown: React.MouseEventHandler = e => {
- document.addEventListener('mousemove', this.handleDrag)
- document.addEventListener('mouseup', this.handleMouseUp)
- this.setState({
- positions: {
- xStart: e.clientX,
- xDynamic: e.clientX,
- yStart: e.clientY,
- yDynamic: e.clientY,
- },
- })
- }
-
- handleDrag: (e: MouseEvent) => void = e => {
- if (this.state.positions) {
- const nextRect = {
- ...this.state.positions,
- xDynamic: e.clientX,
- yDynamic: e.clientY,
+ const handleDrag = (e: MouseEvent): void => {
+ setPositions(prevPositions => {
+ if (prevPositions) {
+ const nextRect = {
+ ...prevPositions,
+ xDynamic: e.clientX,
+ yDynamic: e.clientY,
+ }
+ const rect = getRect(nextRect)
+ onSelectionMove && onSelectionMove(e, rect)
+
+ return nextRect
}
- this.setState({ positions: nextRect })
-
- const rect = this.getRect(nextRect)
- this.props.onSelectionMove && this.props.onSelectionMove(e, rect)
- }
+ return prevPositions
+ })
}
- handleMouseUp: (e: MouseEvent) => void = e => {
+ const handleMouseUp = (e: MouseEvent): void => {
if (!(e instanceof MouseEvent)) {
return
}
- document.removeEventListener('mousemove', this.handleDrag)
- document.removeEventListener('mouseup', this.handleMouseUp)
-
- const finalRect = this.state.positions && this.getRect(this.state.positions)
-
- // clear the rectangle
- this.setState({ positions: null })
-
+ const finalRect = positions && getRect(positions)
+ setPositions(prevPositions => {
+ return prevPositions === positions ? null : prevPositions
+ })
// call onSelectionDone callback with {x0, x1, y0, y1} of final selection rectangle
- this.props.onSelectionDone &&
- finalRect &&
- this.props.onSelectionDone(e, finalRect)
+ onSelectionDone && finalRect && onSelectionDone(e, finalRect)
}
- render(): React.ReactNode {
- const { svg, children } = this.props
-
- return svg ? (
- {
- this.parentRef = ref
- }}
- >
- {children}
- {this.state.positions && this.renderRect(this.state.positions)}
-
- ) : (
- {
- this.parentRef = ref
- }}
- >
- {this.state.positions && this.renderRect(this.state.positions)}
- {children}
-
- )
+ const handleMouseDown: React.MouseEventHandler = e => {
+ setPositions({
+ xStart: e.clientX,
+ xDynamic: e.clientX,
+ yStart: e.clientY,
+ yDynamic: e.clientY,
+ })
}
+
+ React.useEffect(() => {
+ document.addEventListener('mousemove', handleDrag)
+ document.addEventListener('mouseup', handleMouseUp)
+ return () => {
+ document.removeEventListener('mousemove', handleDrag)
+ document.removeEventListener('mouseup', handleMouseUp)
+ }
+ }, [handleDrag, handleMouseUp])
+
+ return svg ? (
+ {
+ parentRef.current = ref
+ }}
+ >
+ {children}
+ {positions && renderRect(positions)}
+
+ ) : (
+ {
+ parentRef.current = ref
+ }}
+ >
+ {positions && renderRect(positions)}
+ {children}
+
+ )
}
diff --git a/protocol-designer/src/components/SettingsPage/FeatureFlagCard/FeatureFlagCard.tsx b/protocol-designer/src/components/SettingsPage/FeatureFlagCard/FeatureFlagCard.tsx
index 755027ee57e..7e7a65215ac 100644
--- a/protocol-designer/src/components/SettingsPage/FeatureFlagCard/FeatureFlagCard.tsx
+++ b/protocol-designer/src/components/SettingsPage/FeatureFlagCard/FeatureFlagCard.tsx
@@ -1,28 +1,38 @@
import * as React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import sortBy from 'lodash/sortBy'
import { ContinueModal, Card, ToggleButton } from '@opentrons/components'
import { resetScrollElements } from '../../../ui/steps/utils'
+import {
+ userFacingFlags,
+ FlagTypes,
+ actions as featureFlagActions,
+ selectors as featureFlagSelectors,
+} from '../../../feature-flags'
import { Portal } from '../../portals/MainPageModalPortal'
import styles from '../SettingsPage.css'
import modalStyles from '../../modals/modal.css'
-import { userFacingFlags, Flags, FlagTypes } from '../../../feature-flags'
-export interface Props {
- flags: Flags
- setFeatureFlags: (flags: Flags) => unknown
-}
+export function FeatureFlagCard(): JSX.Element {
+ const flags = useSelector(featureFlagSelectors.getFeatureFlagData)
+ const dispatch = useDispatch()
-export const FeatureFlagCard = (props: Props): JSX.Element => {
const [modalFlagName, setModalFlagName] = React.useState(
null
)
const { t } = useTranslation(['modal', 'card', 'feature_flags'])
- const prereleaseModeEnabled = props.flags.PRERELEASE_MODE === true
+ const setFeatureFlags = (
+ flags: Partial>
+ ): void => {
+ dispatch(featureFlagActions.setFeatureFlags(flags))
+ }
+
+ const prereleaseModeEnabled = flags.PRERELEASE_MODE === true
// @ts-expect-error(sa, 2021-6-21): Object.keys not smart enough to take keys from props.flags
- const allFlags: FlagTypes[] = sortBy(Object.keys(props.flags))
+ const allFlags: FlagTypes[] = sortBy(Object.keys(flags))
const userFacingFlagNames = allFlags.filter(flagName =>
userFacingFlags.includes(flagName)
@@ -59,7 +69,7 @@ export const FeatureFlagCard = (props: Props): JSX.Element => {
{
resetScrollElements()
setModalFlagName(flagName)
@@ -85,7 +95,7 @@ export const FeatureFlagCard = (props: Props): JSX.Element => {
let flagSwitchDirection: string = 'on'
if (modalFlagName) {
- const isFlagOn: boolean | null | undefined = props.flags[modalFlagName]
+ const isFlagOn: boolean | null | undefined = flags[modalFlagName]
flagSwitchDirection = isFlagOn ? 'off' : 'on'
}
return (
@@ -100,8 +110,8 @@ export const FeatureFlagCard = (props: Props): JSX.Element => {
)}
onCancelClick={() => setModalFlagName(null)}
onContinueClick={() => {
- props.setFeatureFlags({
- [modalFlagName as string]: !props.flags[modalFlagName],
+ setFeatureFlags({
+ [modalFlagName as string]: !flags[modalFlagName],
})
setModalFlagName(null)
}}
diff --git a/protocol-designer/src/components/SettingsPage/FeatureFlagCard/index.ts b/protocol-designer/src/components/SettingsPage/FeatureFlagCard/index.ts
deleted file mode 100644
index 034aba2e859..00000000000
--- a/protocol-designer/src/components/SettingsPage/FeatureFlagCard/index.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { connect } from 'react-redux'
-import {
- FeatureFlagCard as FeatureFlagCardComponent,
- Props as FeatureFlagCardProps,
-} from './FeatureFlagCard'
-import {
- actions as featureFlagActions,
- selectors as featureFlagSelectors,
-} from '../../../feature-flags'
-import { Dispatch } from 'redux'
-import { BaseState } from '../../../types'
-interface SP {
- flags: FeatureFlagCardProps['flags']
-}
-interface DP {
- setFeatureFlags: FeatureFlagCardProps['setFeatureFlags']
-}
-
-const mapStateToProps = (state: BaseState): SP => ({
- flags: featureFlagSelectors.getFeatureFlagData(state),
-})
-
-const mapDispatchToProps = (dispatch: Dispatch): DP => ({
- setFeatureFlags: flags => dispatch(featureFlagActions.setFeatureFlags(flags)),
-})
-
-export const FeatureFlagCard = connect(
- mapStateToProps,
- mapDispatchToProps
-)(FeatureFlagCardComponent)
diff --git a/protocol-designer/src/components/SettingsPage/SettingsApp.tsx b/protocol-designer/src/components/SettingsPage/SettingsApp.tsx
index 9f6f03c3253..a1c26ae6c21 100644
--- a/protocol-designer/src/components/SettingsPage/SettingsApp.tsx
+++ b/protocol-designer/src/components/SettingsPage/SettingsApp.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { connect } from 'react-redux'
+import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import {
Card,
@@ -16,29 +16,20 @@ import {
selectors as tutorialSelectors,
} from '../../tutorial'
import { OLDEST_MIGRATEABLE_VERSION } from '../../load-file/migration'
-import { FeatureFlagCard } from './FeatureFlagCard'
-import styles from './SettingsPage.css'
-import { BaseState, ThunkDispatch } from '../../types'
+import { FeatureFlagCard } from './FeatureFlagCard/FeatureFlagCard'
-interface Props {
- canClearHintDismissals: boolean
- hasOptedIn: boolean | null
- restoreHints: () => unknown
- toggleOptedIn: () => unknown
-}
+import styles from './SettingsPage.css'
-interface SP {
- canClearHintDismissals: Props['canClearHintDismissals']
- hasOptedIn: Props['hasOptedIn']
-}
+export function SettingsApp(): JSX.Element {
+ const dispatch = useDispatch()
+ const hasOptedIn = useSelector(analyticsSelectors.getHasOptedIn)
+ const canClearHintDismissals = useSelector(
+ tutorialSelectors.getCanClearHintDismissals
+ )
+ const _toggleOptedIn = hasOptedIn
+ ? analyticsActions.optOut
+ : analyticsActions.optIn
-function SettingsAppComponent(props: Props): JSX.Element {
- const {
- canClearHintDismissals,
- hasOptedIn,
- restoreHints,
- toggleOptedIn,
- } = props
const { t } = useTranslation(['card', 'application', 'button'])
return (
<>
@@ -64,7 +55,9 @@ function SettingsAppComponent(props: Props): JSX.Element {
+ dispatch(tutorialActions.clearAllHintDismissals())
+ }
>
{canClearHintDismissals
? t('button:restore')
@@ -82,7 +75,7 @@ function SettingsAppComponent(props: Props): JSX.Element {
dispatch(_toggleOptedIn())}
/>
@@ -99,33 +92,3 @@ function SettingsAppComponent(props: Props): JSX.Element {
>
)
}
-
-function mapStateToProps(state: BaseState): SP {
- return {
- hasOptedIn: analyticsSelectors.getHasOptedIn(state),
- canClearHintDismissals: tutorialSelectors.getCanClearHintDismissals(state),
- }
-}
-
-function mergeProps(
- stateProps: SP,
- dispatchProps: { dispatch: ThunkDispatch }
-): Props {
- const { dispatch } = dispatchProps
- const { hasOptedIn } = stateProps
-
- const _toggleOptedIn = hasOptedIn
- ? analyticsActions.optOut
- : analyticsActions.optIn
- return {
- ...stateProps,
- toggleOptedIn: () => dispatch(_toggleOptedIn()),
- restoreHints: () => dispatch(tutorialActions.clearAllHintDismissals()),
- }
-}
-
-export const SettingsApp = connect(
- mapStateToProps,
- null,
- mergeProps
-)(SettingsAppComponent)
diff --git a/protocol-designer/src/components/SettingsPage/index.tsx b/protocol-designer/src/components/SettingsPage/index.tsx
index 74a55b434fa..475ba23946c 100644
--- a/protocol-designer/src/components/SettingsPage/index.tsx
+++ b/protocol-designer/src/components/SettingsPage/index.tsx
@@ -1,27 +1,17 @@
import * as React from 'react'
-import { connect } from 'react-redux'
+import { useSelector } from 'react-redux'
-import { BaseState } from '../../types'
-import { selectors, Page } from '../../navigation'
+import { selectors } from '../../navigation'
import { SettingsApp } from './SettingsApp'
export { SettingsSidebar } from './SettingsSidebar'
-interface Props {
- currentPage: Page
-}
-
-const SettingsPageComponent = (props: Props): JSX.Element => {
- switch (props.currentPage) {
+export function SettingsPage(): JSX.Element {
+ const currentPage = useSelector(selectors.getCurrentPage)
+ switch (currentPage) {
// TODO: Ian 2019-08-21 when we have other pages, put them here
case 'settings-app':
default:
return
}
}
-
-const STP = (state: BaseState): Props => ({
- currentPage: selectors.getCurrentPage(state),
-})
-
-export const SettingsPage = connect(STP)(SettingsPageComponent)
diff --git a/protocol-designer/src/components/StepEditForm/fields/DisposalVolumeField.tsx b/protocol-designer/src/components/StepEditForm/fields/DisposalVolumeField.tsx
index 837ba6ce166..6e73d5ba046 100644
--- a/protocol-designer/src/components/StepEditForm/fields/DisposalVolumeField.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/DisposalVolumeField.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-import { connect } from 'react-redux'
+import { useSelector } from 'react-redux'
import cx from 'classnames'
import {
@@ -15,9 +15,8 @@ import { selectors as uiLabwareSelectors } from '../../../ui/labware'
import { getBlowoutLocationOptionsForForm } from '../utils'
import { TextField } from './TextField'
-import { FieldProps, FieldPropsByName } from '../types'
-import { PathOption, StepType } from '../../../form-types'
-import { BaseState } from '../../../types'
+import type { FieldProps, FieldPropsByName } from '../types'
+import type { PathOption, StepType } from '../../../form-types'
import styles from '../StepEditForm.css'
@@ -37,25 +36,51 @@ const DropdownFormField = (props: DropdownFormFieldProps): JSX.Element => {
/>
)
}
-
-interface SP {
- disposalDestinationOptions: Options
- maxDisposalVolume?: number | null
-}
-interface OP {
- aspirate_airGap_checkbox?: boolean | null
- aspirate_airGap_volume?: string | null
+interface DisposalVolumeFieldProps {
path: PathOption
pipette: string | null
propsForFields: FieldPropsByName
stepType: StepType
volume: string | null
+ aspirate_airGap_checkbox?: boolean | null
+ aspirate_airGap_volume?: string | null
}
-type Props = SP & OP
-const DisposalVolumeFieldComponent = (props: Props): JSX.Element => {
- const { propsForFields, maxDisposalVolume } = props
+export const DisposalVolumeField = (
+ props: DisposalVolumeFieldProps
+): JSX.Element => {
+ const {
+ path,
+ stepType,
+ volume,
+ pipette,
+ propsForFields,
+ aspirate_airGap_checkbox,
+ aspirate_airGap_volume,
+ } = props
const { t } = useTranslation(['application', 'form'])
+
+ const disposalOptions = useSelector(uiLabwareSelectors.getDisposalOptions)
+ const pipetteEntities = useSelector(stepFormSelectors.getPipetteEntities)
+ const blowoutLocationOptions = getBlowoutLocationOptionsForForm({
+ path,
+ stepType,
+ })
+ const maxDisposalVolume = getMaxDisposalVolumeForMultidispense(
+ {
+ aspirate_airGap_checkbox,
+ aspirate_airGap_volume,
+ path,
+ pipette,
+ volume,
+ },
+ pipetteEntities
+ )
+ const disposalDestinationOptions = [
+ ...disposalOptions,
+ ...blowoutLocationOptions,
+ ]
+
const volumeBoundsCaption =
maxDisposalVolume != null
? `max ${maxDisposalVolume} ${t('units.microliter')}`
@@ -97,7 +122,7 @@ const DisposalVolumeFieldComponent = (props: Props): JSX.Element => {
) : null}
@@ -105,38 +130,3 @@ const DisposalVolumeFieldComponent = (props: Props): JSX.Element => {
)
}
-const mapSTP = (state: BaseState, ownProps: OP): SP => {
- const {
- aspirate_airGap_checkbox,
- aspirate_airGap_volume,
- path,
- pipette,
- stepType,
- volume,
- } = ownProps
-
- const blowoutLocationOptions = getBlowoutLocationOptionsForForm({
- path,
- stepType,
- })
-
- const disposalOptions = uiLabwareSelectors.getDisposalOptions(state)
-
- const maxDisposalVolume = getMaxDisposalVolumeForMultidispense(
- {
- aspirate_airGap_checkbox,
- aspirate_airGap_volume,
- path,
- pipette,
- volume,
- },
- stepFormSelectors.getPipetteEntities(state)
- )
-
- return {
- maxDisposalVolume,
- disposalDestinationOptions: [...disposalOptions, ...blowoutLocationOptions],
- }
-}
-
-export const DisposalVolumeField = connect(mapSTP)(DisposalVolumeFieldComponent)
diff --git a/protocol-designer/src/components/StepEditForm/fields/FlowRateField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/FlowRateField/index.tsx
index 4a3080c01cb..89cf6a43e69 100644
--- a/protocol-designer/src/components/StepEditForm/fields/FlowRateField/index.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/FlowRateField/index.tsx
@@ -1,9 +1,8 @@
import * as React from 'react'
import { FlowRateInput, FlowRateInputProps } from './FlowRateInput'
-import { connect } from 'react-redux'
+import { useSelector } from 'react-redux'
import { selectors as stepFormSelectors } from '../../../../step-forms'
import { FieldProps } from '../../types'
-import { BaseState } from '../../../../types'
interface OP extends FieldProps {
pipetteId?: string | null
@@ -12,33 +11,13 @@ interface OP extends FieldProps {
label?: FlowRateInputProps['label']
}
-interface SP {
- innerKey: string
- defaultFlowRate?: number | null
- minFlowRate: number
- maxFlowRate: number
- pipetteDisplayName: string
-}
-
-interface Props extends FlowRateInputProps {
- innerKey: string
-}
-
// Add a key to force re-constructing component when values change
-function FlowRateInputWithKey(props: Props): JSX.Element {
- const { innerKey, ...otherProps } = props
- return
-}
-
-function mapStateToProps(state: BaseState, ownProps: OP): SP {
- const { flowRateType, pipetteId, name } = ownProps
-
- const pipette =
- pipetteId != null
- ? stepFormSelectors.getPipetteEntities(state)[pipetteId]
- : null
+export function FlowRateField(props: OP): JSX.Element {
+ const { pipetteId, flowRateType, value, ...passThruProps } = props
+ const pipetteEntities = useSelector(stepFormSelectors.getPipetteEntities)
+ const pipette = pipetteId != null ? pipetteEntities[pipetteId] : null
const pipetteDisplayName = pipette ? pipette.spec.displayName : 'pipette'
-
+ const innerKey = `${name}:${String(value || 0)}`
let defaultFlowRate
if (pipette) {
if (flowRateType === 'aspirate') {
@@ -48,30 +27,16 @@ function mapStateToProps(state: BaseState, ownProps: OP): SP {
}
}
- // force each field to have a new instance created when value is changed
- const innerKey = `${name}:${String(ownProps.value || 0)}`
-
- return {
- innerKey,
- defaultFlowRate,
- minFlowRate: 0,
- // NOTE: since we only have rule-of-thumb, max is entire volume in 1 second
- maxFlowRate: pipette ? pipette.spec.maxVolume : Infinity,
- pipetteDisplayName,
- }
-}
-
-const mergeProps = (
- stateProps: SP,
- _dispatchProps: null,
- ownProps: OP
-): Props => {
- const { pipetteId, ...passThruProps } = ownProps
- return { ...stateProps, ...passThruProps }
+ return (
+
+ )
}
-
-export const FlowRateField = connect(
- mapStateToProps,
- null,
- mergeProps
-)(FlowRateInputWithKey)
diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx
index 6ee70be22ef..54cc63213b6 100644
--- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
-import { connect } from 'react-redux'
+import { useSelector } from 'react-redux'
import {
FormGroup,
InputField,
@@ -14,51 +14,63 @@ import {
getIsDelayPositionField,
} from '../../../../form-types'
import { selectors as stepFormSelectors } from '../../../../step-forms'
+import { TipPositionModal } from './TipPositionModal'
+import { getDefaultMmFromBottom } from './utils'
import stepFormStyles from '../../StepEditForm.css'
import styles from './TipPositionInput.css'
-import { TipPositionModal } from './TipPositionModal'
-import { getDefaultMmFromBottom } from './utils'
-import { BaseState } from '../../../../types'
-import { FieldProps } from '../../types'
+import type { FieldProps } from '../../types'
-interface OP extends FieldProps {
+interface TipPositionFieldProps extends FieldProps {
labwareId?: string | null
className?: string
}
-interface SP {
- mmFromBottom: number | null
- wellDepthMm: number
-}
+export function TipPositionField(props: TipPositionFieldProps): JSX.Element {
+ const {
+ disabled,
+ name,
+ tooltipContent,
+ updateValue,
+ isIndeterminate,
+ labwareId,
+ } = props
+ const { t } = useTranslation('application')
+ const [targetProps, tooltipProps] = useHoverTooltip()
+ const [isModalOpen, setModalOpen] = React.useState(false)
+ const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities)
+ const labwareDef =
+ labwareId != null && labwareEntities[labwareId] != null
+ ? labwareEntities[labwareId].def
+ : null
-type Props = OP & SP
+ let wellDepthMm = 0
+ if (labwareDef != null) {
+ // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths
+ const firstWell = labwareDef.wells.A1
+ if (firstWell) {
+ wellDepthMm = getWellsDepth(labwareDef, ['A1'])
+ }
+ }
-function TipPositionInput(props: Props): JSX.Element {
- const [isModalOpen, setModalOpen] = React.useState(false)
+ if (wellDepthMm === 0 && labwareId != null && labwareDef != null) {
+ console.error(
+ `expected to find the well depth mm with labwareId ${labwareId} but could not`
+ )
+ }
const handleOpen = (): void => {
- if (props.wellDepthMm) {
+ if (wellDepthMm) {
setModalOpen(true)
}
}
const handleClose = (): void => {
setModalOpen(false)
}
-
- const {
- disabled,
- name,
- mmFromBottom,
- tooltipContent,
- wellDepthMm,
- updateValue,
- isIndeterminate,
- } = props
- const { t } = useTranslation('application')
const isTouchTipField = getIsTouchTipField(name)
const isDelayPositionField = getIsDelayPositionField(name)
- let value: number | string = ''
+ let value: string | number = '0'
+ const mmFromBottom = typeof value === 'number' ? value : null
if (wellDepthMm !== null) {
// show default value for field in parens if no mmFromBottom value is selected
value =
@@ -66,9 +78,6 @@ function TipPositionInput(props: Props): JSX.Element {
? mmFromBottom
: getDefaultMmFromBottom({ name, wellDepthMm })
}
-
- const [targetProps, tooltipProps] = useHoverTooltip()
-
return (
<>
{tooltipContent}
@@ -127,31 +136,3 @@ const Wrapper = (props: WrapperProps): JSX.Element => {
)
}
-const mapSTP = (state: BaseState, ownProps: OP): SP => {
- const { labwareId, value } = ownProps
-
- let wellDepthMm = 0
- const labwareDef =
- labwareId != null
- ? stepFormSelectors.getLabwareEntities(state)[labwareId]?.def
- : null
-
- if (labwareDef != null) {
- // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths
- const firstWell = labwareDef.wells.A1
- if (firstWell) wellDepthMm = getWellsDepth(labwareDef, ['A1'])
- }
-
- if (wellDepthMm === 0 && labwareId != null && labwareDef != null) {
- console.error(
- `expected to find the well depth mm with labwareId ${labwareId} but could not`
- )
- }
-
- return {
- wellDepthMm,
- mmFromBottom: typeof value === 'number' ? value : null,
- }
-}
-
-export const TipPositionField = connect(mapSTP, () => ({}))(TipPositionInput)
diff --git a/protocol-designer/src/components/StepEditForm/fields/WellOrderField/WellOrderModal.tsx b/protocol-designer/src/components/StepEditForm/fields/WellOrderField/WellOrderModal.tsx
index 39495ba88f5..e2e09a2ea03 100644
--- a/protocol-designer/src/components/StepEditForm/fields/WellOrderField/WellOrderModal.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/WellOrderField/WellOrderModal.tsx
@@ -9,12 +9,12 @@ import {
FormGroup,
DropdownField,
} from '@opentrons/components'
-import modalStyles from '../../../modals/modal.css'
-import { WellOrderOption } from '../../../../form-types'
-
import { WellOrderViz } from './WellOrderViz'
-import styles from './WellOrderInput.css'
+import type { WellOrderOption } from '../../../../form-types'
+
+import modalStyles from '../../../modals/modal.css'
import stepEditStyles from '../../StepEditForm.css'
+import styles from './WellOrderInput.css'
const DEFAULT_FIRST: WellOrderOption = 't2b'
const DEFAULT_SECOND: WellOrderOption = 'l2r'
@@ -37,9 +37,6 @@ export interface WellOrderModalProps {
firstValue?: WellOrderOption | null,
secondValue?: WellOrderOption | null
) => void
- // TODO(jr, 1/18/24): The newest i18next versions only support TypeScript v5
- // so need to type all ts as any for now https://www.i18next.com/overview/typescript
- t: any
}
interface State {
@@ -86,27 +83,24 @@ export const DoneButton = (props: { onClick: () => void }): JSX.Element => {
)
}
-export class WellOrderModal extends React.Component<
- WellOrderModalProps,
- State
-> {
- constructor(props: WellOrderModalProps) {
- super(props)
- const {
- initialFirstValue,
- initialSecondValue,
- } = this.getInitialFirstValues()
- this.state = {
- firstValue: initialFirstValue,
- secondValue: initialSecondValue,
- }
- }
- getInitialFirstValues: () => {
+export const WellOrderModal = (
+ props: WellOrderModalProps
+): JSX.Element | null => {
+ const { t } = useTranslation(['form', 'modal'])
+ const {
+ isOpen,
+ closeModal,
+ firstName,
+ secondName,
+ updateValues,
+ firstValue,
+ secondValue,
+ } = props
+ const getInitialFirstValues: () => {
initialFirstValue: WellOrderOption
initialSecondValue: WellOrderOption
} = () => {
- const { firstValue, secondValue } = this.props
if (firstValue == null || secondValue == null) {
return {
initialFirstValue: DEFAULT_FIRST,
@@ -119,136 +113,133 @@ export class WellOrderModal extends React.Component<
}
}
- applyChanges: () => void = () => {
- this.props.updateValues(this.state.firstValue, this.state.secondValue)
+ const [wellOrder, setWellOrder] = React.useState({
+ firstValue: DEFAULT_FIRST,
+ secondValue: DEFAULT_SECOND,
+ })
+
+ React.useEffect(() => {
+ if (firstValue != null && secondValue != null) {
+ setWellOrder({
+ firstValue: firstValue,
+ secondValue: secondValue,
+ })
+ }
+ }, [firstValue, secondValue])
+
+ const applyChanges = (): void => {
+ updateValues(wellOrder.firstValue, wellOrder.secondValue)
}
- handleReset: () => void = () => {
- this.setState(
- { firstValue: DEFAULT_FIRST, secondValue: DEFAULT_SECOND },
- this.applyChanges
- )
- this.props.closeModal()
+ const handleReset = (): void => {
+ setWellOrder({ firstValue: DEFAULT_FIRST, secondValue: DEFAULT_SECOND })
+ applyChanges()
+ closeModal()
}
- handleCancel: () => void = () => {
- const {
- initialFirstValue,
- initialSecondValue,
- } = this.getInitialFirstValues()
- this.setState({
+ const handleCancel = (): void => {
+ const { initialFirstValue, initialSecondValue } = getInitialFirstValues()
+ setWellOrder({
firstValue: initialFirstValue,
secondValue: initialSecondValue,
})
- this.props.closeModal()
+ closeModal()
}
- handleDone: () => void = () => {
- this.applyChanges()
- this.props.closeModal()
+ const handleDone = (): void => {
+ applyChanges()
+ closeModal()
}
- makeOnChange: (
- ordinality: 'first' | 'second'
- ) => (
+ const makeOnChange = (ordinality: 'first' | 'second') => (
event: React.ChangeEvent
- ) => void = ordinality => event => {
+ ): void => {
const { value } = event.currentTarget
// @ts-expect-error (ce, 2021-06-22) missing one prop or the other
let nextState: State = { [`${ordinality}Value`]: value }
if (ordinality === 'first') {
if (
VERTICAL_VALUES.includes(value as WellOrderOption) &&
- VERTICAL_VALUES.includes(this.state.secondValue)
+ VERTICAL_VALUES.includes(wellOrder.secondValue)
) {
nextState = { ...nextState, secondValue: HORIZONTAL_VALUES[0] }
} else if (
HORIZONTAL_VALUES.includes(value as WellOrderOption) &&
- HORIZONTAL_VALUES.includes(this.state.secondValue)
+ HORIZONTAL_VALUES.includes(wellOrder.secondValue)
) {
nextState = { ...nextState, secondValue: VERTICAL_VALUES[0] }
}
}
- this.setState(nextState)
+ setWellOrder(nextState)
}
- isSecondOptionDisabled: (wellOrderOption: WellOrderOption) => boolean = (
- value: WellOrderOption
- ) => {
- if (VERTICAL_VALUES.includes(this.state.firstValue)) {
+ const isSecondOptionDisabled = (value: WellOrderOption): boolean => {
+ if (VERTICAL_VALUES.includes(wellOrder.firstValue)) {
return VERTICAL_VALUES.includes(value)
- } else if (HORIZONTAL_VALUES.includes(this.state.firstValue)) {
+ } else if (HORIZONTAL_VALUES.includes(wellOrder.firstValue)) {
return HORIZONTAL_VALUES.includes(value)
} else {
return false
}
}
- render(): React.ReactNode | null {
- if (!this.props.isOpen) return null
-
- const { firstValue, secondValue } = this.state
- const { firstName, secondName, t } = this.props
-
- return (
-
-
-
-
{t('modal:well_order.title')}
-
{t('modal:well_order.body')}
-
-
-
-
- ({
- value,
- name: t(`step_edit_form.field.well_order.option.${value}`),
- }))}
- />
-
- {t('modal:well_order.then')}
-
- ({
- value,
- name: t(`step_edit_form.field.well_order.option.${value}`),
- disabled: this.isSecondOptionDisabled(value),
- }))}
- />
-
-
-
-
-
-
-
-
-
-
-
+ if (!isOpen) return null
+
+ return (
+
+
+
+
{t('modal:well_order.title')}
+
{t('modal:well_order.body')}
+
+
+
+
+ ({
+ value,
+ name: t(`step_edit_form.field.well_order.option.${value}`),
+ }))}
+ />
+
+ {t('modal:well_order.then')}
+
+ ({
+ value,
+ name: t(`step_edit_form.field.well_order.option.${value}`),
+ disabled: isSecondOptionDisabled(value),
+ }))}
+ />
+
+
+
+
+
+
+
+
+ )
}
diff --git a/protocol-designer/src/components/StepEditForm/fields/WellOrderField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/WellOrderField/index.tsx
index bfcab5ff3bd..f3867dae2ed 100644
--- a/protocol-designer/src/components/StepEditForm/fields/WellOrderField/index.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/WellOrderField/index.tsx
@@ -128,7 +128,6 @@ export const WellOrderField = (props: WellOrderFieldProps): JSX.Element => {
secondValue={secondValue}
firstName={firstName}
secondName={secondName}
- t={t}
/>
>
)
diff --git a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionModal.tsx
index 0f2e4cb540d..7db677b5da4 100644
--- a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionModal.tsx
+++ b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionModal.tsx
@@ -131,6 +131,7 @@ export const WellSelectionModal = (
onCloseClick,
pipetteId,
nozzleType = null,
+ updateValue,
} = props
const wellFieldData = props.value
// selector data
@@ -170,7 +171,7 @@ export const WellSelectionModal = (
const handleSave = (): void => {
const sortedWells = Object.keys(selectedPrimaryWells).sort(sortWells)
- props.updateValue(sortedWells)
+ updateValue(sortedWells)
onCloseClick()
}
diff --git a/protocol-designer/src/components/StepEditForm/index.tsx b/protocol-designer/src/components/StepEditForm/index.tsx
index 4063528bcd0..1b3f0100f27 100644
--- a/protocol-designer/src/components/StepEditForm/index.tsx
+++ b/protocol-designer/src/components/StepEditForm/index.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
-import { connect } from 'react-redux'
import { useConditionalConfirm } from '@opentrons/components'
import { actions } from '../../steplist'
import { actions as stepsActions } from '../../ui/steps'
@@ -23,48 +23,39 @@ import { makeSingleEditFieldProps } from './fields/makeSingleEditFieldProps'
import { StepEditFormComponent } from './StepEditFormComponent'
import { getDirtyFields } from './utils'
-import type { InvariantContext } from '@opentrons/step-generation'
-import type { BaseState, ThunkDispatch } from '../../types'
-import type { FormData, StepFieldName, StepIdType } from '../../form-types'
+import type { ThunkDispatch } from '../../types'
+import type { StepFieldName, StepIdType } from '../../form-types'
-interface SP {
- canSave: boolean
- formData?: FormData | null
- formHasChanges: boolean
- isNewStep: boolean
- isPristineSetTempForm: boolean
- isPristineSetHeaterShakerTempForm: boolean
- invariantContext: InvariantContext
-}
-interface DP {
- deleteStep: (stepId: string) => unknown
- handleClose: () => unknown
- saveSetTempFormWithAddedPauseUntilTemp: () => unknown
- saveHeaterShakerFormWithAddedPauseUntilTemp: () => unknown
- saveStepForm: () => unknown
- handleChangeFormInput: (name: string, value: unknown) => void
-}
-type StepEditFormManagerProps = SP & DP
-
-const StepEditFormManager = (
- props: StepEditFormManagerProps
-): JSX.Element | null => {
- const {
- canSave,
- deleteStep,
- formData,
- formHasChanges,
- handleChangeFormInput,
- handleClose,
- isNewStep,
- isPristineSetTempForm,
- isPristineSetHeaterShakerTempForm,
- saveSetTempFormWithAddedPauseUntilTemp,
- saveHeaterShakerFormWithAddedPauseUntilTemp,
- saveStepForm,
- invariantContext,
- } = props
+export const StepEditForm = (): JSX.Element | null => {
const { t } = useTranslation('tooltip')
+ const dispatch = useDispatch
>()
+ const canSave = useSelector(stepFormSelectors.getCurrentFormCanBeSaved)
+ const formData = useSelector(stepFormSelectors.getUnsavedForm)
+ const formHasChanges = useSelector(
+ stepFormSelectors.getCurrentFormHasUnsavedChanges
+ )
+ const isNewStep = useSelector(stepFormSelectors.getCurrentFormIsPresaved)
+ const isPristineSetHeaterShakerTempForm = useSelector(
+ stepFormSelectors.getUnsavedFormIsPristineHeaterShakerForm
+ )
+ const isPristineSetTempForm = useSelector(
+ stepFormSelectors.getUnsavedFormIsPristineSetTempForm
+ )
+ const invariantContext = useSelector(getInvariantContext)
+ const deleteStep = (stepId: StepIdType): void =>
+ dispatch(actions.deleteStep(stepId))
+ const handleClose = (): void => dispatch(actions.cancelStepForm())
+ const saveHeaterShakerFormWithAddedPauseUntilTemp = (): void =>
+ dispatch(stepsActions.saveHeaterShakerFormWithAddedPauseUntilTemp())
+ const saveSetTempFormWithAddedPauseUntilTemp = (): void =>
+ dispatch(stepsActions.saveSetTempFormWithAddedPauseUntilTemp())
+ const saveStepForm = (): void => dispatch(stepsActions.saveStepForm())
+
+ const handleChangeFormInput = (name: string, value: unknown): void => {
+ const maskedValue = maskField(name, value)
+ dispatch(actions.changeFormInput({ update: { [name]: maskedValue } }))
+ }
+
const [
showMoreOptionsModal,
setShowMoreOptionsModal,
@@ -161,7 +152,7 @@ const StepEditFormManager = (
}
return (
- <>
+
{showConfirmDeleteModal && (
- >
+
)
}
-
-const mapStateToProps = (state: BaseState): SP => {
- return {
- canSave: stepFormSelectors.getCurrentFormCanBeSaved(state),
- formData: stepFormSelectors.getUnsavedForm(state),
- formHasChanges: stepFormSelectors.getCurrentFormHasUnsavedChanges(state),
- isNewStep: stepFormSelectors.getCurrentFormIsPresaved(state),
- isPristineSetHeaterShakerTempForm: stepFormSelectors.getUnsavedFormIsPristineHeaterShakerForm(
- state
- ),
- isPristineSetTempForm: stepFormSelectors.getUnsavedFormIsPristineSetTempForm(
- state
- ),
- invariantContext: getInvariantContext(state),
- }
-}
-
-const mapDispatchToProps = (dispatch: ThunkDispatch): DP => {
- const deleteStep = (stepId: StepIdType): void =>
- dispatch(actions.deleteStep(stepId))
- const handleClose = (): void => dispatch(actions.cancelStepForm())
- const saveHeaterShakerFormWithAddedPauseUntilTemp = (): void =>
- dispatch(stepsActions.saveHeaterShakerFormWithAddedPauseUntilTemp())
- const saveSetTempFormWithAddedPauseUntilTemp = (): void =>
- dispatch(stepsActions.saveSetTempFormWithAddedPauseUntilTemp())
- const saveStepForm = (): void => dispatch(stepsActions.saveStepForm())
-
- const handleChangeFormInput = (name: string, value: unknown): void => {
- const maskedValue = maskField(name, value)
- dispatch(actions.changeFormInput({ update: { [name]: maskedValue } }))
- }
-
- return {
- deleteStep,
- handleChangeFormInput,
- handleClose,
- saveSetTempFormWithAddedPauseUntilTemp,
- saveStepForm,
- saveHeaterShakerFormWithAddedPauseUntilTemp,
- }
-}
-
-// NOTE(IL, 2020-04-22): This is using connect instead of useSelector in order to
-// avoid zombie children in the many connected field components.
-// (Children of a useSelector parent must always be written to use selectors defensively
-// if their parent (StepEditForm) is NOT using connect.
-// It doesn't matter if the children are using connect or useSelector,
-// only the parent matters.)
-// https://react-redux.js.org/api/hooks#stale-props-and-zombie-children
-export const StepEditForm = connect(
- mapStateToProps,
- mapDispatchToProps
-)((props: StepEditFormManagerProps) => (
- // key by ID so manager state doesn't persist across different forms
-
-))
diff --git a/protocol-designer/src/components/labware/BrowseLabwareModal.tsx b/protocol-designer/src/components/labware/BrowseLabwareModal.tsx
index fc5516636e7..aab72cf0e00 100644
--- a/protocol-designer/src/components/labware/BrowseLabwareModal.tsx
+++ b/protocol-designer/src/components/labware/BrowseLabwareModal.tsx
@@ -1,39 +1,34 @@
import assert from 'assert'
import * as React from 'react'
import cx from 'classnames'
-import { connect } from 'react-redux'
+import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
-
import { Modal } from '@opentrons/components'
+
import * as wellContentsSelectors from '../../top-selectors/well-contents'
import { selectors } from '../../labware-ingred/selectors'
import { selectors as stepFormSelectors } from '../../step-forms'
import * as labwareIngredsActions from '../../labware-ingred/actions'
import { BrowsableLabware } from './BrowsableLabware'
-import type { LabwareDefinition2 } from '@opentrons/shared-data'
-import type { BaseState, ThunkDispatch } from '../../types'
-import type { ContentsByWell } from '../../labware-ingred/types'
-import type { WellIngredientNames } from '../../steplist/types'
-
import modalStyles from '../modals/modal.css'
import styles from './labware.css'
-interface SP {
- definition?: LabwareDefinition2 | null
- wellContents: ContentsByWell
- ingredNames: WellIngredientNames
-}
-
-interface DP {
- drillUp: () => unknown
-}
-
-type Props = SP & DP
-
-const BrowseLabwareModalComponent = (props: Props): JSX.Element | null => {
- const { drillUp, definition, ingredNames, wellContents } = props
+export const BrowseLabwareModal = (): JSX.Element | null => {
const { t } = useTranslation('modal')
+ const dispatch = useDispatch()
+ const labwareId = useSelector(selectors.getDrillDownLabwareId)
+ const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities)
+ const allWellContentsForActiveItem = useSelector(
+ wellContentsSelectors.getAllWellContentsForActiveItem
+ )
+ const ingredNames = useSelector(selectors.getLiquidNamesById)
+ const definition = labwareId ? labwareEntities[labwareId]?.def : null
+ const wellContents =
+ labwareId && allWellContentsForActiveItem
+ ? allWellContentsForActiveItem[labwareId]
+ : null
+
if (!definition) {
assert(definition, 'BrowseLabwareModal expected definition')
return null
@@ -46,7 +41,7 @@ const BrowseLabwareModalComponent = (props: Props): JSX.Element | null => {
modalStyles.modal_contents,
modalStyles.transparent_content
)}
- onCloseClick={drillUp}
+ onCloseClick={() => dispatch(labwareIngredsActions.drillUpFromLabware())}
>
{
)
}
-
-function mapStateToProps(state: BaseState): SP {
- const labwareId = selectors.getDrillDownLabwareId(state)
- const definition = labwareId
- ? stepFormSelectors.getLabwareEntities(state)[labwareId]?.def
- : null
-
- const allWellContentsForActiveItem = wellContentsSelectors.getAllWellContentsForActiveItem(
- state
- )
- const wellContents =
- labwareId && allWellContentsForActiveItem
- ? allWellContentsForActiveItem[labwareId]
- : null
- const ingredNames = selectors.getLiquidNamesById(state)
- return {
- wellContents,
- ingredNames,
- definition,
- }
-}
-
-function mapDispatchToProps(dispatch: ThunkDispatch): DP {
- return { drillUp: () => dispatch(labwareIngredsActions.drillUpFromLabware()) }
-}
-
-export const BrowseLabwareModal = connect(
- mapStateToProps,
- mapDispatchToProps
-)(BrowseLabwareModalComponent)
diff --git a/protocol-designer/src/components/labware/SelectableLabware.tsx b/protocol-designer/src/components/labware/SelectableLabware.tsx
index 86b61664b19..ffe39799487 100644
--- a/protocol-designer/src/components/labware/SelectableLabware.tsx
+++ b/protocol-designer/src/components/labware/SelectableLabware.tsx
@@ -42,20 +42,25 @@ const getChannelsFromNozleType = (nozzleType: NozzleType): ChannelType => {
}
}
-export class SelectableLabware extends React.Component {
- _getWellsFromRect: (rect: GenericRect) => WellGroup = rect => {
- const selectedWells = getCollidingWells(rect)
- return this._wellsFromSelected(selectedWells)
- }
+export const SelectableLabware = (props: Props): JSX.Element => {
+ const {
+ labwareProps,
+ selectedPrimaryWells,
+ selectWells,
+ deselectWells,
+ updateHighlightedWells,
+ nozzleType,
+ ingredNames,
+ wellContents,
+ } = props
+ const labwareDef = labwareProps.definition
- _wellsFromSelected: (
+ const _wellsFromSelected: (
selectedWells: WellGroup
) => WellGroup = selectedWells => {
- const labwareDef = this.props.labwareProps.definition
-
// Returns PRIMARY WELLS from the selection.
- if (this.props.nozzleType != null) {
- const channels = getChannelsFromNozleType(this.props.nozzleType)
+ if (nozzleType != null) {
+ const channels = getChannelsFromNozleType(nozzleType)
// for the wells that have been highlighted,
// get all 8-well well sets and merge them
const primaryWells: WellGroup = reduce(
@@ -78,15 +83,19 @@ export class SelectableLabware extends React.Component {
return selectedWells
}
- handleSelectionMove: (e: MouseEvent, rect: GenericRect) => void = (
+ const _getWellsFromRect: (rect: GenericRect) => WellGroup = rect => {
+ const selectedWells = getCollidingWells(rect)
+ return _wellsFromSelected(selectedWells)
+ }
+
+ const handleSelectionMove: (e: MouseEvent, rect: GenericRect) => void = (
e,
rect
) => {
- const labwareDef = this.props.labwareProps.definition
if (!e.shiftKey) {
- if (this.props.nozzleType != null) {
- const channels = getChannelsFromNozleType(this.props.nozzleType)
- const selectedWells = this._getWellsFromRect(rect)
+ if (nozzleType != null) {
+ const channels = getChannelsFromNozleType(nozzleType)
+ const selectedWells = _getWellsFromRect(rect)
const allWellsForMulti: WellGroup = reduce(
selectedWells,
(acc: WellGroup, _, wellName: string): WellGroup => {
@@ -100,104 +109,90 @@ export class SelectableLabware extends React.Component {
},
{}
)
- this.props.updateHighlightedWells(allWellsForMulti)
+ updateHighlightedWells(allWellsForMulti)
} else {
- this.props.updateHighlightedWells(this._getWellsFromRect(rect))
+ updateHighlightedWells(_getWellsFromRect(rect))
}
}
}
- handleSelectionDone: (e: MouseEvent, rect: GenericRect) => void = (
+ const handleSelectionDone: (e: MouseEvent, rect: GenericRect) => void = (
e,
rect
) => {
- const wells = this._wellsFromSelected(this._getWellsFromRect(rect))
+ const wells = _wellsFromSelected(_getWellsFromRect(rect))
if (e.shiftKey) {
- this.props.deselectWells(wells)
+ deselectWells(wells)
} else {
- this.props.selectWells(wells)
+ selectWells(wells)
}
}
- handleMouseEnterWell: (args: WellMouseEvent) => void = args => {
- if (this.props.nozzleType != null) {
- const channels = getChannelsFromNozleType(this.props.nozzleType)
- const labwareDef = this.props.labwareProps.definition
+ const handleMouseEnterWell: (args: WellMouseEvent) => void = args => {
+ if (nozzleType != null) {
+ const channels = getChannelsFromNozleType(nozzleType)
const wellSet = getWellSetForMultichannel(
labwareDef,
args.wellName,
channels
)
const nextHighlightedWells = arrayToWellGroup(wellSet || [])
- nextHighlightedWells &&
- this.props.updateHighlightedWells(nextHighlightedWells)
+ nextHighlightedWells && updateHighlightedWells(nextHighlightedWells)
} else {
- this.props.updateHighlightedWells({ [args.wellName]: null })
+ updateHighlightedWells({ [args.wellName]: null })
}
}
- handleMouseLeaveWell: (args: WellMouseEvent) => void = args => {
- this.props.updateHighlightedWells({})
- }
-
- render(): React.ReactNode {
- const {
- labwareProps,
- ingredNames,
- wellContents,
- nozzleType,
- selectedPrimaryWells,
- } = this.props
- // For rendering, show all wells not just primary wells
- const allSelectedWells =
- nozzleType != null
- ? reduce(
- selectedPrimaryWells,
- (acc, _, wellName): WellGroup => {
- const channels = getChannelsFromNozleType(nozzleType)
- const wellSet = getWellSetForMultichannel(
- this.props.labwareProps.definition,
- wellName,
- channels
- )
- if (!wellSet) return acc
- return { ...acc, ...arrayToWellGroup(wellSet) }
- },
- {}
- )
- : selectedPrimaryWells
+ // For rendering, show all wells not just primary wells
+ const allSelectedWells =
+ nozzleType != null
+ ? reduce(
+ selectedPrimaryWells,
+ (acc, _, wellName): WellGroup => {
+ const channels = getChannelsFromNozleType(nozzleType)
+ const wellSet = getWellSetForMultichannel(
+ labwareDef,
+ wellName,
+ channels
+ )
+ if (!wellSet) return acc
+ return { ...acc, ...arrayToWellGroup(wellSet) }
+ },
+ {}
+ )
+ : selectedPrimaryWells
- return (
-
-
- {({
- makeHandleMouseEnterWell,
- handleMouseLeaveWell,
- tooltipWellName,
- }) => (
- {
- this.handleMouseLeaveWell(mouseEventArgs)
- handleMouseLeaveWell(mouseEventArgs.event)
- }}
- onMouseEnterWell={({ wellName, event }) => {
- if (wellContents !== null) {
- this.handleMouseEnterWell({ wellName, event })
- makeHandleMouseEnterWell(
- wellName,
- wellContents[wellName]?.ingreds
- )(event)
- }
- }}
- />
- )}
-
-
- )
- }
+ return (
+
+
+ {({
+ makeHandleMouseEnterWell,
+ handleMouseLeaveWell,
+ tooltipWellName,
+ }) => (
+ {
+ handleMouseLeaveWell(mouseEventArgs)
+ updateHighlightedWells({})
+ handleMouseLeaveWell(mouseEventArgs.event)
+ }}
+ onMouseEnterWell={({ wellName, event }) => {
+ if (wellContents !== null) {
+ handleMouseEnterWell({ wellName, event })
+ makeHandleMouseEnterWell(
+ wellName,
+ wellContents[wellName]?.ingreds
+ )(event)
+ }
+ }}
+ />
+ )}
+
+
+ )
}
diff --git a/protocol-designer/src/components/labware/WellTooltip.tsx b/protocol-designer/src/components/labware/WellTooltip.tsx
index cb88982d99c..d1e531c500a 100644
--- a/protocol-designer/src/components/labware/WellTooltip.tsx
+++ b/protocol-designer/src/components/labware/WellTooltip.tsx
@@ -2,11 +2,13 @@ import * as React from 'react'
import { Popper, Reference, Manager } from 'react-popper'
import cx from 'classnames'
+import { LocationLiquidState } from '@opentrons/step-generation'
import { Portal } from '../portals/TopPortal'
import { PillTooltipContents } from '../steplist/SubstepRow'
+
import styles from './labware.css'
-import { LocationLiquidState } from '@opentrons/step-generation'
-import { WellIngredientNames } from '../../steplist/types'
+
+import type { WellIngredientNames } from '../../steplist/types'
const DEFAULT_TOOLTIP_OFFSET = 22
const WELL_BORDER_WIDTH = 4
@@ -20,7 +22,7 @@ interface WellTooltipParams {
tooltipWellName?: string | null
}
-interface Props {
+interface WellTooltipProps {
children: (wellTooltipParams: WellTooltipParams) => React.ReactNode
ingredNames: WellIngredientNames
}
@@ -32,7 +34,7 @@ interface State {
tooltipWellIngreds?: LocationLiquidState | null
tooltipOffset?: number | null
}
-const initialState: State = {
+const initialTooltipState: State = {
tooltipX: null,
tooltipY: null,
tooltipWellName: null,
@@ -40,10 +42,13 @@ const initialState: State = {
tooltipOffset: DEFAULT_TOOLTIP_OFFSET,
}
-export class WellTooltip extends React.Component {
- state: State = initialState
+export const WellTooltip = (props: WellTooltipProps): JSX.Element => {
+ const { children, ingredNames } = props
+ const [tooltipState, setTooltipState] = React.useState(
+ initialTooltipState
+ )
- makeHandleMouseEnterWell: (
+ const makeHandleMouseEnterWell: (
wellName: string,
wellIngreds: LocationLiquidState
) => (e: React.MouseEvent) => void = (wellName, wellIngreds) => e => {
@@ -52,7 +57,7 @@ export class WellTooltip extends React.Component {
const wellBoundingRect = target.getBoundingClientRect()
const { left, top, height, width } = wellBoundingRect
if (Object.keys(wellIngreds).length > 0 && left && top) {
- this.setState({
+ setTooltipState({
tooltipX: left + width / 2,
tooltipY: top + height / 2,
tooltipWellName: wellName,
@@ -63,69 +68,73 @@ export class WellTooltip extends React.Component {
}
}
- handleMouseLeaveWell: () => void = () => {
- this.setState(initialState)
+ const handleMouseLeaveWell = (): void => {
+ setTooltipState(initialTooltipState)
}
- render(): React.ReactNode {
- const { tooltipX, tooltipY, tooltipOffset } = this.state
+ const {
+ tooltipX,
+ tooltipY,
+ tooltipOffset,
+ tooltipWellIngreds,
+ tooltipWellName,
+ } = tooltipState
- return (
-
-
-
- {({ ref }) => (
-
-
-
- )}
-
- {this.props.children({
- makeHandleMouseEnterWell: this.makeHandleMouseEnterWell,
- handleMouseLeaveWell: this.handleMouseLeaveWell,
- tooltipWellName: this.state.tooltipWellName,
- })}
- {this.state.tooltipWellName && (
-
- {({ ref, style, placement, arrowProps }) => {
- return (
-
-
-
- )
- }}
-
+ return (
+ <>
+
+
+ {({ ref }) => (
+
+
+
)}
-
-
- )
- }
+
+ {children({
+ makeHandleMouseEnterWell: makeHandleMouseEnterWell,
+ handleMouseLeaveWell: handleMouseLeaveWell,
+ tooltipWellName: tooltipWellName,
+ })}
+ {tooltipWellName && (
+
+ {({ ref, style, placement, arrowProps }) => {
+ return (
+
+
+
+ )
+ }}
+
+ )}
+
+ >
+ )
}
diff --git a/protocol-designer/src/components/modals/FileUploadMessageModal/FileUploadMessageModal.tsx b/protocol-designer/src/components/modals/FileUploadMessageModal/FileUploadMessageModal.tsx
index 875efea30a2..8567ab839cf 100644
--- a/protocol-designer/src/components/modals/FileUploadMessageModal/FileUploadMessageModal.tsx
+++ b/protocol-designer/src/components/modals/FileUploadMessageModal/FileUploadMessageModal.tsx
@@ -1,29 +1,30 @@
import * as React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import cx from 'classnames'
import { AlertModal, OutlineButton } from '@opentrons/components'
-import modalStyles from '../modal.css'
+import {
+ selectors as loadFileSelectors,
+ actions as loadFileActions,
+} from '../../../load-file'
import { getModalContents } from './modalContents'
-import { FileUploadMessage } from '../../../load-file'
-
-export interface FileUploadMessageModalProps {
- message?: FileUploadMessage | null
- cancelProtocolMigration: (event: React.MouseEvent) => unknown
- dismissModal: (event: React.MouseEvent) => unknown
-}
+import modalStyles from '../modal.css'
-export function FileUploadMessageModal(
- props: FileUploadMessageModalProps
-): JSX.Element | null {
- const { message, cancelProtocolMigration, dismissModal } = props
+export function FileUploadMessageModal(): JSX.Element | null {
+ const message = useSelector(loadFileSelectors.getFileUploadMessages)
+ const dispatch = useDispatch()
const { t } = useTranslation('button')
+
+ const dismissModal = (): void => {
+ dispatch(loadFileActions.dismissFileUploadMessage())
+ }
if (!message) return null
const { title, body, okButtonText } = getModalContents(message)
let buttons = [
{
children: t('cancel'),
- onClick: cancelProtocolMigration,
+ onClick: () => dispatch(loadFileActions.undoLoadFile()),
className: modalStyles.bottom_button,
},
{
diff --git a/protocol-designer/src/components/modals/FileUploadMessageModal/index.ts b/protocol-designer/src/components/modals/FileUploadMessageModal/index.ts
deleted file mode 100644
index 9e15523bfde..00000000000
--- a/protocol-designer/src/components/modals/FileUploadMessageModal/index.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import {
- FileUploadMessageModal as FileUploadMessageModalComponent,
- FileUploadMessageModalProps,
-} from './FileUploadMessageModal'
-import { connect } from 'react-redux'
-import {
- selectors as loadFileSelectors,
- actions as loadFileActions,
-} from '../../../load-file'
-import { Dispatch } from 'redux'
-import { BaseState } from '../../../types'
-
-type Props = FileUploadMessageModalProps
-interface SP {
- message: Props['message']
-}
-type DP = Omit
-
-function mapStateToProps(state: BaseState): SP {
- return {
- message: loadFileSelectors.getFileUploadMessages(state),
- }
-}
-
-function mapDispatchToProps(dispatch: Dispatch): DP {
- return {
- cancelProtocolMigration: () => dispatch(loadFileActions.undoLoadFile()),
- dismissModal: () => dispatch(loadFileActions.dismissFileUploadMessage()),
- }
-}
-
-export const FileUploadMessageModal = connect(
- mapStateToProps,
- mapDispatchToProps
-)(FileUploadMessageModalComponent)
diff --git a/protocol-designer/src/components/modals/LabwareUploadMessageModal/LabwareUploadMessageModal.tsx b/protocol-designer/src/components/modals/LabwareUploadMessageModal/LabwareUploadMessageModal.tsx
index 08e88586940..749613c7144 100644
--- a/protocol-designer/src/components/modals/LabwareUploadMessageModal/LabwareUploadMessageModal.tsx
+++ b/protocol-designer/src/components/modals/LabwareUploadMessageModal/LabwareUploadMessageModal.tsx
@@ -1,10 +1,15 @@
import * as React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import assert from 'assert'
import cx from 'classnames'
import { AlertModal, OutlineButton, ButtonProps } from '@opentrons/components'
+import {
+ selectors as labwareDefSelectors,
+ actions as labwareDefActions,
+ LabwareUploadMessage,
+} from '../../../labware-defs'
import modalStyles from '../modal.css'
-import { LabwareUploadMessage } from '../../../labware-defs'
const MessageBody = (props: {
message: LabwareUploadMessage
@@ -85,11 +90,32 @@ export interface LabwareUploadMessageModalProps {
overwriteLabwareDef?: () => unknown
}
-export const LabwareUploadMessageModal = (
- props: LabwareUploadMessageModalProps
-): JSX.Element | null => {
- const { message, dismissModal, overwriteLabwareDef } = props
+export const LabwareUploadMessageModal = (): JSX.Element | null => {
+ const message = useSelector(labwareDefSelectors.getLabwareUploadMessage)
+ const dispatch = useDispatch()
const { t } = useTranslation('modal')
+ const dismissModal = (): void => {
+ dispatch(labwareDefActions.dismissLabwareUploadMessage())
+ }
+ const overwriteLabwareDef = (): void => {
+ if (message && message.messageType === 'ASK_FOR_LABWARE_OVERWRITE') {
+ dispatch(
+ labwareDefActions.replaceCustomLabwareDef({
+ defURIToOverwrite: message.defURIToOverwrite,
+ newDef: message.newDef,
+ isOverwriteMismatched: message.isOverwriteMismatched,
+ })
+ )
+ } else {
+ assert(
+ false,
+ `labware def should only be overwritten when messageType is ASK_FOR_LABWARE_OVERWRITE. Got ${String(
+ message?.messageType
+ )}`
+ )
+ }
+ }
+
if (!message) return null
let buttons: ButtonProps[] = [{ children: 'OK', onClick: dismissModal }]
diff --git a/protocol-designer/src/components/modals/LabwareUploadMessageModal/index.ts b/protocol-designer/src/components/modals/LabwareUploadMessageModal/index.ts
deleted file mode 100644
index dae8d981612..00000000000
--- a/protocol-designer/src/components/modals/LabwareUploadMessageModal/index.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import assert from 'assert'
-import { connect } from 'react-redux'
-import {
- selectors as labwareDefSelectors,
- actions as labwareDefActions,
-} from '../../../labware-defs'
-import {
- LabwareUploadMessageModal as LabwareUploadMessageModalComponent,
- LabwareUploadMessageModalProps,
-} from './LabwareUploadMessageModal'
-import { Dispatch } from 'redux'
-import { BaseState } from '../../../types'
-
-type Props = LabwareUploadMessageModalProps
-interface SP {
- message: Props['message']
-}
-
-function mapStateToProps(state: BaseState): SP {
- return {
- message: labwareDefSelectors.getLabwareUploadMessage(state),
- }
-}
-
-function mergeProps(
- stateProps: SP,
- dispatchProps: {
- dispatch: Dispatch
- }
-): Props {
- const { dispatch } = dispatchProps
- const { message } = stateProps
- return {
- ...stateProps,
- overwriteLabwareDef: () => {
- if (message && message.messageType === 'ASK_FOR_LABWARE_OVERWRITE') {
- dispatch(
- labwareDefActions.replaceCustomLabwareDef({
- defURIToOverwrite: message.defURIToOverwrite,
- newDef: message.newDef,
- isOverwriteMismatched: message.isOverwriteMismatched,
- })
- )
- } else {
- assert(
- false,
- `labware def should only be overwritten when messageType is ASK_FOR_LABWARE_OVERWRITE. Got ${String(
- message?.messageType
- )}`
- )
- }
- },
- dismissModal: () =>
- dispatch(labwareDefActions.dismissLabwareUploadMessage()),
- }
-}
-
-export const LabwareUploadMessageModal = connect(
- mapStateToProps,
- null,
- mergeProps
-)(LabwareUploadMessageModalComponent)
diff --git a/protocol-designer/src/components/steplist/MultiChannelSubstep.tsx b/protocol-designer/src/components/steplist/MultiChannelSubstep.tsx
index 9464c7f6246..3e85f149859 100644
--- a/protocol-designer/src/components/steplist/MultiChannelSubstep.tsx
+++ b/protocol-designer/src/components/steplist/MultiChannelSubstep.tsx
@@ -4,10 +4,10 @@ import cx from 'classnames'
import { Icon } from '@opentrons/components'
import { PDListItem } from '../lists'
import { SubstepRow } from './SubstepRow'
-import styles from './StepItem.css'
import { formatVolume } from './utils'
+import styles from './StepItem.css'
-import {
+import type {
StepItemSourceDestRow,
SubstepIdentifier,
WellIngredientNames,
@@ -18,95 +18,87 @@ const DEFAULT_COLLAPSED_STATE = true
interface MultiChannelSubstepProps {
rowGroup: StepItemSourceDestRow[]
ingredNames: WellIngredientNames
- highlighted?: boolean
stepId: string
substepIndex: number
- selectSubstep: (substepIdentifier: SubstepIdentifier) => unknown
+ selectSubstep: (substepIdentifier: SubstepIdentifier) => void
+ highlighted?: boolean
}
-interface MultiChannelSubstepState {
- collapsed: boolean
-}
+export function MultiChannelSubstep(
+ props: MultiChannelSubstepProps
+): JSX.Element {
+ const [collapsed, setCollapsed] = React.useState(
+ DEFAULT_COLLAPSED_STATE
+ )
-export class MultiChannelSubstep extends React.PureComponent<
- MultiChannelSubstepProps,
- MultiChannelSubstepState
-> {
- state: MultiChannelSubstepState = { collapsed: DEFAULT_COLLAPSED_STATE }
+ const {
+ rowGroup,
+ highlighted,
+ stepId,
+ selectSubstep,
+ substepIndex,
+ ingredNames,
+ } = props
- handleToggleCollapsed: () => void = () => {
- this.setState({ collapsed: !this.state.collapsed })
+ const handleToggleCollapsed = (): void => {
+ setCollapsed(!collapsed)
}
- render(): React.ReactNode {
- const {
- rowGroup,
- highlighted,
- stepId,
- selectSubstep,
- substepIndex,
- } = this.props
- const { collapsed } = this.state
-
- // NOTE: need verbose null check for flow to be happy
- const firstChannelSource = rowGroup[0].source
- const lastChannelSource = rowGroup[rowGroup.length - 1].source
- const sourceWellRange = `${
- firstChannelSource ? firstChannelSource.well : ''
- }:${lastChannelSource ? lastChannelSource.well : ''}`
- const firstChannelDest = rowGroup[0].dest
- const lastChannelDest = rowGroup[rowGroup.length - 1].dest
- const destWellRange = `${firstChannelDest ? firstChannelDest.well : ''}:${
- lastChannelDest ? lastChannelDest.well : ''
- }`
- return (
- selectSubstep({ stepId, substepIndex })}
- onMouseLeave={() => selectSubstep(null)}
- className={cx({ [styles.highlighted]: highlighted })}
+ // NOTE: need verbose null check for flow to be happy
+ const firstChannelSource = rowGroup[0].source
+ const lastChannelSource = rowGroup[rowGroup.length - 1].source
+ const sourceWellRange = `${
+ firstChannelSource ? firstChannelSource.well : ''
+ }:${lastChannelSource ? lastChannelSource.well : ''}`
+ const firstChannelDest = rowGroup[0].dest
+ const lastChannelDest = rowGroup[rowGroup.length - 1].dest
+ const destWellRange = `${firstChannelDest ? firstChannelDest.well : ''}:${
+ lastChannelDest ? lastChannelDest.well : ''
+ }`
+ return (
+ selectSubstep({ stepId, substepIndex })}
+ onMouseLeave={() => selectSubstep(null)}
+ className={cx({ [styles.highlighted]: highlighted })}
+ >
+ {/* Header row */}
+
- {/* Header row */}
-
- multi
-
- {firstChannelSource ? sourceWellRange : ''}
-
- {`${formatVolume(
- rowGroup[0].volume
- )} μL`}
-
- {firstChannelDest ? destWellRange : ''}
-
-
-
-
-
+ multi
+
+ {firstChannelSource ? sourceWellRange : ''}
+
+ {`${formatVolume(
+ rowGroup[0].volume
+ )} μL`}
+
+ {firstChannelDest ? destWellRange : ''}
+
+
+
+
+
- {!collapsed &&
- rowGroup.map((row, rowKey) => {
- // Channel rows (1 for each channel in multi-channel pipette
- return (
-
- )
- })}
-
- )
- }
+ {!collapsed &&
+ rowGroup.map((row, rowKey) => {
+ // Channel rows (1 for each channel in multi-channel pipette
+ return (
+
+ )
+ })}
+
+ )
}
diff --git a/protocol-designer/src/components/steplist/StartingDeckStateTerminalItem.tsx b/protocol-designer/src/components/steplist/StartingDeckStateTerminalItem.tsx
index 0c40c7c0337..7a4009665ab 100644
--- a/protocol-designer/src/components/steplist/StartingDeckStateTerminalItem.tsx
+++ b/protocol-designer/src/components/steplist/StartingDeckStateTerminalItem.tsx
@@ -1,20 +1,14 @@
-import { connect } from 'react-redux'
import * as React from 'react'
-import { TerminalItem } from './TerminalItem'
+import { useSelector } from 'react-redux'
import { PDListItem } from '../lists'
import { START_TERMINAL_TITLE } from '../../constants'
-import { BaseState } from '../../types'
import { START_TERMINAL_ITEM_ID } from '../../steplist'
import { selectors as stepFormSelectors } from '../../step-forms'
+import { TerminalItem } from './TerminalItem'
-interface Props {
- showHint: boolean
-}
-
-type SP = Props
-
-function StartingDeckStateTerminalItemComponent(props: Props): JSX.Element {
- const { showHint } = props
+export function StartingDeckStateTerminalItem(): JSX.Element {
+ const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities)
+ const showHint = Object.keys(labwareEntities).length <= 1
const hintContents = (
Add labware to the deck and assign liquids to the wells they start in
@@ -27,14 +21,3 @@ function StartingDeckStateTerminalItemComponent(props: Props): JSX.Element {
)
}
-
-function mapStateToProps(state: BaseState): SP {
- // since default-trash counts as 1, labwareCount <= 1 means "user did not add labware"
- const noLabware =
- Object.keys(stepFormSelectors.getLabwareEntities(state)).length <= 1
- return { showHint: noLabware }
-}
-
-export const StartingDeckStateTerminalItem = connect(mapStateToProps)(
- StartingDeckStateTerminalItemComponent
-)
diff --git a/protocol-designer/src/components/steplist/StepList.tsx b/protocol-designer/src/components/steplist/StepList.tsx
index da83c2afb16..f496a4a1922 100644
--- a/protocol-designer/src/components/steplist/StepList.tsx
+++ b/protocol-designer/src/components/steplist/StepList.tsx
@@ -1,28 +1,37 @@
import * as React from 'react'
+import { useDispatch, useSelector } from 'react-redux'
import { SidePanel } from '@opentrons/components'
-import { PresavedStepItem } from './PresavedStepItem'
-import { StartingDeckStateTerminalItem } from './StartingDeckStateTerminalItem'
-import { TerminalItem } from './TerminalItem'
import { END_TERMINAL_TITLE } from '../../constants'
-import { END_TERMINAL_ITEM_ID } from '../../steplist'
-
+import {
+ END_TERMINAL_ITEM_ID,
+ actions as steplistActions,
+} from '../../steplist'
+import { actions as stepsActions, getIsMultiSelectMode } from '../../ui/steps'
+import { selectors as stepFormSelectors } from '../../step-forms'
import { StepCreationButton } from '../StepCreationButton'
import { DraggableStepItems } from './DraggableStepItems'
import { MultiSelectToolbar } from './MultiSelectToolbar'
+import { PresavedStepItem } from './PresavedStepItem'
+import { StartingDeckStateTerminalItem } from './StartingDeckStateTerminalItem'
+import { TerminalItem } from './TerminalItem'
-import { StepIdType } from '../../form-types'
+import type { StepIdType } from '../../form-types'
+import type { ThunkDispatch } from '../../types'
export interface StepListProps {
isMultiSelectMode?: boolean | null
orderedStepIds: StepIdType[]
- reorderSelectedStep: (delta: number) => unknown
- reorderSteps: (steps: StepIdType[]) => unknown
+ reorderSelectedStep: (delta: number) => void
+ reorderSteps: (steps: StepIdType[]) => void
}
-export class StepList extends React.Component {
- handleKeyDown: (e: KeyboardEvent) => void = e => {
- const { reorderSelectedStep } = this.props
+export const StepList = (): JSX.Element => {
+ const orderedStepIds = useSelector(stepFormSelectors.getOrderedStepIds)
+ const isMultiSelectMode = useSelector(getIsMultiSelectMode)
+ const dispatch = useDispatch>()
+
+ const handleKeyDown: (e: KeyboardEvent) => void = e => {
const key = e.key
const altIsPressed = e.altKey
@@ -34,34 +43,36 @@ export class StepList extends React.Component {
delta = 1
}
if (!delta) return
- reorderSelectedStep(delta)
+ dispatch(stepsActions.reorderSelectedStep(delta))
}
}
- componentDidMount(): void {
- global.addEventListener('keydown', this.handleKeyDown, false)
- }
+ React.useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent): void => {
+ handleKeyDown(e)
+ }
- componentWillUnmount(): void {
- global.removeEventListener('keydown', this.handleKeyDown, false)
- }
+ global.addEventListener('keydown', onKeyDown, false)
- render(): React.ReactNode {
- return (
-
-
+ return () => {
+ global.removeEventListener('keydown', onKeyDown, false)
+ }
+ }, [])
-
-
-
-
-
-
- )
- }
+ return (
+
+
+
+
+ {
+ dispatch(steplistActions.reorderSteps(stepIds))
+ }}
+ />
+
+
+
+
+ )
}
diff --git a/protocol-designer/src/containers/ConnectedMainPanel.tsx b/protocol-designer/src/containers/ConnectedMainPanel.tsx
index febe576eea2..0bb424298e3 100644
--- a/protocol-designer/src/containers/ConnectedMainPanel.tsx
+++ b/protocol-designer/src/containers/ConnectedMainPanel.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
-import { connect } from 'react-redux'
+import { useSelector } from 'react-redux'
import { Splash } from '@opentrons/components'
-import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../steplist'
+import { START_TERMINAL_ITEM_ID } from '../steplist'
import { Portal as MainPageModalPortal } from '../components/portals/MainPageModalPortal'
import { DeckSetupManager } from '../components/DeckSetupManager'
import { SettingsPage } from '../components/SettingsPage'
@@ -9,23 +9,21 @@ import { FilePage } from '../components/FilePage'
import { LiquidsPage } from '../components/LiquidsPage'
import { Hints } from '../components/Hints'
import { LiquidPlacementModal } from '../components/LiquidPlacementModal'
-import { LabwareSelectionModal } from '../components/LabwareSelectionModal'
+import { LabwareSelectionModal } from '../components/LabwareSelectionModal/LabwareSelectionModal'
import { FormManager } from '../components/FormManager'
import { Alerts } from '../components/alerts/Alerts'
-
import { getSelectedTerminalItemId } from '../ui/steps'
import { selectors as labwareIngredSelectors } from '../labware-ingred/selectors'
-import { selectors, Page } from '../navigation'
-import { BaseState } from '../types'
+import { selectors } from '../navigation'
-interface Props {
- page: Page
- selectedTerminalItemId: TerminalItemId | null | undefined
- ingredSelectionMode: boolean
-}
+export function MainPanel(): JSX.Element {
+ const page = useSelector(selectors.getCurrentPage)
+ const selectedTerminalItemId = useSelector(getSelectedTerminalItemId)
+ const selectedLabware = useSelector(
+ labwareIngredSelectors.getSelectedLabwareId
+ )
+ const ingredSelectionMode = selectedLabware != null
-function MainPanelComponent(props: Props): JSX.Element {
- const { page, selectedTerminalItemId, ingredSelectionMode } = props
switch (page) {
case 'file-splash':
return
@@ -55,14 +53,3 @@ function MainPanelComponent(props: Props): JSX.Element {
}
}
}
-
-function mapStateToProps(state: BaseState): Props {
- return {
- page: selectors.getCurrentPage(state),
- selectedTerminalItemId: getSelectedTerminalItemId(state),
- ingredSelectionMode:
- labwareIngredSelectors.getSelectedLabwareId(state) != null,
- }
-}
-
-export const ConnectedMainPanel = connect(mapStateToProps)(MainPanelComponent)
diff --git a/protocol-designer/src/containers/ConnectedSidebar.tsx b/protocol-designer/src/containers/ConnectedSidebar.tsx
index 092c288f4d1..914dade085f 100644
--- a/protocol-designer/src/containers/ConnectedSidebar.tsx
+++ b/protocol-designer/src/containers/ConnectedSidebar.tsx
@@ -1,31 +1,25 @@
import * as React from 'react'
-import { connect } from 'react-redux'
-import { selectors, Page } from '../navigation'
+import { useSelector } from 'react-redux'
+import { selectors } from '../navigation'
import { selectors as labwareIngredSelectors } from '../labware-ingred/selectors'
-import { ConnectedStepList } from './ConnectedStepList'
-import { IngredientsList } from './IngredientsList'
+import { StepList } from '../components/steplist'
import { FileSidebar } from '../components/FileSidebar/FileSidebar'
import { LiquidsSidebar } from '../components/LiquidsSidebar'
import { SettingsSidebar } from '../components/SettingsPage'
+import { IngredientsList } from '../components/IngredientsList'
-import { BaseState } from '../types'
-
-interface Props {
- page: Page
- liquidPlacementMode: boolean
-}
-
-function Sidebar(props: Props): JSX.Element | null {
- switch (props.page) {
+export function Sidebar(): JSX.Element | null {
+ const page = useSelector(selectors.getCurrentPage)
+ const selectedLabware = useSelector(
+ labwareIngredSelectors.getSelectedLabwareId
+ )
+ const liquidPlacementMode = selectedLabware != null
+ switch (page) {
case 'liquids':
return
case 'steplist':
- return props.liquidPlacementMode ? (
-
- ) : (
-
- )
+ return liquidPlacementMode ? :
case 'file-splash':
case 'file-detail':
return
@@ -35,16 +29,3 @@ function Sidebar(props: Props): JSX.Element | null {
}
return null
}
-
-function mapStateToProps(state: BaseState): Props {
- const page = selectors.getCurrentPage(state)
- const liquidPlacementMode =
- labwareIngredSelectors.getSelectedLabwareId(state) != null
-
- return {
- page,
- liquidPlacementMode,
- }
-}
-
-export const ConnectedSidebar = connect(mapStateToProps)(Sidebar)
diff --git a/protocol-designer/src/containers/ConnectedStepList.ts b/protocol-designer/src/containers/ConnectedStepList.ts
deleted file mode 100644
index 20b2218c036..00000000000
--- a/protocol-designer/src/containers/ConnectedStepList.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { connect } from 'react-redux'
-import { BaseState, ThunkDispatch } from '../types'
-import { StepIdType } from '../form-types'
-import { actions as steplistActions } from '../steplist'
-import { actions as stepsActions, getIsMultiSelectMode } from '../ui/steps'
-import { selectors as stepFormSelectors } from '../step-forms'
-import { StepList, StepListProps } from '../components/steplist'
-
-type Props = StepListProps
-interface SP {
- orderedStepIds: Props['orderedStepIds']
- isMultiSelectMode: boolean | null | undefined
-}
-type DP = Omit
-
-function mapStateToProps(state: BaseState): SP {
- return {
- orderedStepIds: stepFormSelectors.getOrderedStepIds(state),
- isMultiSelectMode: getIsMultiSelectMode(state),
- }
-}
-
-function mapDispatchToProps(dispatch: ThunkDispatch): DP {
- return {
- reorderSelectedStep: (delta: number) => {
- dispatch(stepsActions.reorderSelectedStep(delta))
- },
- reorderSteps: (stepIds: StepIdType[]) => {
- dispatch(steplistActions.reorderSteps(stepIds))
- },
- }
-}
-
-export const ConnectedStepList = connect(
- mapStateToProps,
- mapDispatchToProps
-)(StepList)
diff --git a/protocol-designer/src/containers/IngredientsList.ts b/protocol-designer/src/containers/IngredientsList.ts
deleted file mode 100644
index af2a3255613..00000000000
--- a/protocol-designer/src/containers/IngredientsList.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as React from 'react'
-import { connect } from 'react-redux'
-import { Dispatch } from 'redux'
-import { selectors as labwareIngredSelectors } from '../labware-ingred/selectors'
-import * as wellSelectionSelectors from '../top-selectors/well-contents'
-import { removeWellsContents } from '../labware-ingred/actions'
-import { BaseState } from '../types'
-import { IngredientsList as IngredientsListComponent } from '../components/IngredientsList'
-import { SelectedContainerId } from '../labware-ingred/reducers'
-type Props = React.ComponentProps
-type SP = Omit & {
- _labwareId: string | null | undefined
-}
-
-function mapStateToProps(state: BaseState): SP {
- const selectedLabwareId: SelectedContainerId = labwareIngredSelectors.getSelectedLabwareId(
- state
- ) as SelectedContainerId
- const labwareWellContents =
- (selectedLabwareId &&
- labwareIngredSelectors.getLiquidsByLabwareId(state)[selectedLabwareId]) ||
- {}
- return {
- liquidGroupsById: labwareIngredSelectors.getLiquidGroupsById(state),
- labwareWellContents,
- selectedIngredientGroupId: wellSelectionSelectors.getSelectedWellsCommonIngredId(
- state
- ),
- selected: false,
- _labwareId: selectedLabwareId,
- }
-}
-
-function mergeProps(
- stateProps: SP,
- dispatchProps: {
- dispatch: Dispatch
- }
-): Props {
- const { dispatch } = dispatchProps
- const { _labwareId, ...passThruProps } = stateProps
- return {
- ...passThruProps,
- removeWellsContents: args => {
- if (_labwareId) {
- dispatch(removeWellsContents({ ...args, labwareId: _labwareId }))
- }
- },
- }
-}
-
-export const IngredientsList = connect(
- mapStateToProps,
- null,
- mergeProps
-)(IngredientsListComponent)
diff --git a/protocol-designer/src/localization/index.ts b/protocol-designer/src/localization/index.ts
index c5c643bbabe..9937020ba98 100644
--- a/protocol-designer/src/localization/index.ts
+++ b/protocol-designer/src/localization/index.ts
@@ -37,11 +37,6 @@ i18n.use(initReactI18next).init(
},
},
saveMissing: true,
- missingKeyHandler: (lng, ns, key) => {
- process.env.NODE_ENV === 'test'
- ? console.error(`Missing ${lng} Translation: key={${key}} ns={${ns}}`)
- : console.warn(`Missing ${lng} Translation: key={${key}} ns={${ns}}`)
- },
},
err => {
if (err) {