Skip to content

Commit

Permalink
refactor(protocol-designer): gate timeline generation with form and f…
Browse files Browse the repository at this point in the history
…ield validation (#2574)

Consolidate step error logic to formLevel and fieldLevel error getters. Remove error block from
stepFormToArgs functions. Clean up hydration pathways.
  • Loading branch information
b-cooper authored Nov 2, 2018
1 parent 2d97353 commit 63418f3
Show file tree
Hide file tree
Showing 25 changed files with 551 additions and 483 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@import '@opentrons/components';

:root {
--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
3 changes: 2 additions & 1 deletion protocol-designer/src/components/StepEditForm/formFields.js
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({
ingredLocations,
})

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

0 comments on commit 63418f3

Please sign in to comment.