Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

refactor(protocol-designer): gate timeline generation with form and field validation #2574

Merged
merged 12 commits into from
Nov 2, 2018
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@import '@opentrons/components';

:root {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just to fix up a css warning that was appearing in the js console that was bugging me.

--mw-labeled-toggle: 25rem;
}

.sidebar_item {
background-color: white;
margin: 0.4rem 0.125rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,33 @@ import WellSelectionModal from './WellSelectionModal'
import {Portal} from '../../portals/MainPageModalPortal'
import type {StepFieldName} from '../../../steplist/fieldLevel'
import styles from '../StepEditForm.css'
import type { FocusHandlers } from '../index'

type Props = {
name: StepFieldName,
primaryWellCount?: number,
disabled: boolean,
onClick?: (e: SyntheticMouseEvent<*>) => mixed,
errorToShow: ?string,
isMulti: ?boolean,
pipetteId: ?string,
labwareId: ?string,
onFieldBlur: $PropertyType<FocusHandlers, 'onFieldBlur'>,
onFieldFocus: $PropertyType<FocusHandlers, 'onFieldFocus'>,
}

type State = {isModalOpen: boolean}

class WellSelectionInput extends React.Component<Props, State> {
state = {isModalOpen: false}

toggleModal = () => {
this.setState({isModalOpen: !this.state.isModalOpen})
handleOpen = () => {
this.props.onFieldFocus(this.props.name)
this.setState({isModalOpen: true})
}

handleClose= () => {
this.props.onFieldBlur(this.props.name)
this.setState({isModalOpen: false})
}

render () {
Expand All @@ -37,15 +45,15 @@ class WellSelectionInput extends React.Component<Props, State> {
readOnly
name={this.props.name}
value={this.props.primaryWellCount ? String(this.props.primaryWellCount) : null}
onClick={this.toggleModal}
onClick={this.handleOpen}
error={this.props.errorToShow} />
<Portal>
<WellSelectionModal
key={modalKey}
pipetteId={this.props.pipetteId}
labwareId={this.props.labwareId}
isOpen={this.state.isModalOpen}
onCloseClick={this.toggleModal}
onCloseClick={this.handleClose}
name={this.props.name} />
</Portal>
</FormGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {connect} from 'react-redux'
import {selectors as pipetteSelectors} from '../../../pipettes'
import {selectors as steplistSelectors} from '../../../steplist'
import {getFieldErrors, type StepFieldName} from '../../../steplist/fieldLevel'
import {openWellSelectionModal} from '../../../well-selection/actions'
import type {BaseState, ThunkDispatch} from '../../../types'
import {showFieldErrors} from '../StepFormField'
import type {FocusHandlers} from '../index'
Expand All @@ -16,7 +15,11 @@ type OP = {
name: StepFieldName,
pipetteFieldName: StepFieldName,
labwareFieldName: StepFieldName,
} & FocusHandlers
onFieldBlur: $PropertyType<FocusHandlers, 'onFieldBlur'>,
onFieldFocus: $PropertyType<FocusHandlers, 'onFieldFocus'>,
focusedField: $PropertyType<FocusHandlers, 'focusedField'>,
dirtyFields: $PropertyType<FocusHandlers, 'dirtyFields'>,
}

type SP = {
isMulti: $PropertyType<Props, 'isMulti'>,
Expand Down Expand Up @@ -47,27 +50,11 @@ function mergeProps (
dispatchProps: {dispatch: ThunkDispatch<*>},
ownProps: OP
): Props {
const {dispatch} = dispatchProps
const {_pipetteId, _selectedLabwareId, _wellFieldErrors} = stateProps
const disabled = !(_pipetteId && _selectedLabwareId)
const {name, focusedField, dirtyFields} = ownProps
const {name, focusedField, dirtyFields, onFieldBlur, onFieldFocus} = ownProps
const showErrors = showFieldErrors({name, focusedField, dirtyFields})

const onClick = () => {
if (ownProps.onFieldBlur) {
ownProps.onFieldBlur(ownProps.name)
}
if (_pipetteId && _selectedLabwareId) {
dispatch(
openWellSelectionModal({
pipetteId: _pipetteId,
labwareId: _selectedLabwareId,
formFieldAccessor: ownProps.name,
})
)
}
}

return {
name,
disabled,
Expand All @@ -76,7 +63,8 @@ function mergeProps (
isMulti: stateProps.isMulti,
primaryWellCount: stateProps.primaryWellCount,
errorToShow: showErrors ? _wellFieldErrors[0] : null,
onClick,
onFieldBlur,
onFieldFocus,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,15 @@ export const LabwareDropdown = connect(LabwareDropdownSTP)((props: LabwareDropdo
name={name}
focusedField={focusedField}
dirtyFields={dirtyFields}
render={({value, updateValue}) => {
render={({value, updateValue, errorToShow}) => {
// blank out the dropdown if labware id does not exist
const availableLabwareIds = labwareOptions.map(opt => opt.value)
const fieldValue = availableLabwareIds.includes(value)
? String(value)
: null
return (
<DropdownField
error={errorToShow}
className={className}
options={labwareOptions}
onBlur={() => { onFieldBlur(name) }}
Expand Down
5 changes: 3 additions & 2 deletions protocol-designer/src/containers/ConnectedStepItem.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow
import * as React from 'react'
import {connect} from 'react-redux'
import isEmpty from 'lodash/isEmpty'
import type {BaseState, ThunkDispatch} from '../types'

import type {SubstepIdentifier} from '../steplist/types'
Expand Down Expand Up @@ -40,8 +41,8 @@ function mapStateToProps (state: BaseState, ownProps: OP): SP {
const hoveredStep = steplistSelectors.getHoveredStepId(state)
const selected = steplistSelectors.getSelectedStepId(state) === stepId
const collapsed = steplistSelectors.getCollapsedSteps(state)[stepId]

const hasError = fileDataSelectors.getErrorStepId(state) === stepId
const formAndFieldErrors = steplistSelectors.getFormAndFieldErrorsByStepId(state)[stepId]
const hasError = fileDataSelectors.getErrorStepId(state) === stepId || !isEmpty(formAndFieldErrors)
const warnings = (typeof stepId === 'number') // TODO: Ian 2018-07-13 remove when stepId always number
? dismissSelectors.getTimelineWarningsPerStep(state)[stepId]
: []
Expand Down
48 changes: 24 additions & 24 deletions protocol-designer/src/file-data/selectors/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,47 +116,47 @@ export const getInitialRobotState: BaseState => StepGeneration.RobotState = crea
}
}
)
function compoundCommandCreatorFromFormData (validatedForm: StepGeneration.CommandCreatorData): ?StepGeneration.CompoundCommandCreator {
switch (validatedForm.stepType) {
function compoundCommandCreatorFromStepArgs (stepArgs: StepGeneration.CommandCreatorData): ?StepGeneration.CompoundCommandCreator {
switch (stepArgs.stepType) {
case 'consolidate':
return StepGeneration.consolidate(validatedForm)
return StepGeneration.consolidate(stepArgs)
case 'transfer':
return StepGeneration.transfer(validatedForm)
return StepGeneration.transfer(stepArgs)
case 'distribute':
return StepGeneration.distribute(validatedForm)
return StepGeneration.distribute(stepArgs)
case 'mix':
return StepGeneration.mix(validatedForm)
return StepGeneration.mix(stepArgs)
default:
return null
}
}

// exposes errors and last valid robotState
export const robotStateTimeline: Selector<StepGeneration.Timeline> = createSelector(
steplistSelectors.validatedForms,
steplistSelectors.getArgsAndErrorsByStepId,
steplistSelectors.orderedSteps,
getInitialRobotState,
(forms, orderedSteps, initialRobotState) => {
const allFormData: Array<StepGeneration.CommandCreatorData | null> = orderedSteps.map(stepId => {
return (forms[stepId] && forms[stepId].validatedForm) || null
(allStepArgsAndErrors, orderedSteps, initialRobotState) => {
const allStepArgs: Array<StepGeneration.CommandCreatorData | null> = orderedSteps.map(stepId => {
return (allStepArgsAndErrors[stepId] && allStepArgsAndErrors[stepId].stepArgs) || null
})

// TODO: Ian 2018-06-14 `takeWhile` isn't inferring the right type
// $FlowFixMe
const continuousValidForms: Array<StepGeneration.CommandCreatorData> = takeWhile(
allFormData,
f => f
const continuousStepArgs: Array<StepGeneration.CommandCreatorData> = takeWhile(
allStepArgs,
stepArgs => stepArgs
)

const commandCreators = continuousValidForms.reduce(
(acc: Array<StepGeneration.CommandCreator>, formData, formIndex) => {
const {stepType} = formData
const commandCreators = continuousStepArgs.reduce(
(acc: Array<StepGeneration.CommandCreator>, stepArgs, stepIndex) => {
const {stepType} = stepArgs
let reducedCommandCreator = null

if (formData.stepType === 'pause') {
reducedCommandCreator = StepGeneration.delay(formData)
if (stepArgs.stepType === 'pause') {
reducedCommandCreator = StepGeneration.delay(stepArgs)
} else { // NOTE: compound return an array of command creators, atomic steps only return one command creator
const compoundCommandCreator: ?StepGeneration.CompoundCommandCreator = compoundCommandCreatorFromFormData(formData)
const compoundCommandCreator: ?StepGeneration.CompoundCommandCreator = compoundCommandCreatorFromStepArgs(stepArgs)
reducedCommandCreator = compoundCommandCreator && StepGeneration.reduceCommandCreators(compoundCommandCreator(initialRobotState))
}
if (!reducedCommandCreator) {
Expand All @@ -168,13 +168,13 @@ export const robotStateTimeline: Selector<StepGeneration.Timeline> = createSelec
// Drop tips eagerly, per pipette
// NOTE: this assumes all step forms that use a pipette have both
// 'pipette' and 'changeTip' fields (and they're not named something else).
const pipetteId = formData.pipette
const pipetteId = stepArgs.pipette
if (pipetteId) {
const nextFormForPipette = continuousValidForms
.slice(formIndex + 1)
.find(form => form.pipette === pipetteId)
const nextStepArgsForPipette = continuousStepArgs
.slice(stepIndex + 1)
.find(stepArgs => stepArgs.pipette === pipetteId)

const willReuseTip = nextFormForPipette && nextFormForPipette.changeTip === 'never'
const willReuseTip = nextStepArgsForPipette && nextStepArgsForPipette.changeTip === 'never'
if (!willReuseTip) {
return [
...acc,
Expand Down
10 changes: 6 additions & 4 deletions protocol-designer/src/labware-ingred/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type {
} from '../types'
import * as actions from '../actions'
import {getPDMetadata} from '../../file-types'
import type {BaseState, Selector, Options} from '../../types'
import type {BaseState, Options} from '../../types'
import type {LoadFileAction} from '../../load-file'
import type {
RemoveWellsContents,
Expand Down Expand Up @@ -294,8 +294,10 @@ const rootReducer = combineReducers({
renameLabwareFormMode,
})

type RootSlice = {labwareIngred: RootState}
type Selector<T> = (RootSlice) => T
// SELECTORS
const rootSelector = (state: BaseState): RootState => state.labwareIngred
const rootSelector = (state: RootSlice): RootState => state.labwareIngred

const getLabware: Selector<{[labwareId: string]: ?Labware}> = createSelector(
rootSelector,
Expand All @@ -318,8 +320,8 @@ const getLabwareTypes: Selector<LabwareTypeById> = createSelector(
)
)

const getLiquidGroupsById = (state: BaseState) => rootSelector(state).ingredients
const getIngredientLocations = (state: BaseState) => rootSelector(state).ingredLocations
const getLiquidGroupsById = (state: RootSlice) => rootSelector(state).ingredients
const getIngredientLocations = (state: RootSlice) => rootSelector(state).ingredLocations

const getNextLiquidGroupId: Selector<string> = createSelector(
getLiquidGroupsById,
Expand Down
Loading