diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/NameThisLabware.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/NameThisLabware.tsx index 8582e690397..e0ef41b9d21 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/NameThisLabware.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/NameThisLabware.tsx @@ -26,7 +26,7 @@ export const NameThisLabware = (props: Props): JSX.Element => { } const saveNickname = (): void => { - setLabwareName() + setLabwareName(inputValue ?? null) } const wrapperRef: React.RefObject = useOnClickOutside({ onClickOutside: saveNickname, diff --git a/protocol-designer/src/components/EditableTextField.tsx b/protocol-designer/src/components/EditableTextField.tsx index a43f61041dc..ce43fbb2d27 100644 --- a/protocol-designer/src/components/EditableTextField.tsx +++ b/protocol-designer/src/components/EditableTextField.tsx @@ -9,80 +9,64 @@ 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(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/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/StepEditForm/fields/TipPositionField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx index 17e95716455..680c67192c2 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx @@ -21,18 +21,11 @@ import styles from './TipPositionInput.css' import type { FieldProps } from '../../types' -interface OP extends FieldProps { +interface Props extends FieldProps { labwareId?: string | null className?: string } -interface SP { - mmFromBottom: number | null - wellDepthMm: number -} - -type Props = OP & SP - export function TipPositionField(props: Props): JSX.Element { const { disabled, @@ -64,7 +57,7 @@ export function TipPositionField(props: Props): JSX.Element { } const handleOpen = (): void => { - if (props.wellDepthMm) { + if (wellDepthMm) { setModalOpen(true) } } 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 ( + + ) + })} +
+ ) }