Skip to content

Commit

Permalink
feat(protocol-designer): pipette tiprack assignment
Browse files Browse the repository at this point in the history
- refactor pipette reducer to 'byMount' and 'byId' (instead of 'left' and 'right')
- update pipette selectors with new reducer state shape
- NewFileModal allows & requires user to save tiprack model per pipette
- only allow adding assigned tiprack models

Closes #1750
  • Loading branch information
IanLondon authored Jun 27, 2018
1 parent 7c51e98 commit e0555af
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 100 deletions.
9 changes: 6 additions & 3 deletions protocol-designer/src/components/LabwareDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ function LabwareItem (props: LabwareItemProps) {
type LabwareDropdownProps = {
onClose: (e?: SyntheticEvent<*>) => void,
onContainerChoose: OnContainerChoose,
slot: string | false
slot: string | false,
permittedTipracks: Array<string>
}

export default function LabwareDropdown (props: LabwareDropdownProps) {
const {onClose, onContainerChoose, slot} = props
const {onClose, onContainerChoose, slot, permittedTipracks} = props
// do not render without a slot
if (!slot) return null

Expand All @@ -58,7 +59,9 @@ export default function LabwareDropdown (props: LabwareDropdownProps) {
['tiprack-200ul', '200uL Tip Rack', 'Tiprack-200ul'],
['tiprack-1000ul', '1000uL Tip Rack', 'Tiprack-200ul'],
['tiprack-1000ul-chem', '10x10 1000uL Chem-Tip Rack', 'Tiprack-1000ul-chem']
].map(labwareItemMapper)}
].filter(labwareModelNameImage =>
permittedTipracks.includes(labwareModelNameImage[0])
).map(labwareItemMapper)}
</Accordion>
<Accordion title='Tube Rack'>
{[
Expand Down
130 changes: 108 additions & 22 deletions protocol-designer/src/components/modals/NewFileModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ import formStyles from '../forms.css'
import modalStyles from './modal.css'

type State = {
name?: string,
leftPipette?: string, // TODO: make this union of pipette option values
rightPipette?: string,
name: string,

// TODO: make this union of pipette option values
leftPipette: string,
rightPipette: string,

// TODO: Ian 2018-06-22 type as labware-type enums of tipracks
leftTiprackModel: ?string,
rightTiprackModel: ?string
}

type Props = {
Expand All @@ -24,19 +30,40 @@ type Props = {
onSave: State => mixed
}

// 'invalid' state is just a concern of these dropdowns, not selected pipette state in general
const INVALID = 'INVALID'
// 'USER_HAS_NOT_SELECTED' state is just a concern of these dropdowns,
// not selected pipette state in general
// It's needed b/c user must select 'None' explicitly,
// they cannot just leave the dropdown blank.
const USER_HAS_NOT_SELECTED = 'USER_HAS_NOT_SELECTED'
// TODO: Ian 2018-06-22 use pristinity instead of this?

const pipetteOptionsWithInvalid = [
{name: '', value: INVALID},
const pipetteOptionsWithNone = [
{name: 'None', value: ''},
...pipetteOptions
]

const pipetteOptionsWithInvalid = [
{name: '', value: USER_HAS_NOT_SELECTED},
...pipetteOptionsWithNone
]

// TODO: Ian 2018-06-22 get this programatically from shared-data labware defs
// and exclude options that are incompatible with pipette
// and also auto-select tiprack if there's only one compatible tiprack for a pipette
const tiprackOptions = [
{name: '10 μL', value: 'tiprack-10ul'},
{name: '200 μL', value: 'tiprack-200ul'},
{name: '1000 μL', value: 'tiprack-1000ul'},
{name: '1000 μL Chem', value: 'tiprack-1000ul-chem'}
// {name: '300 μL', value: 'GEB-tiprack-300ul'} // NOTE this is not supported by Python API yet
]

const initialState = {
name: '',
leftPipette: INVALID,
rightPipette: INVALID
leftPipette: USER_HAS_NOT_SELECTED,
rightPipette: USER_HAS_NOT_SELECTED,
leftTiprackModel: null,
rightTiprackModel: null
}

export default class NewFileModal extends React.Component<Props, State> {
Expand All @@ -53,10 +80,26 @@ export default class NewFileModal extends React.Component<Props, State> {
}

handleChange = (accessor: $Keys<State>) => (e: SyntheticInputEvent<*>) => {
// skip tiprack update if no pipette selected
if (accessor === 'leftTiprackModel' && !this.state.leftPipette) return
if (accessor === 'rightTiprackModel' && !this.state.rightPipette) return

const value: string = e.target.value

const nextState: {[$Keys<State>]: mixed} = {
[accessor]: value
}

// clear tiprack selection if corresponding pipette model is deselected
if (accessor === 'leftPipette' && !value) {
nextState.leftTiprackModel = null
} else if (accessor === 'rightPipette' && !value) {
nextState.rightTiprackModel = null
}

this.setState({
...this.state,
[accessor]: value
...nextState
})
}

Expand All @@ -69,9 +112,52 @@ export default class NewFileModal extends React.Component<Props, State> {
return null
}

const {name, leftPipette, rightPipette} = this.state
const canSubmit = (leftPipette !== INVALID && rightPipette !== INVALID) && // neither can be invalid
(leftPipette || rightPipette) // at least one must not be none (empty string)
const {
name,
leftPipette,
rightPipette,
leftTiprackModel,
rightTiprackModel
} = this.state

const pipetteSelectionIsValid = (
// neither can be invalid
(leftPipette !== USER_HAS_NOT_SELECTED && rightPipette !== USER_HAS_NOT_SELECTED) &&
// at least one must not be none (empty string)
(leftPipette || rightPipette)
)

// if pipette selected, corresponding tiprack type also selected
const tiprackSelectionIsValid = (
(leftPipette ? Boolean(leftTiprackModel) : true) &&
(rightPipette ? Boolean(rightTiprackModel) : true)
)

const canSubmit = pipetteSelectionIsValid && tiprackSelectionIsValid

const pipetteFields = [
['leftPipette', 'Left Pipette'],
['rightPipette', 'Right Pipette']
].map(([name, label]) => {
const value = this.state[name]
return (
<FormGroup key={name} label={`${label}*:`} className={formStyles.column_1_2}>
<DropdownField options={value === USER_HAS_NOT_SELECTED
? pipetteOptionsWithInvalid
: pipetteOptionsWithNone}
value={value}
onChange={this.handleChange(name)} />
</FormGroup>
)
})

const tiprackFields = ['leftTiprackModel', 'rightTiprackModel'].map(name => (
<FormGroup key={name} label='Tip rack*:' className={formStyles.column_1_2}>
<DropdownField options={tiprackOptions}
value={this.state[name]}
onChange={this.handleChange(name)} />
</FormGroup>
))

return <AlertModal className={modalStyles.modal}
buttons={[
Expand All @@ -82,22 +168,22 @@ export default class NewFileModal extends React.Component<Props, State> {
<h2>Create New Protocol</h2>

<FormGroup label='Protocol Name:'>
<InputField placeholder='Untitled' value={name} onChange={this.handleChange('name')} />
<InputField placeholder='Untitled'
value={name}
onChange={this.handleChange('name')}
/>
</FormGroup>

<div className={styles.pipette_text}>
Select the pipettes you will be using. This cannot be changed later.
</div>

<div className={formStyles.row_wrapper}>
<FormGroup label='Left pipette*:' className={formStyles.column_1_2}>
<DropdownField options={pipetteOptionsWithInvalid}
value={leftPipette} onChange={this.handleChange('leftPipette')} />
</FormGroup>
<FormGroup label='Right pipette*:' className={formStyles.column_1_2}>
<DropdownField options={pipetteOptionsWithInvalid}
value={rightPipette} onChange={this.handleChange('rightPipette')} />
</FormGroup>
{pipetteFields}
</div>

<div className={formStyles.row_wrapper}>
{tiprackFields}
</div>
</form>
</AlertModal>
Expand Down
4 changes: 3 additions & 1 deletion protocol-designer/src/containers/ConnectedNewFileModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ function mapDispatchToProps (dispatch: Dispatch<*>): DispatchProps {

dispatch(pipetteActions.updatePipettes({
left: fields.leftPipette,
right: fields.rightPipette
right: fields.rightPipette,
leftTiprackModel: fields.leftTiprackModel,
rightTiprackModel: fields.rightTiprackModel
}))

dispatch(navigationActions.toggleNewProtocolModal(false))
Expand Down
53 changes: 32 additions & 21 deletions protocol-designer/src/containers/LabwareDropdown.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
// @flow
import { connect } from 'react-redux'
import * as React from 'react'
import {connect} from 'react-redux'
import type {Dispatch} from 'redux'
import { closeLabwareSelector, createContainer } from '../labware-ingred/actions'
import { selectors } from '../labware-ingred/reducers'
import {closeLabwareSelector, createContainer} from '../labware-ingred/actions'
import {selectors as labwareIngredSelectors} from '../labware-ingred/reducers'
import {selectors as pipetteSelectors} from '../pipettes'
import LabwareDropdown from '../components/LabwareDropdown.js'
import type {BaseState} from '../types'

export default connect(
(state: BaseState) => ({
slot: selectors.canAdd(state)
}),
(dispatch: Dispatch<*>) => ({dispatch}), // TODO Ian 2018-02-19 what does flow want for no-op mapDispatchToProps?
(stateProps, dispatchProps: {dispatch: Dispatch<*>}) => {
// TODO Ian 2017-12-04: Use thunks to grab slot, don't use this funky mergeprops
const dispatch = dispatchProps.dispatch
type Props = React.ElementProps<typeof LabwareDropdown>

return {
...stateProps,
onClose: () => {
dispatch(closeLabwareSelector())
},
onContainerChoose: (containerType) => {
if (stateProps.slot) {
dispatch(createContainer({slot: stateProps.slot, containerType}))
}
type SP = {
slot: $PropertyType<Props, 'slot'>,
permittedTipracks: $PropertyType<Props, 'permittedTipracks'>
}

function mapStateToProps (state: BaseState): SP {
return {
slot: labwareIngredSelectors.canAdd(state),
permittedTipracks: pipetteSelectors.permittedTipracks(state)
}
}

function mergeProps (stateProps: SP, dispatchProps: {dispatch: Dispatch<*>}): Props {
const dispatch = dispatchProps.dispatch

return {
...stateProps,
onClose: () => {
dispatch(closeLabwareSelector())
},
onContainerChoose: (containerType) => {
if (stateProps.slot) {
dispatch(createContainer({slot: stateProps.slot, containerType}))
}
}
}
)(LabwareDropdown)
}

export default connect(mapStateToProps, null, mergeProps)(LabwareDropdown)
3 changes: 1 addition & 2 deletions protocol-designer/src/labware-ingred/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type {
Wells
} from '../types'
import * as actions from '../actions'
import type {BaseState, Selector} from '../../types'
import type {BaseState, Selector, Options} from '../../types'
import type {CopyLabware, DeleteIngredient, EditIngredient} from '../actions'

// external actions (for types)
Expand Down Expand Up @@ -282,7 +282,6 @@ const loadedContainersBySlot = createSelector(
)

/** Returns options for dropdowns, excluding tiprack labware */
type Options = Array<{value: string, name: string}>
const labwareOptions: Selector<Options> = createSelector(
getLabware,
getLabwareNames,
Expand Down
8 changes: 7 additions & 1 deletion protocol-designer/src/pipettes/actions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// @flow
import type {PipetteName} from './pipetteData'

export const updatePipettes = (payload: {'left'?: ?PipetteName, 'right'?: ?PipetteName}) => ({
type UpdatePipettesPayload = {
left: ?PipetteName,
right: ?PipetteName,
leftTiprackModel: ?string,
rightTiprackModel: ?string
}
export const updatePipettes = (payload: UpdatePipettesPayload) => ({
type: 'UPDATE_PIPETTES',
payload
})
6 changes: 5 additions & 1 deletion protocol-designer/src/pipettes/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow
import * as actions from './actions'
import {rootReducer} from './reducers'
import {rootReducer, type RootState} from './reducers'
import * as selectors from './selectors'
export * from './types'

Expand All @@ -9,3 +9,7 @@ export {
rootReducer,
selectors
}

export type {
RootState
}
Loading

0 comments on commit e0555af

Please sign in to comment.