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 ( -
-
{value}
- -
+ + {({ ref }) => ( +
+ } + /> + + )} +
) } + + return ( +
+
{value}
+ +
+ ) } 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) {