From 63418f3ec8d5443828b1fe1685da83d014e97ffa Mon Sep 17 00:00:00 2001 From: Brian Arthur Cooper Date: Fri, 2 Nov 2018 10:52:14 -0400 Subject: [PATCH] refactor(protocol-designer): gate timeline generation with form and field validation (#2574) Consolidate step error logic to formLevel and fieldLevel error getters. Remove error block from stepFormToArgs functions. Clean up hydration pathways. --- .../components/SettingsPage/SettingsPage.css | 4 + .../WellSelectionInput/WellSelectionInput.js | 18 +- .../StepEditForm/WellSelectionInput/index.js | 28 +-- .../src/components/StepEditForm/formFields.js | 3 +- .../src/containers/ConnectedStepItem.js | 5 +- .../src/file-data/selectors/commands.js | 48 ++-- .../src/labware-ingred/reducers/index.js | 10 +- protocol-designer/src/pipettes/reducers.js | 112 +++++---- protocol-designer/src/pipettes/selectors.js | 11 +- .../src/steplist/fieldLevel/errors.js | 7 +- .../src/steplist/fieldLevel/index.js | 26 ++- .../src/steplist/formLevel/errors.js | 31 ++- .../formLevel/getDefaultsForStepType.js | 18 ++ .../src/steplist/formLevel/index.js | 3 +- .../formLevel/stepFormToArgs/index.js | 23 +- .../formLevel/stepFormToArgs/mixFormToArgs.js | 109 +++------ .../stepFormToArgs/pauseFormToArgs.js | 44 +--- .../stepFormToArgs/transferLikeFormToArgs.js | 220 +++++++----------- .../formLevel/stepFormToArgs/types.js | 7 - .../src/steplist/generateSubsteps.js | 60 +++-- protocol-designer/src/steplist/selectors.js | 203 +++++++++++----- protocol-designer/src/steplist/types.js | 20 +- .../src/top-selectors/substep-highlight.js | 14 +- .../src/top-selectors/substeps.js | 9 +- .../src/well-selection/reducers.js | 1 + 25 files changed, 551 insertions(+), 483 deletions(-) delete mode 100644 protocol-designer/src/steplist/formLevel/stepFormToArgs/types.js diff --git a/protocol-designer/src/components/SettingsPage/SettingsPage.css b/protocol-designer/src/components/SettingsPage/SettingsPage.css index 86e027a4f76..7a0c73f20f0 100644 --- a/protocol-designer/src/components/SettingsPage/SettingsPage.css +++ b/protocol-designer/src/components/SettingsPage/SettingsPage.css @@ -1,5 +1,9 @@ @import '@opentrons/components'; +:root { + --mw-labeled-toggle: 25rem; +} + .sidebar_item { background-color: white; margin: 0.4rem 0.125rem; diff --git a/protocol-designer/src/components/StepEditForm/WellSelectionInput/WellSelectionInput.js b/protocol-designer/src/components/StepEditForm/WellSelectionInput/WellSelectionInput.js index cec74553177..3dad606c7a3 100644 --- a/protocol-designer/src/components/StepEditForm/WellSelectionInput/WellSelectionInput.js +++ b/protocol-designer/src/components/StepEditForm/WellSelectionInput/WellSelectionInput.js @@ -5,16 +5,18 @@ 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, + onFieldFocus: $PropertyType, } type State = {isModalOpen: boolean} @@ -22,8 +24,14 @@ type State = {isModalOpen: boolean} class WellSelectionInput extends React.Component { 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 () { @@ -37,7 +45,7 @@ class WellSelectionInput extends React.Component { readOnly name={this.props.name} value={this.props.primaryWellCount ? String(this.props.primaryWellCount) : null} - onClick={this.toggleModal} + onClick={this.handleOpen} error={this.props.errorToShow} /> { pipetteId={this.props.pipetteId} labwareId={this.props.labwareId} isOpen={this.state.isModalOpen} - onCloseClick={this.toggleModal} + onCloseClick={this.handleClose} name={this.props.name} /> diff --git a/protocol-designer/src/components/StepEditForm/WellSelectionInput/index.js b/protocol-designer/src/components/StepEditForm/WellSelectionInput/index.js index aadef47c69c..977c218a826 100644 --- a/protocol-designer/src/components/StepEditForm/WellSelectionInput/index.js +++ b/protocol-designer/src/components/StepEditForm/WellSelectionInput/index.js @@ -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' @@ -16,7 +15,11 @@ type OP = { name: StepFieldName, pipetteFieldName: StepFieldName, labwareFieldName: StepFieldName, -} & FocusHandlers + onFieldBlur: $PropertyType, + onFieldFocus: $PropertyType, + focusedField: $PropertyType, + dirtyFields: $PropertyType, +} type SP = { isMulti: $PropertyType, @@ -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, @@ -76,7 +63,8 @@ function mergeProps ( isMulti: stateProps.isMulti, primaryWellCount: stateProps.primaryWellCount, errorToShow: showErrors ? _wellFieldErrors[0] : null, - onClick, + onFieldBlur, + onFieldFocus, } } diff --git a/protocol-designer/src/components/StepEditForm/formFields.js b/protocol-designer/src/components/StepEditForm/formFields.js index 4d6ecf25678..ae473a63dbe 100644 --- a/protocol-designer/src/components/StepEditForm/formFields.js +++ b/protocol-designer/src/components/StepEditForm/formFields.js @@ -191,7 +191,7 @@ 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) @@ -199,6 +199,7 @@ export const LabwareDropdown = connect(LabwareDropdownSTP)((props: LabwareDropdo : null return ( { onFieldBlur(name) }} diff --git a/protocol-designer/src/containers/ConnectedStepItem.js b/protocol-designer/src/containers/ConnectedStepItem.js index 4744df22367..b0332967704 100644 --- a/protocol-designer/src/containers/ConnectedStepItem.js +++ b/protocol-designer/src/containers/ConnectedStepItem.js @@ -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' @@ -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] : [] diff --git a/protocol-designer/src/file-data/selectors/commands.js b/protocol-designer/src/file-data/selectors/commands.js index d26530e28bf..275e792b699 100644 --- a/protocol-designer/src/file-data/selectors/commands.js +++ b/protocol-designer/src/file-data/selectors/commands.js @@ -116,16 +116,16 @@ 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 } @@ -133,30 +133,30 @@ function compoundCommandCreatorFromFormData (validatedForm: StepGeneration.Comma // exposes errors and last valid robotState export const robotStateTimeline: Selector = createSelector( - steplistSelectors.validatedForms, + steplistSelectors.getArgsAndErrorsByStepId, steplistSelectors.orderedSteps, getInitialRobotState, - (forms, orderedSteps, initialRobotState) => { - const allFormData: Array = orderedSteps.map(stepId => { - return (forms[stepId] && forms[stepId].validatedForm) || null + (allStepArgsAndErrors, orderedSteps, initialRobotState) => { + const allStepArgs: Array = 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 = takeWhile( - allFormData, - f => f + const continuousStepArgs: Array = takeWhile( + allStepArgs, + stepArgs => stepArgs ) - const commandCreators = continuousValidForms.reduce( - (acc: Array, formData, formIndex) => { - const {stepType} = formData + const commandCreators = continuousStepArgs.reduce( + (acc: Array, 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) { @@ -168,13 +168,13 @@ export const robotStateTimeline: Selector = 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, diff --git a/protocol-designer/src/labware-ingred/reducers/index.js b/protocol-designer/src/labware-ingred/reducers/index.js index 21fec641fd7..552726e4a99 100644 --- a/protocol-designer/src/labware-ingred/reducers/index.js +++ b/protocol-designer/src/labware-ingred/reducers/index.js @@ -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, @@ -294,8 +294,10 @@ const rootReducer = combineReducers({ ingredLocations, }) +type RootSlice = {labwareIngred: RootState} +type Selector = (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, @@ -318,8 +320,8 @@ const getLabwareTypes: Selector = 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 = createSelector( getLiquidGroupsById, diff --git a/protocol-designer/src/pipettes/reducers.js b/protocol-designer/src/pipettes/reducers.js index 5922a711fec..4151a1fec93 100644 --- a/protocol-designer/src/pipettes/reducers.js +++ b/protocol-designer/src/pipettes/reducers.js @@ -42,42 +42,30 @@ function createPipette ( } } -export type PipetteReducerState = { - byMount: {| - left: ?string, - right: ?string, - |}, - byId: { - [pipetteId: string]: PipetteData, - }, -} +export type PipetteIdByMount = {| + left: ?string, + right: ?string, +|} -const pipettes = handleActions({ - LOAD_FILE: (state: PipetteReducerState, action: LoadFileAction): PipetteReducerState => { +export type PipetteById = {[pipetteId: string]: PipetteData} + +const byId = handleActions({ + LOAD_FILE: (state: PipetteById, action: LoadFileAction): PipetteById => { const file = action.payload const {pipettes} = file // TODO: Ian 2018-06-29 create fns to access ProtocolFile data const {pipetteTiprackAssignments} = file['designer-application'].data - const pipetteIds = Object.keys(pipettes) - return { - byMount: { - left: pipetteIds.find(id => pipettes[id].mount === 'left'), - right: pipetteIds.find(id => pipettes[id].mount === 'right'), - }, - byId: reduce( - pipettes, - (acc: {[pipetteId: string]: PipetteData}, p: FilePipette, id: string) => { - const newPipette = createPipette(p.mount, p.model, pipetteTiprackAssignments[id], id) - return newPipette - ? {...acc, [id]: newPipette} - : acc - }, {}), - } + return reduce(pipettes, (acc: {[pipetteId: string]: PipetteData}, p: FilePipette, id: string) => { + const newPipette = createPipette(p.mount, p.model, pipetteTiprackAssignments[id], id) + return newPipette + ? {...acc, [id]: newPipette} + : acc + }, {}) }, CREATE_NEW_PROTOCOL: ( - state: PipetteReducerState, + state: PipetteById, action: {payload: NewProtocolFields} - ): PipetteReducerState => { + ): PipetteById => { const {left, right} = action.payload const leftPipette = (left.pipetteModel && left.tiprackModel) @@ -88,7 +76,7 @@ const pipettes = handleActions({ ? createPipette('right', right.pipetteModel, right.tiprackModel) : null - const newPipettes = ([leftPipette, rightPipette]).reduce( + const newPipettes: PipetteById = ([leftPipette, rightPipette]).reduce( (acc: {[string]: PipetteData}, pipette: ?PipetteData) => { if (!pipette) return acc return { @@ -98,41 +86,65 @@ const pipettes = handleActions({ }, {}) return { - byMount: { - left: leftPipette ? leftPipette.id : state.byMount.left, - right: rightPipette ? rightPipette.id : state.byMount.right, - }, - byId: { - ...state.byId, - ...newPipettes, - }, + ...state, + ...newPipettes, } }, SWAP_PIPETTES: ( - state: PipetteReducerState, + state: PipetteById, action: {payload: NewProtocolFields} - ): PipetteReducerState => { - const byId = mapValues(state.byId, (pipette: PipetteData): PipetteData => ({ + ): PipetteById => { + return mapValues(state, (pipette: PipetteData): PipetteData => ({ ...pipette, mount: (pipette.mount === 'left') ? 'right' : 'left', })) + }, +}, {}) + +const byMount = handleActions({ + LOAD_FILE: (state: PipetteIdByMount, action: LoadFileAction): PipetteIdByMount => { + const file = action.payload + const {pipettes} = file + // TODO: Ian 2018-06-29 create fns to access ProtocolFile data + const pipetteIds = Object.keys(pipettes) + return { + left: pipetteIds.find(id => pipettes[id].mount === 'left'), + right: pipetteIds.find(id => pipettes[id].mount === 'right'), + } + }, + CREATE_NEW_PROTOCOL: ( + state: PipetteIdByMount, + action: {payload: NewProtocolFields} + ): PipetteIdByMount => { + const {left, right} = action.payload + + const leftPipette = (left.pipetteModel && left.tiprackModel) + ? createPipette('left', left.pipetteModel, left.tiprackModel) + : null - return ({ - byMount: { - left: state.byMount.right, - right: state.byMount.left, - }, - byId, - }) + const rightPipette = (right.pipetteModel && right.tiprackModel) + ? createPipette('right', right.pipetteModel, right.tiprackModel) + : null + + return { + left: leftPipette ? leftPipette.id : state.left, + right: rightPipette ? rightPipette.id : state.right, + } }, -}, {byMount: {left: null, right: null}, byId: {}}) + SWAP_PIPETTES: (state: PipetteIdByMount, action: {payload: NewProtocolFields}): PipetteIdByMount => ({ + left: state.right, + right: state.left, + }), +}, {left: null, right: null}) const _allReducers = { - pipettes, + byMount, + byId, } export type RootState = { - pipettes: PipetteReducerState, + byId: PipetteById, + byMount: PipetteIdByMount, } export const rootReducer = combineReducers(_allReducers) diff --git a/protocol-designer/src/pipettes/selectors.js b/protocol-designer/src/pipettes/selectors.js index 855efb51332..f505de9cb9b 100644 --- a/protocol-designer/src/pipettes/selectors.js +++ b/protocol-designer/src/pipettes/selectors.js @@ -4,15 +4,18 @@ import reduce from 'lodash/reduce' import get from 'lodash/get' import {getPipetteModels, getPipette, getLabware} from '@opentrons/shared-data' -import type {BaseState, Selector} from '../types' import type {DropdownOption} from '@opentrons/components' import type {PipetteData} from '../step-generation' +import type {RootState} from './reducers' type PipettesById = {[pipetteId: string]: PipetteData} +type RootSlice = {pipettes: RootState} -const rootSelector = (state: BaseState) => state.pipettes.pipettes +type Selector = (RootSlice) => T -export const pipettesById = createSelector( +export const rootSelector = (state: {pipettes: RootState}) => state.pipettes + +export const pipettesById: Selector = createSelector( rootSelector, pipettes => pipettes.byId ) @@ -54,7 +57,7 @@ export const equippedPipetteOptions: Selector> = createSel }, ] : acc, - []) + []) } ) diff --git a/protocol-designer/src/steplist/fieldLevel/errors.js b/protocol-designer/src/steplist/fieldLevel/errors.js index 58a3d7ad8b9..db75bcb26b9 100644 --- a/protocol-designer/src/steplist/fieldLevel/errors.js +++ b/protocol-designer/src/steplist/fieldLevel/errors.js @@ -7,11 +7,15 @@ import isArray from 'lodash/isArray' // TODO: reconcile difference between returning error string and key -export type FieldError = 'REQUIRED' | 'UNDER_WELL_MINIMUM' // TODO: add other possible field errors +export type FieldError = + | 'REQUIRED' + | 'UNDER_WELL_MINIMUM' + | 'NON_ZERO' const FIELD_ERRORS: {[FieldError]: string} = { REQUIRED: 'This field is required', UNDER_WELL_MINIMUM: 'or more wells are required', + NON_ZERO: 'Must be greater than zero', } // TODO: test these @@ -21,6 +25,7 @@ const FIELD_ERRORS: {[FieldError]: string} = { type errorChecker = (value: mixed) => ?string export const requiredField = (value: mixed): ?string => !value ? FIELD_ERRORS.REQUIRED : null +export const nonZero = (value: mixed) => (value && Number(value) === 0) ? FIELD_ERRORS.NON_ZERO : null export const minimumWellCount = (minimum: number): errorChecker => (wells: mixed): ?string => ( (isArray(wells) && (wells.length < minimum)) ? `${minimum} ${FIELD_ERRORS.UNDER_WELL_MINIMUM}` : null ) diff --git a/protocol-designer/src/steplist/fieldLevel/index.js b/protocol-designer/src/steplist/fieldLevel/index.js index c494bd0c65f..071193a2252 100644 --- a/protocol-designer/src/steplist/fieldLevel/index.js +++ b/protocol-designer/src/steplist/fieldLevel/index.js @@ -4,6 +4,7 @@ import {selectors as pipetteSelectors} from '../../pipettes' import { requiredField, minimumWellCount, + nonZero, composeErrors, } from './errors' import { @@ -16,18 +17,23 @@ import { type ValueProcessor, } from './processing' import type {StepFieldName} from './types' -import type {BaseState} from '../../types' +import type {StepFormContextualState} from '../types' export type { StepFieldName, } -const hydrateLabware = (state, id) => (labwareIngredSelectors.getLabware(state)[id]) +const hydrateLabware = (state: StepFormContextualState, id: string) => ( + labwareIngredSelectors.getLabware(state)[id] +) +const hydratePipette = (state: StepFormContextualState, id: string) => ( + pipetteSelectors.pipettesById(state)[id] +) type StepFieldHelpers = { getErrors?: (mixed) => Array, processValue?: ValueProcessor, - hydrate?: (state: BaseState, id: string) => mixed, + hydrate?: (state: StepFormContextualState, id: string) => mixed, } const stepFieldHelperMap: {[StepFieldName]: StepFieldHelpers} = { 'aspirate_airGap_volume': { processValue: composeProcessors(castToFloat, onlyPositiveNumbers) }, @@ -36,6 +42,10 @@ const stepFieldHelperMap: {[StepFieldName]: StepFieldHelpers} = { hydrate: hydrateLabware, }, 'aspirate_mix_volume': { processValue: composeProcessors(castToFloat, onlyPositiveNumbers) }, + 'aspirate_wells': { + getErrors: composeErrors(requiredField, minimumWellCount(1)), + processValue: defaultTo([]), + }, 'dispense_delayMinutes': { processValue: composeProcessors(castToNumber, defaultTo(0)), }, @@ -48,7 +58,7 @@ const stepFieldHelperMap: {[StepFieldName]: StepFieldHelpers} = { }, 'dispense_mix_volume': { processValue: composeProcessors(castToFloat, onlyPositiveNumbers) }, 'dispense_wells': { - getErrors: composeErrors(minimumWellCount(1)), + getErrors: composeErrors(requiredField, minimumWellCount(1)), processValue: defaultTo([]), }, 'aspirate_disposalVol_volume': { @@ -69,18 +79,18 @@ const stepFieldHelperMap: {[StepFieldName]: StepFieldHelpers} = { }, 'pipette': { getErrors: composeErrors(requiredField), - hydrate: (state, id) => pipetteSelectors.pipettesById(state)[id], + hydrate: hydratePipette, }, 'times': { getErrors: composeErrors(requiredField), processValue: composeProcessors(castToNumber, onlyPositiveNumbers, onlyIntegers, defaultTo(0)), }, 'volume': { - getErrors: composeErrors(requiredField), + getErrors: composeErrors(requiredField, nonZero), processValue: composeProcessors(castToFloat, onlyPositiveNumbers, defaultTo(0)), }, 'wells': { - getErrors: composeErrors(minimumWellCount(1)), + getErrors: composeErrors(requiredField, minimumWellCount(1)), processValue: defaultTo([]), }, } @@ -96,7 +106,7 @@ export const processField = (name: StepFieldName, value: mixed): ?mixed => { return fieldProcessor ? fieldProcessor(value) : value } -export const hydrateField = (state: BaseState, name: StepFieldName, value: string): ?mixed => { +export const hydrateField = (state: StepFormContextualState, name: StepFieldName, value: string): ?mixed => { const hydrator = stepFieldHelperMap[name] && stepFieldHelperMap[name].hydrate return hydrator ? hydrator(state, value) : value } diff --git a/protocol-designer/src/steplist/formLevel/errors.js b/protocol-designer/src/steplist/formLevel/errors.js index 0fbc15bed3c..85f77937a41 100644 --- a/protocol-designer/src/steplist/formLevel/errors.js +++ b/protocol-designer/src/steplist/formLevel/errors.js @@ -6,12 +6,15 @@ import type {StepFieldName} from '../fieldLevel' /******************* ** Error Messages ** ********************/ -export type FormErrorKey = 'INCOMPATIBLE_ASPIRATE_LABWARE' +export type FormErrorKey = + | 'INCOMPATIBLE_ASPIRATE_LABWARE' | 'INCOMPATIBLE_DISPENSE_LABWARE' | 'INCOMPATIBLE_LABWARE' | 'WELL_RATIO_TRANSFER' | 'WELL_RATIO_CONSOLIDATE' | 'WELL_RATIO_DISTRIBUTE' + | 'PAUSE_TYPE_REQUIRED' + | 'TIME_PARAM_REQUIRED' export type FormError = { title: string, @@ -32,6 +35,14 @@ const FORM_ERRORS: {[FormErrorKey]: FormError} = { title: 'Selected labware may be incompatible with selected pipette', dependentFields: ['labware', 'pipette'], }, + PAUSE_TYPE_REQUIRED: { + title: 'Must either pause for amount of time, or until told to resume', + dependentFields: ['pauseForAmountOfTime'], + }, + TIME_PARAM_REQUIRED: { + title: 'Must include hours, minutes, or seconds', + dependentFields: ['pauseForAmountOfTime'], + }, WELL_RATIO_TRANSFER: { title: 'In transfer actions the number of source and destination wells must match', body: 'You may want to use a Distribute or Consolidate instead of Transfer', @@ -74,6 +85,24 @@ export const incompatibleAspirateLabware = (fields: HydratedFormData): ?FormErro return (!canPipetteUseLabware(pipette.model, aspirate_labware.type)) ? FORM_ERRORS.INCOMPATIBLE_ASPIRATE_LABWARE : null } +export const pauseForTimeOrUntilTold = (fields: HydratedFormData): ?FormError => { + const {pauseForAmountOfTime, pauseHour, pauseMinute, pauseSecond} = fields + if (pauseForAmountOfTime === 'true') { + // user selected pause for amount of time + const hours = parseFloat(pauseHour) || 0 + const minutes = parseFloat(pauseMinute) || 0 + const seconds = parseFloat(pauseSecond) || 0 + const totalSeconds = hours * 3600 + minutes * 60 + seconds + return totalSeconds <= 0 ? FORM_ERRORS.TIME_PARAM_REQUIRED : null + } else if (pauseForAmountOfTime === 'false') { + // user selected pause until resume + return null + } else { + // user selected neither pause until resume nor pause for amount of time + return FORM_ERRORS.PAUSE_TYPE_REQUIRED + } +} + export const wellRatioTransfer = (fields: HydratedFormData): ?FormError => { const {aspirate_wells, dispense_wells} = fields if (!aspirate_wells || !dispense_wells) return null diff --git a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.js b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.js index 7872b26a859..7c89e25f802 100644 --- a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.js +++ b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.js @@ -14,37 +14,55 @@ export default function getDefaultsForStepType (stepType: StepType) { case 'transfer': return { 'aspirate_changeTip': DEFAULT_CHANGE_TIP_OPTION, + 'aspirate_labware': null, 'aspirate_wellOrder_first': DEFAULT_WELL_ORDER_FIRST_OPTION, 'aspirate_wellOrder_second': DEFAULT_WELL_ORDER_SECOND_OPTION, 'aspirate_mmFromBottom': DEFAULT_MM_FROM_BOTTOM_ASPIRATE, + 'aspirate_wells': [], + 'dispense_labware': null, 'dispense_wellOrder_first': DEFAULT_WELL_ORDER_FIRST_OPTION, 'dispense_wellOrder_second': DEFAULT_WELL_ORDER_SECOND_OPTION, 'dispense_mmFromBottom': DEFAULT_MM_FROM_BOTTOM_DISPENSE, + 'dispense_wells': [], + 'volume': undefined, } case 'consolidate': return { 'aspirate_changeTip': DEFAULT_CHANGE_TIP_OPTION, + 'aspirate_labware': null, 'aspirate_mmFromBottom': DEFAULT_MM_FROM_BOTTOM_ASPIRATE, 'aspirate_wellOrder_first': DEFAULT_WELL_ORDER_FIRST_OPTION, 'aspirate_wellOrder_second': DEFAULT_WELL_ORDER_SECOND_OPTION, + 'aspirate_wells': [], + 'dispense_labware': null, 'dispense_mmFromBottom': DEFAULT_MM_FROM_BOTTOM_DISPENSE, + 'dispense_wells': [], + 'volume': undefined, } case 'mix': return { 'aspirate_changeTip': DEFAULT_CHANGE_TIP_OPTION, + 'labware': null, 'aspirate_wellOrder_first': DEFAULT_WELL_ORDER_FIRST_OPTION, 'aspirate_wellOrder_second': DEFAULT_WELL_ORDER_SECOND_OPTION, + 'wells': [], 'mmFromBottom': DEFAULT_MM_FROM_BOTTOM_DISPENSE, // NOTE: mix uses dispense for both asp + disp, for now + 'volume': undefined, } case 'distribute': return { 'aspirate_changeTip': DEFAULT_CHANGE_TIP_OPTION, 'aspirate_disposalVol_checkbox': true, 'aspirate_disposalVol_destination': FIXED_TRASH_ID, + 'aspirate_labware': null, 'aspirate_mmFromBottom': DEFAULT_MM_FROM_BOTTOM_ASPIRATE, + 'aspirate_wells': [], + 'dispense_labware': null, 'dispense_wellOrder_first': DEFAULT_WELL_ORDER_FIRST_OPTION, 'dispense_wellOrder_second': DEFAULT_WELL_ORDER_SECOND_OPTION, 'dispense_mmFromBottom': DEFAULT_MM_FROM_BOTTOM_DISPENSE, + 'dispense_wells': [], + 'volume': undefined, } default: return {} diff --git a/protocol-designer/src/steplist/formLevel/index.js b/protocol-designer/src/steplist/formLevel/index.js index c0fddc56955..c03ad1c7971 100644 --- a/protocol-designer/src/steplist/formLevel/index.js +++ b/protocol-designer/src/steplist/formLevel/index.js @@ -4,6 +4,7 @@ import { incompatibleAspirateLabware, incompatibleDispenseLabware, incompatibleLabware, + pauseForTimeOrUntilTold, wellRatioTransfer, wellRatioConsolidate, wellRatioDistribute, @@ -26,7 +27,7 @@ export {default as stepFormToArgs} from './stepFormToArgs' type FormHelpers = {getErrors?: (mixed) => Array, getWarnings?: (mixed) => Array} const stepFormHelperMap: {[StepType]: FormHelpers} = { mix: {getErrors: composeErrors(incompatibleLabware)}, - pause: {getErrors: composeErrors(incompatibleLabware)}, + pause: {getErrors: composeErrors(pauseForTimeOrUntilTold)}, transfer: { getErrors: composeErrors(incompatibleAspirateLabware, incompatibleDispenseLabware, wellRatioTransfer), getWarnings: composeWarnings(maxDispenseWellVolume), diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/index.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/index.js index 6d86a56df28..2ce5e543f6a 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/index.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/index.js @@ -1,36 +1,29 @@ // @flow -import type { FormData } from '../../../form-types' -import type { CommandCreatorData } from '../../../step-generation' +import type {FormData} from '../../../form-types' +import type {CommandCreatorData} from '../../../step-generation' import mixFormToArgs from './mixFormToArgs' import pauseFormToArgs from './pauseFormToArgs' import transferLikeFormToArgs from './transferLikeFormToArgs' -import type { StepFormContext } from './types' - -export type ValidFormAndErrors = { - errors: {[string]: string}, - validatedForm: CommandCreatorData | null, // TODO: incompleteData field when this is null? -} // NOTE: this acts as an adapter for the PD defined data shape of the step forms // to create arguments that the step generation service is expecting // in order to generate command creators -const stepFormToArgs = (formData: FormData, context?: StepFormContext = {}): * => { // really returns ValidFormAndErrors +type StepArgs = CommandCreatorData | null + +const stepFormToArgs = (formData: FormData): StepArgs => { switch (formData.stepType) { case 'transfer': case 'consolidate': case 'distribute': - return transferLikeFormToArgs(formData, context) + return transferLikeFormToArgs(formData) case 'pause': return pauseFormToArgs(formData) case 'mix': - return mixFormToArgs(formData, context) + return mixFormToArgs(formData) default: - return { - errors: {_form: `Unsupported step type: ${formData.stepType}`}, - validatedForm: null, - } + return null } } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.js index bfa10b33412..7c453ab6afa 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.js @@ -5,95 +5,60 @@ import intersection from 'lodash/intersection' import type { FormData } from '../../../form-types' import type { MixFormData } from '../../../step-generation' import { DEFAULT_CHANGE_TIP_OPTION } from '../../../constants' -import type { StepFormContext } from './types' import { orderWells } from '../../utils' -type ValidationAndErrors = { - errors: {[string]: string}, - validatedForm: F | null, -} - -const mixFormToArgs = (formData: FormData, context: StepFormContext): ValidationAndErrors => { - const requiredFields = ['pipette', 'labware', 'volume', 'times'] - - let errors = {} +type MixStepArgs = MixFormData - requiredFields.forEach(field => { - if (formData[field] == null) { - errors[field] = 'This field is required' - } - }) +// TODO: BC 2018-10-30 move getting labwareDef into hydration layer upstream +const mixFormToArgs = (hydratedFormData: FormData): MixStepArgs => { + const {labware, pipette} = hydratedFormData + const touchTip = !!hydratedFormData['touchTip'] - const {labware, pipette} = formData - const touchTip = !!formData['touchTip'] + let wells = hydratedFormData.wells || [] + const orderFirst = hydratedFormData.aspirate_wellOrder_first + const orderSecond = hydratedFormData.aspirate_wellOrder_second - let wells = formData.wells || [] - const orderFirst = formData.aspirate_wellOrder_first - const orderSecond = formData.aspirate_wellOrder_second - if (context && context.labware && labware) { - const labwareById = context.labware - const labwareDef = labwareById[labware] && getLabware(labwareById[labware].type) - if (labwareDef) { - const allWellsOrdered = orderWells(labwareDef.ordering, orderFirst, orderSecond) - wells = intersection(allWellsOrdered, wells) - } else { - console.warn('the specified labware definition could not be located') - } + const labwareDef = labware && getLabware(labware.type) + if (labwareDef) { + const allWellsOrdered = orderWells(labwareDef.ordering, orderFirst, orderSecond) + wells = intersection(allWellsOrdered, wells) + } else { + console.warn('the specified labware definition could not be located') } - const volume = Number(formData.volume) || 0 - const times = Number(formData.times) || 0 + const volume = Number(hydratedFormData.volume) || 0 + const times = Number(hydratedFormData.times) || 0 // NOTE: for mix, there is only one tip offset field, // and it applies to both aspirate and dispense - const aspirateOffsetFromBottomMm = Number(formData['mmFromBottom']) - const dispenseOffsetFromBottomMm = Number(formData['mmFromBottom']) + const aspirateOffsetFromBottomMm = Number(hydratedFormData['mmFromBottom']) + const dispenseOffsetFromBottomMm = Number(hydratedFormData['mmFromBottom']) // It's radiobutton, so one should always be selected. - const changeTip = formData['aspirate_changeTip'] || DEFAULT_CHANGE_TIP_OPTION + const changeTip = hydratedFormData['aspirate_changeTip'] || DEFAULT_CHANGE_TIP_OPTION - const blowout = formData['dispense_blowout_labware'] + const blowout = hydratedFormData['dispense_blowout_labware'] - const delay = formData['dispense_delay_checkbox'] - ? ((Number(formData['dispense_delayMinutes']) || 0) * 60) + - (Number(formData['dispense_delaySeconds'] || 0)) + const delay = hydratedFormData['dispense_delay_checkbox'] + ? ((Number(hydratedFormData['dispense_delayMinutes']) || 0) * 60) + + (Number(hydratedFormData['dispense_delaySeconds'] || 0)) : null // TODO Ian 2018-05-08 delay number parsing errors - if (wells.length <= 0) { - errors.wells = '1 or more wells is required' - } - - if (volume <= 0) { - errors.volume = 'Volume must be a number greater than 0' - } - - if (times <= 0 || !Number.isInteger(times)) { - errors.times = 'Number of repetitions must be an integer greater than 0' - } - - // TODO: BC 2018-08-21 remove this old validation logic, currently unused - const isErrorFree = !(Object.values(errors).length > 0) - return { - errors, - validatedForm: isErrorFree && labware && pipette - ? { - stepType: 'mix', - name: `Mix ${formData.id}`, // TODO real name for steps - description: 'description would be here 2018-03-01', // TODO get from form - labware, - wells, - volume, - times, - touchTip, - delay, - changeTip, - blowout, - pipette, - aspirateOffsetFromBottomMm, - dispenseOffsetFromBottomMm, - } - : null, + stepType: 'mix', + name: `Mix ${hydratedFormData.id}`, // TODO real name for steps + description: 'description would be here 2018-03-01', // TODO get from form + labware: labware.id, + wells, + volume, + times, + touchTip, + delay, + changeTip, + blowout, + pipette: pipette.id, + aspirateOffsetFromBottomMm, + dispenseOffsetFromBottomMm, } } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.js index 76be9f4bf98..b7af4e8056c 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.js @@ -3,12 +3,9 @@ import type { FormData } from '../../../form-types' import type { PauseFormData } from '../../../step-generation' -type ValidationAndErrors = { - errors: {[string]: string}, - validatedForm: F | null, -} +type PauseStepArgs = PauseFormData -const pauseFormToArgs = (formData: FormData): ValidationAndErrors => { +const pauseFormToArgs = (formData: FormData): PauseStepArgs => { const hours = parseFloat(formData['pauseHour']) || 0 const minutes = parseFloat(formData['pauseMinute']) || 0 const seconds = parseFloat(formData['pauseSecond']) || 0 @@ -16,34 +13,17 @@ const pauseFormToArgs = (formData: FormData): ValidationAndErrors const message = formData['pauseMessage'] || '' - // TODO: BC 2018-08-21 remove this old validation logic once no longer preventing save - let errors = {} - if (!formData['pauseForAmountOfTime']) { - errors = {...errors, 'pauseForAmountOfTime': 'Pause for amount of time vs pause until user input is required'} - } - if (formData['pauseForAmountOfTime'] === 'true' && (totalSeconds <= 0)) { - errors = {...errors, '_pause-times': 'Must include hours, minutes, or seconds'} - } - const hasErrors = Object.values(errors).length > 0 - return { - errors, - validatedForm: hasErrors - ? null - : { - stepType: 'pause', - name: `Pause ${formData.id}`, // TODO real name for steps - description: 'description would be here 2018-03-01', // TODO get from form - wait: (formData['pauseForAmountOfTime'] === 'false') - ? true - : totalSeconds, - message, - meta: { - hours, - minutes, - seconds, - }, - }, + stepType: 'pause', + name: `Pause ${formData.id}`, // TODO real name for steps + description: 'description would be here 2018-03-01', // TODO get from form + wait: (formData['pauseForAmountOfTime'] === 'false') ? true : totalSeconds, + message, + meta: { + hours, + minutes, + seconds, + }, } } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js index 4e17f131675..9e7eb41e9b0 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js @@ -9,76 +9,67 @@ import type { TransferFormData, } from '../../../step-generation' import { DEFAULT_CHANGE_TIP_OPTION } from '../../../constants' -import type { StepFormContext } from './types' import { orderWells } from '../../utils' export const SOURCE_WELL_DISPOSAL_DESTINATION = 'source_well' -type ValidationAndErrors = { - errors: {[string]: string}, - validatedForm: F | null, -} - -function getMixData (formData, checkboxField, volumeField, timesField) { - // TODO Ian 2018-04-03 is error reporting necessary? Or are only valid inputs allowed in these fields? - const checkbox = formData[checkboxField] - const volume = parseFloat(formData[volumeField]) - const times = parseInt(formData[timesField]) +function getMixData (hydratedFormData, checkboxField, volumeField, timesField) { + const checkbox = hydratedFormData[checkboxField] + const volume = parseFloat(hydratedFormData[volumeField]) + const times = parseInt(hydratedFormData[timesField]) return (checkbox && volume > 0 && times > 0) ? {volume, times} : null } -type TransferLikeValidationAndErrors = - | ValidationAndErrors - | ValidationAndErrors - | ValidationAndErrors - -const transferLikeFormToArgs = (formData: FormData, context: StepFormContext): TransferLikeValidationAndErrors => { - const stepType = formData.stepType - const pipette = formData['pipette'] - const volume = Number(formData['volume']) - const sourceLabware = formData['aspirate_labware'] - const destLabware = formData['dispense_labware'] - const blowout = formData['dispense_blowout_checkbox'] ? formData['dispense_blowout_labware'] : null - - const aspirateOffsetFromBottomMm = Number(formData['aspirate_mmFromBottom']) - const dispenseOffsetFromBottomMm = Number(formData['dispense_mmFromBottom']) - - const delayAfterDispense = formData['dispense_delay_checkbox'] - ? ((Number(formData['dispense_delayMinutes']) || 0) * 60) + - (Number(formData['dispense_delaySeconds'] || 0)) +type TransferLikeStepArgs = ConsolidateFormData | DistributeFormData | TransferFormData | null + +// TODO: BC 2018-10-30 move getting labwareDef into hydration layer upstream +const transferLikeFormToArgs = (hydratedFormData: FormData): TransferLikeStepArgs => { + const stepType = hydratedFormData.stepType + const pipette = hydratedFormData['pipette'] + const volume = Number(hydratedFormData['volume']) + const sourceLabware = hydratedFormData['aspirate_labware'] + const destLabware = hydratedFormData['dispense_labware'] + const blowout = hydratedFormData['dispense_blowout_checkbox'] ? hydratedFormData['dispense_blowout_labware'] : null + + const aspirateOffsetFromBottomMm = Number(hydratedFormData['aspirate_mmFromBottom']) + const dispenseOffsetFromBottomMm = Number(hydratedFormData['dispense_mmFromBottom']) + + const delayAfterDispense = hydratedFormData['dispense_delay_checkbox'] + ? ((Number(hydratedFormData['dispense_delayMinutes']) || 0) * 60) + + (Number(hydratedFormData['dispense_delaySeconds'] || 0)) : null - const mixFirstAspirate = formData['aspirate_mix_checkbox'] + const mixFirstAspirate = hydratedFormData['aspirate_mix_checkbox'] ? { - volume: Number(formData['aspirate_mix_volume']), - times: parseInt(formData['aspirate_mix_times']), // TODO handle unparseable + volume: Number(hydratedFormData['aspirate_mix_volume']), + times: parseInt(hydratedFormData['aspirate_mix_times']), } : null const mixBeforeAspirate = getMixData( - formData, + hydratedFormData, 'aspirate_mix_checkbox', 'aspirate_mix_volume', 'aspirate_mix_times' ) const mixInDestination = getMixData( - formData, + hydratedFormData, 'dispense_mix_checkbox', 'dispense_mix_volume', 'dispense_mix_times' ) - const changeTip = formData['aspirate_changeTip'] || DEFAULT_CHANGE_TIP_OPTION + const changeTip = hydratedFormData['aspirate_changeTip'] || DEFAULT_CHANGE_TIP_OPTION const commonFields = { - pipette, + pipette: pipette.id, volume, - sourceLabware, - destLabware, + sourceLabware: sourceLabware.id, + destLabware: destLabware.id, aspirateOffsetFromBottomMm, dispenseOffsetFromBottomMm, @@ -87,9 +78,9 @@ const transferLikeFormToArgs = (formData: FormData, context: StepFormContext): T changeTip, delayAfterDispense, mixInDestination, - preWetTip: formData['aspirate_preWetTip'] || false, - touchTipAfterAspirate: formData['aspirate_touchTip'] || false, - touchTipAfterDispense: formData['dispense_touchTip'] || false, + preWetTip: hydratedFormData['aspirate_preWetTip'] || false, + touchTipAfterAspirate: hydratedFormData['aspirate_touchTip'] || false, + touchTipAfterDispense: hydratedFormData['dispense_touchTip'] || false, description: 'description would be here 2018-03-01', // TODO get from form } @@ -100,29 +91,26 @@ const transferLikeFormToArgs = (formData: FormData, context: StepFormContext): T aspirate_wellOrder_second, dispense_wellOrder_first, dispense_wellOrder_second, - } = formData + } = hydratedFormData sourceWells = sourceWells || [] destWells = destWells || [] - if (context && context.labware) { - const labwareById = context.labware - if (stepType !== 'distribute' && sourceLabware) { - const sourceLabwareDef = labwareById[sourceLabware] && getLabware(labwareById[sourceLabware].type) - if (sourceLabwareDef) { - const allWellsOrdered = orderWells(sourceLabwareDef.ordering, aspirate_wellOrder_first, aspirate_wellOrder_second) - sourceWells = intersection(allWellsOrdered, sourceWells) - } else { - console.warn('the specified source labware definition could not be located') - } + if (stepType !== 'distribute' && sourceLabware) { + const sourceLabwareDef = sourceLabware && getLabware(sourceLabware.type) + if (sourceLabwareDef) { + const allWellsOrdered = orderWells(sourceLabwareDef.ordering, aspirate_wellOrder_first, aspirate_wellOrder_second) + sourceWells = intersection(allWellsOrdered, sourceWells) + } else { + console.warn('the specified source labware definition could not be located') } - if (stepType !== 'consolidate' && destLabware) { - const destLabwareDef = labwareById[destLabware] && getLabware(labwareById[destLabware].type) - if (destLabwareDef) { - const allWellsOrdered = orderWells(destLabwareDef.ordering, dispense_wellOrder_first, dispense_wellOrder_second) - destWells = intersection(allWellsOrdered, destWells) - } else { - console.warn('the specified destination labware definition could not be located') - } + } + if (stepType !== 'consolidate' && destLabware) { + const destLabwareDef = destLabware && getLabware(destLabware.type) + if (destLabwareDef) { + const allWellsOrdered = orderWells(destLabwareDef.ordering, dispense_wellOrder_first, dispense_wellOrder_second) + destWells = intersection(allWellsOrdered, destWells) + } else { + console.warn('the specified destination labware definition could not be located') } } @@ -130,9 +118,9 @@ const transferLikeFormToArgs = (formData: FormData, context: StepFormContext): T let disposalDestination = null let disposalLabware = null let disposalWell = null - if (formData['aspirate_disposalVol_checkbox']) { // TODO: BC 09-17-2018 handle unparseable values? - disposalVolume = Number(formData['aspirate_disposalVol_volume']) - disposalDestination = formData['aspirate_disposalVol_destination'] + if (hydratedFormData['aspirate_disposalVol_checkbox']) { + disposalVolume = Number(hydratedFormData['aspirate_disposalVol_volume']) + disposalDestination = hydratedFormData['aspirate_disposalVol_destination'] if (disposalDestination === SOURCE_WELL_DISPOSAL_DESTINATION) { disposalLabware = sourceLabware disposalWell = sourceWells[0] @@ -143,87 +131,51 @@ const transferLikeFormToArgs = (formData: FormData, context: StepFormContext): T } } - // TODO: BC 2018-08-21 remove this old validation logic once no longer preventing save - const requiredFieldErrors = [ - 'pipette', - 'aspirate_labware', - 'dispense_labware', - ].reduce((acc, fieldName) => (!formData[fieldName]) - ? {...acc, [fieldName]: 'This field is required'} - : acc, - {}) - let errors = {...requiredFieldErrors} - if (isNaN(volume) || !(volume > 0)) { - // $FlowFixMe: Cannot assign `'Volume mus...'` to `errors['volume']` because property `volume` is missing in object literal - errors = {...errors, 'volume': 'Volume must be a positive number'} - } - if (stepType === 'transfer' && (sourceWells.length !== destWells.length || sourceWells.length === 0)) { - // $FlowFixMe: Cannot assign `'Numbers of...'` to `errors._mismatchedWells` because property `_mismatchedWells` is missing in object literal - errors = {...errors, '_mismatchedWells': 'Numbers of wells must match'} - } - if (stepType === 'consolidate' && (sourceWells.length <= 1 || destWells.length !== 1)) { - // $FlowFixMe: Cannot assign `'Multiple s...'` to `errors._mismatchedWells` because property `_mismatchedWells` is missing in object literal - errors = {...errors, '_mismatchedWells': 'Multiple source wells and exactly one destination well is required.'} - } - if (stepType === 'distribute' && (sourceWells.length !== 1 || destWells.length <= 1)) { - // $FlowFixMe: Cannot assign `'Single sou...'` to `errors._mismatchedWells` because property `_mismatchedWells` is missing in object literal - errors = {...errors, '_mismatchedWells': 'Single source well and multiple destination wells is required.'} - } - - let stepArguments: TransferLikeValidationAndErrors = {errors, validatedForm: null} switch (stepType) { case 'transfer': { - const transferStepArguments: ValidationAndErrors = { - errors, - validatedForm: Object.values(errors).length === 0 ? { - ...commonFields, - disposalVolume, - stepType: 'transfer', - sourceWells, - destWells, - mixBeforeAspirate, - name: `Transfer ${formData.id}`, // TODO Ian 2018-04-03 real name for steps - } : null, + const transferStepArguments: TransferFormData = { + ...commonFields, + disposalVolume, + stepType: 'transfer', + sourceWells, + destWells, + mixBeforeAspirate, + name: `Transfer ${hydratedFormData.id}`, // TODO Ian 2018-04-03 real name for steps } - stepArguments = transferStepArguments - break + return transferStepArguments } case 'consolidate': { - const consolidateStepArguments: ValidationAndErrors = { - errors, - validatedForm: Object.values(errors).length === 0 ? { - ...commonFields, - disposalVolume, - mixFirstAspirate, - sourceWells, - destWell: destWells[0], - stepType: 'consolidate', - name: `Consolidate ${formData.id}`, // TODO Ian 2018-04-03 real name for steps - } : null, + const consolidateStepArguments: ConsolidateFormData = { + ...commonFields, + disposalVolume, + mixFirstAspirate, + sourceWells, + destWell: destWells[0], + stepType: 'consolidate', + name: `Consolidate ${hydratedFormData.id}`, // TODO Ian 2018-04-03 real name for steps } - stepArguments = consolidateStepArguments - break + return consolidateStepArguments } case 'distribute': { - const distributeStepArguments: ValidationAndErrors = { - errors, - validatedForm: Object.values(errors).length === 0 ? { - ...commonFields, - disposalVolume, - disposalLabware, - disposalWell, - mixBeforeAspirate, - sourceWell: sourceWells[0], - destWells, - stepType: 'distribute', - name: `Distribute ${formData.id}`, // TODO Ian 2018-04-03 real name for steps - } : null, + const distributeStepArguments: DistributeFormData = { + ...commonFields, + disposalVolume, + disposalLabware, + disposalWell, + mixBeforeAspirate, + sourceWell: sourceWells[0], + destWells, + stepType: 'distribute', + name: `Distribute ${hydratedFormData.id}`, // TODO Ian 2018-04-03 real name for steps } - stepArguments = distributeStepArguments - break + return distributeStepArguments + } + default: { + // should never hit default, just a sanity check + console.error('Called TransferLikeFormToArgs with non Transfer-Like step type') + return null } } - return stepArguments } export default transferLikeFormToArgs diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/types.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/types.js deleted file mode 100644 index 98f280d5a54..00000000000 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/types.js +++ /dev/null @@ -1,7 +0,0 @@ -// @flow - -import type { Labware } from '../../../labware-ingred/types' - -export type StepFormContext = { - labware?: ?{[labwareId: string]: ?Labware}, -} diff --git a/protocol-designer/src/steplist/generateSubsteps.js b/protocol-designer/src/steplist/generateSubsteps.js index d7cebefe818..22ca615ae03 100644 --- a/protocol-designer/src/steplist/generateSubsteps.js +++ b/protocol-designer/src/steplist/generateSubsteps.js @@ -2,15 +2,15 @@ import cloneDeep from 'lodash/cloneDeep' import range from 'lodash/range' import mapValues from 'lodash/mapValues' +import isEmpty from 'lodash/isEmpty' import substepTimeline from './substepTimeline' import { utils as steplistUtils, type NamedIngred, + type StepArgsAndErrors, } from '../steplist' -import { type ValidFormAndErrors } from './formLevel/stepFormToArgs' - import type { SubstepItemData, SourceDestSubstepItem, @@ -42,14 +42,14 @@ export type GetIngreds = (labware: string, well: string) => Array type GetLabwareType = (labwareId: string) => ?string function transferLikeSubsteps (args: { - validatedForm: ConsolidateFormData | DistributeFormData | TransferFormData | MixFormData, + stepArgs: ConsolidateFormData | DistributeFormData | TransferFormData | MixFormData, allPipetteData: AllPipetteData, getLabwareType: GetLabwareType, robotState: RobotState, stepId: number, }): ?SourceDestSubstepItem { const { - validatedForm, + stepArgs, allPipetteData, getLabwareType, stepId, @@ -59,9 +59,7 @@ function transferLikeSubsteps (args: { // TODO: Ian 2018-07-31 develop more elegant way to bypass tip handling for simulation/test const robotState = cloneDeep(args.robotState) robotState.tipState.pipettes = mapValues(robotState.tipState.pipettes, () => true) - const { - pipette: pipetteId, - } = validatedForm + const {pipette: pipetteId} = stepArgs const pipette = allPipetteData[pipetteId] @@ -71,43 +69,43 @@ function transferLikeSubsteps (args: { } // if false, show aspirate vol instead - const showDispenseVol = validatedForm.stepType === 'distribute' + const showDispenseVol = stepArgs.stepType === 'distribute' let substepCommandCreators // Call appropriate command creator with the validateForm fields. // Disable any mix args so those aspirate/dispenses don't show up in substeps - if (validatedForm.stepType === 'transfer') { + if (stepArgs.stepType === 'transfer') { const commandCallArgs = { - ...validatedForm, + ...stepArgs, mixBeforeAspirate: null, mixInDestination: null, preWetTip: false, } substepCommandCreators = transfer(commandCallArgs)(robotState) - } else if (validatedForm.stepType === 'distribute') { + } else if (stepArgs.stepType === 'distribute') { const commandCallArgs = { - ...validatedForm, + ...stepArgs, mixBeforeAspirate: null, preWetTip: false, } substepCommandCreators = distribute(commandCallArgs)(robotState) - } else if (validatedForm.stepType === 'consolidate') { + } else if (stepArgs.stepType === 'consolidate') { const commandCallArgs = { - ...validatedForm, + ...stepArgs, mixFirstAspirate: null, mixInDestination: null, preWetTip: false, } substepCommandCreators = consolidate(commandCallArgs)(robotState) - } else if (validatedForm.stepType === 'mix') { - substepCommandCreators = mix(validatedForm)(robotState) + } else if (stepArgs.stepType === 'mix') { + substepCommandCreators = mix(stepArgs)(robotState) } else { // TODO Ian 2018-05-21 Use assert here. Should be unreachable - console.warn(`transferLikeSubsteps got unsupported stepType "${validatedForm.stepType}"`) + console.warn(`transferLikeSubsteps got unsupported stepType "${stepArgs.stepType}"`) return null } @@ -142,7 +140,7 @@ function transferLikeSubsteps (args: { } return { source, - dest: validatedForm.stepType === 'mix' ? source : dest, // NOTE: since source and dest are same for mix, we're showing source on both sides. Otherwise dest would show the intermediate volume state + dest: stepArgs.stepType === 'mix' ? source : dest, // NOTE: since source and dest are same for mix, we're showing source on both sides. Otherwise dest would show the intermediate volume state volume: showDispenseVol ? nextMultiRow.volume : currentMultiRow.volume, } }) @@ -165,7 +163,7 @@ function transferLikeSubsteps (args: { ) return { multichannel: true, - stepType: validatedForm.stepType, + stepType: stepArgs.stepType, parentStepId: stepId, multiRows: mergedMultiRows, } @@ -211,7 +209,7 @@ function transferLikeSubsteps (args: { return { multichannel: false, - stepType: validatedForm.stepType, + stepType: stepArgs.stepType, parentStepId: stepId, rows: mergedRows, } @@ -220,7 +218,7 @@ function transferLikeSubsteps (args: { // NOTE: This is the fn used by the `allSubsteps` selector export function generateSubsteps ( - valForm: ?ValidFormAndErrors, + stepArgsAndErrors: ?StepArgsAndErrors, allPipetteData: AllPipetteData, getLabwareType: GetLabwareType, robotState: ?RobotState, @@ -234,26 +232,26 @@ export function generateSubsteps ( // TODO: BC: 2018-08-21 replace old error check with new logic in field, form, and timeline level // Don't try to render with form errors. TODO LATER: presentational error state of substeps? - if (!valForm || !valForm.validatedForm || Object.values(valForm.errors).length > 0) { + if (!stepArgsAndErrors || !stepArgsAndErrors.stepArgs || !isEmpty(stepArgsAndErrors.errors)) { return null } - const validatedForm = valForm.validatedForm + const {stepArgs} = stepArgsAndErrors - if (validatedForm.stepType === 'pause') { + if (stepArgs.stepType === 'pause') { // just returns formData - const formData: PauseFormData = validatedForm + const formData: PauseFormData = stepArgs return formData } if ( - validatedForm.stepType === 'consolidate' || - validatedForm.stepType === 'distribute' || - validatedForm.stepType === 'transfer' || - validatedForm.stepType === 'mix' + stepArgs.stepType === 'consolidate' || + stepArgs.stepType === 'distribute' || + stepArgs.stepType === 'transfer' || + stepArgs.stepType === 'mix' ) { return transferLikeSubsteps({ - validatedForm, + stepArgs, allPipetteData, getLabwareType, robotState, @@ -261,6 +259,6 @@ export function generateSubsteps ( }) } - console.warn('allSubsteps doesn\'t support step type: ', validatedForm.stepType, stepId) + console.warn('allSubsteps doesn\'t support step type: ', stepArgs.stepType, stepId) return null } diff --git a/protocol-designer/src/steplist/selectors.js b/protocol-designer/src/steplist/selectors.js index 9d7c76c765a..c1c345cd868 100644 --- a/protocol-designer/src/steplist/selectors.js +++ b/protocol-designer/src/steplist/selectors.js @@ -4,15 +4,22 @@ import last from 'lodash/last' import reduce from 'lodash/reduce' import mapValues from 'lodash/mapValues' import max from 'lodash/max' +import isEmpty from 'lodash/isEmpty' +import each from 'lodash/each' +import some from 'lodash/some' import {selectors as labwareIngredSelectors} from '../labware-ingred/reducers' +import {selectors as pipetteSelectors} from '../pipettes' import { getFormWarnings, getFormErrors, stepFormToArgs, } from './formLevel' import type {FormError, FormWarning} from './formLevel' -import {hydrateField} from './fieldLevel' +import { + hydrateField, + getFieldErrors, +} from './fieldLevel' import {initialSelectedItemState} from './reducers' import type {RootState, OrderedStepsState, SelectableItem} from './reducers' import type {BaseState, Selector} from '../types' @@ -22,6 +29,9 @@ import type { FormSectionState, SubstepIdentifier, TerminalItemId, + StepFormAndFieldErrors, + StepArgsAndErrors, + StepFormContextualState, } from './types' import type { @@ -29,17 +39,37 @@ import type { StepIdType, } from '../form-types' -import { type ValidFormAndErrors } from './formLevel/stepFormToArgs' +const NO_SAVED_FORM_ERROR = 'NO_SAVED_FORM_ERROR' // TODO Ian 2018-01-19 Rethink the hard-coded 'steplist' key in Redux root const rootSelector = (state: BaseState): RootState => state.steplist // ======= Selectors =============================================== -const getUnsavedForm = createSelector( +const getUnsavedForm: Selector = createSelector( rootSelector, (state: RootState) => state.unsavedForm ) + +const getStepFormContextualState: Selector = createSelector( + labwareIngredSelectors.rootSelector, + pipetteSelectors.rootSelector, + (labwareIngred, _pipettes) => ({ + labwareIngred: labwareIngred, + pipettes: _pipettes, + }) +) + +const getHydratedUnsavedForm: Selector = createSelector( + getUnsavedForm, + getStepFormContextualState, + (_unsavedForm, _contextualState) => ( + _unsavedForm && mapValues(_unsavedForm, (value, name) => ( + hydrateField(_contextualState, name, value) + )) + ) +) + // TODO Ian 2018-02-08 rename formData to something like getUnsavedForm or unsavedFormFields // NOTE: DEPRECATED use getUnsavedForm instead const formData = getUnsavedForm @@ -119,34 +149,76 @@ const getSavedForms: Selector<{[StepIdType]: FormData}> = createSelector( getSteps, orderedStepsSelector, (state: BaseState) => rootSelector(state).savedStepForms, - (_steps, _orderedSteps, _savedStepForms) => { - _orderedSteps.forEach(stepId => { - if (!_steps[stepId]) { + (steps, orderedSteps, savedStepForms) => { + orderedSteps.forEach(stepId => { + if (!steps[stepId]) { console.error(`Encountered an undefined step: ${stepId}`) } }) - return _savedStepForms + return savedStepForms } ) -// TODO Brian 2018-08-21 rename validatedForms -> stepArguments since it should only include -// the results of translating form data into step generation arguments -const validatedForms: Selector<{[StepIdType]: ValidFormAndErrors}> = createSelector( - getSteps, +const getHydratedSavedForms: Selector<{[StepIdType]: FormData}> = createSelector( getSavedForms, + getStepFormContextualState, + (savedForms, contextualState) => ( + mapValues(savedForms, (savedForm) => ( + mapValues(savedForm, (value, name) => ( + hydrateField(contextualState, name, value) + )) + )) + ) +) + +// TODO type with hydrated form type +const getAllErrorsFromHydratedForm = (hydratedForm: FormData): StepFormAndFieldErrors => { + let errors: StepFormAndFieldErrors = {} + + each(hydratedForm, (value, fieldName) => { + const fieldErrors = getFieldErrors(fieldName, value) + if (fieldErrors && fieldErrors.length > 0) { + errors = { + ...errors, + field: { + ...errors.field, + [fieldName]: fieldErrors, + }, + } + } + }) + const formErrors = getFormErrors(hydratedForm.stepType, hydratedForm) + if (formErrors && formErrors.length > 0) { + errors = {...errors, form: formErrors} + } + + return errors +} + +// TODO Brian 2018-10-29 separate out getErrors and getStepArgs +const getArgsAndErrorsByStepId: Selector<{[StepIdType]: StepArgsAndErrors}> = createSelector( + getSteps, + getHydratedSavedForms, orderedStepsSelector, - labwareIngredSelectors.getLabware, - (_steps, _savedStepForms, _orderedSteps, _labware) => { - return reduce(_orderedSteps, (acc, stepId) => { - const nextStepData = (_steps[stepId] && _savedStepForms[stepId]) - ? stepFormToArgs(_savedStepForms[stepId], {labware: _labware}) + (steps, savedStepForms, orderedSteps) => { + return reduce(orderedSteps, (acc, stepId) => { + let nextStepData + if (steps[stepId] && savedStepForms[stepId]) { + const savedForm = savedStepForms[stepId] + + const errors = getAllErrorsFromHydratedForm(savedForm) + + nextStepData = isEmpty(errors) + ? {stepArgs: stepFormToArgs(savedForm)} + : {errors, stepArgs: null} + } else { // NOTE: usually, stepFormData is undefined here b/c there's no saved step form for it: - : { - errors: {'form': ['no saved form for step ' + stepId]}, - validatedForm: null, - } // TODO Ian 2018-03-20 revisit "no saved form for step" - + nextStepData = { + errors: {form: [{title: NO_SAVED_FORM_ERROR}]}, + stepArgs: null, + } + } // TODO Ian 2018-03-20 revisit "no saved form for step" return { ...acc, [stepId]: nextStepData, @@ -155,6 +227,21 @@ const validatedForms: Selector<{[StepIdType]: ValidFormAndErrors}> = createSelec } ) +// TODO: BC 2018-10-30 after separation of getStepArgs and getStepErrors +// , move the NO_SAVED_FORM_ERROR into a separate wrapping selector +// it is currently there to keep the step item error state from appearing +// before you've saved the form once +const getFormAndFieldErrorsByStepId: Selector<{[StepIdType]: StepFormAndFieldErrors}> = createSelector( + getArgsAndErrorsByStepId, + (stepsArgsAndErrors) => ( + mapValues(stepsArgsAndErrors, (argsAndErrors) => { + const formErrors = argsAndErrors.errors && argsAndErrors.errors.form + if (formErrors && some(formErrors, error => error.title === NO_SAVED_FORM_ERROR)) return {} + return argsAndErrors.errors + }) + ) +) + const isNewStepForm = createSelector( formData, getSavedForms, @@ -163,15 +250,15 @@ const isNewStepForm = createSelector( /** Array of labware (labwareId's) involved in hovered Step, or [] */ const hoveredStepLabware: Selector> = createSelector( - validatedForms, + getArgsAndErrorsByStepId, getHoveredStepId, - (_forms, _hoveredStep) => { + (allStepArgsAndErrors, hoveredStep) => { const blank = [] - if (typeof _hoveredStep !== 'number' || !_forms[_hoveredStep]) { + if (typeof hoveredStep !== 'number' || !allStepArgsAndErrors[hoveredStep]) { return blank } - const stepForm = _forms[_hoveredStep].validatedForm + const stepForm = allStepArgsAndErrors[hoveredStep].stepArgs if (!stepForm) { return blank @@ -211,35 +298,35 @@ const stepCreationButtonExpandedSelector: Selector = createSelector( const nextStepId: Selector = createSelector( // generates the next step ID to use getSteps, - (_steps): number => { - const allStepIds = Object.keys(_steps).map(stepId => parseInt(stepId)) + (steps): number => { + const allStepIds = Object.keys(steps).map(stepId => parseInt(stepId)) return allStepIds.length === 0 ? 0 : max(allStepIds) + 1 } ) -// TODO: remove this when we add in form level validation -const currentFormErrors: Selector = (state: BaseState) => { - const form = formData(state) - return form && stepFormToArgs(form).errors // TODO refactor selectors -} - -const formLevelWarnings: Selector> = (state) => { - const formData = getUnsavedForm(state) - if (!formData) return [] - const {id, stepType, ...fields} = formData - const hydratedFields = mapValues(fields, (value, name) => hydrateField(state, name, value)) - return getFormWarnings(stepType, hydratedFields) -} +const formLevelWarnings: Selector> = createSelector( + getUnsavedForm, + getStepFormContextualState, + (unsavedFormData, contextualState) => { + if (!unsavedFormData) return [] + const {id, stepType, ...fields} = unsavedFormData + const hydratedFields = mapValues(fields, (value, name) => hydrateField(contextualState, name, value)) + return getFormWarnings(stepType, hydratedFields) + } +) -const formLevelErrors: Selector> = (state) => { - const formData = getUnsavedForm(state) - if (!formData) return [] - const {id, stepType, ...fields} = formData - const hydratedFields = mapValues(fields, (value, name) => hydrateField(state, name, value)) - return getFormErrors(stepType, hydratedFields) -} +const formLevelErrors: Selector> = createSelector( + getUnsavedForm, + getStepFormContextualState, + (unsavedFormData, contextualState) => { + if (!unsavedFormData) return [] + const {id, stepType, ...fields} = unsavedFormData + const hydratedFields = mapValues(fields, (value, name) => hydrateField(contextualState, name, value)) + return getFormErrors(stepType, hydratedFields) + } +) const formSectionCollapseSelector: Selector = createSelector( rootSelector, @@ -251,11 +338,11 @@ export const allSteps: Selector<{[stepId: StepIdType]: StepItemData}> = createSe getCollapsedSteps, getSavedForms, labwareIngredSelectors.getLabware, - (steps, collapsedSteps, _savedForms, _labware) => { + (steps, collapsedSteps, savedForms, labware) => { return mapValues( steps, (step: StepItemData, id: StepIdType): StepItemData => { - const savedForm = (_savedForms && _savedForms[id]) || null + const savedForm = (savedForms && savedForms[id]) || null // Assign the step title let title @@ -291,22 +378,22 @@ const getSelectedStep = createSelector( } ) -// TODO: BC: 2018-08-21 remove this, always allow save +// TODO: BC: 2018-10-26 remove this when we decide to not block save export const currentFormCanBeSaved: Selector = createSelector( - formData, + getHydratedUnsavedForm, getSelectedStepId, allSteps, - labwareIngredSelectors.getLabware, - (formData, selectedStepId, allSteps, labware) => - ((typeof selectedStepId === 'number') && allSteps[selectedStepId] && formData) - ? Object.values(stepFormToArgs(formData, {labware}).errors).length === 0 - : null + (hydratedForm, selectedStepId, _allSteps) => { + if (selectedStepId == null || !_allSteps[selectedStepId] || !hydratedForm) return null + return isEmpty(getAllErrorsFromHydratedForm(hydratedForm)) + } ) export default { rootSelector, allSteps, + getFormAndFieldErrorsByStepId, currentFormCanBeSaved, getSelectedStep, @@ -319,12 +406,12 @@ export default { getActiveItem, getHoveredSubstep, getUnsavedForm, + getHydratedUnsavedForm, formData, // TODO: remove after sunset formModalData, nextStepId, - validatedForms, + getArgsAndErrorsByStepId, isNewStepForm, - currentFormErrors, // TODO: remove after sunset formLevelWarnings, formLevelErrors, formSectionCollapse: formSectionCollapseSelector, diff --git a/protocol-designer/src/steplist/types.js b/protocol-designer/src/steplist/types.js index 7ff0e4d4e98..dc0e707afac 100644 --- a/protocol-designer/src/steplist/types.js +++ b/protocol-designer/src/steplist/types.js @@ -1,6 +1,9 @@ // @flow -import type {PauseFormData} from '../step-generation' +import type {PauseFormData, CommandCreatorData} from '../step-generation' import type {FormData, StepIdType, StepType, TransferLikeStepType} from '../form-types' +import type {BaseState} from '../types' +import type {FormError} from './formLevel/errors' +import type {StepFieldName} from './fieldLevel' // sections of the form that are expandable/collapsible export type FormSectionState = {aspirate: boolean, dispense: boolean} @@ -84,3 +87,18 @@ export type StepItemData = { } export type SubSteps = {[StepIdType]: ?SubstepItemData} + +export type StepFormAndFieldErrors = { + field?: {[StepFieldName]: Array}, + form?: Array, +} + +export type StepArgsAndErrors = { + errors: StepFormAndFieldErrors, + stepArgs: CommandCreatorData | null, // TODO: incompleteData field when this is null? +} + +export type StepFormContextualState = { + labwareIngred: $PropertyType, + pipettes: $PropertyType, +} diff --git a/protocol-designer/src/top-selectors/substep-highlight.js b/protocol-designer/src/top-selectors/substep-highlight.js index d5f95f98892..2c11597e09f 100644 --- a/protocol-designer/src/top-selectors/substep-highlight.js +++ b/protocol-designer/src/top-selectors/substep-highlight.js @@ -136,21 +136,21 @@ function _getSelectedWellsForSubstep ( export const wellHighlightsByLabwareId: Selector = createSelector( fileDataSelectors.robotStateTimeline, - steplistSelectors.validatedForms, + steplistSelectors.getArgsAndErrorsByStepId, steplistSelectors.getHoveredStepId, steplistSelectors.getHoveredSubstep, allSubsteps, steplistSelectors.orderedSteps, - (robotStateTimeline, forms, hoveredStepId, hoveredSubstep, allSubsteps, orderedSteps) => { + (robotStateTimeline, allStepArgsAndErrors, hoveredStepId, hoveredSubstep, allSubsteps, orderedSteps) => { const timeline = robotStateTimeline.timeline const stepId = hoveredStepId const timelineIndex = orderedSteps.findIndex(i => i === stepId) const frame = timeline[timelineIndex] const robotState = frame && frame.robotState - const form = stepId != null && forms[stepId] && forms[stepId].validatedForm + const stepArgs = stepId != null && allStepArgsAndErrors[stepId] && allStepArgsAndErrors[stepId].stepArgs - if (!robotState || stepId == null || !form) { - // nothing hovered, or no form for step + if (!robotState || stepId == null || !stepArgs) { + // nothing hovered, or no stepArgs for step return {} } @@ -162,14 +162,14 @@ export const wellHighlightsByLabwareId: Selector = if (hoveredSubstep != null) { // wells for hovered substep selectedWells = _getSelectedWellsForSubstep( - form, + stepArgs, labwareId, allSubsteps[stepId], hoveredSubstep.substepIndex ) } else { // wells for step overall - selectedWells = _getSelectedWellsForStep(form, labwareId, robotState) + selectedWells = _getSelectedWellsForStep(stepArgs, labwareId, robotState) } // return selected wells eg {A1: true, B4: true} diff --git a/protocol-designer/src/top-selectors/substeps.js b/protocol-designer/src/top-selectors/substeps.js index ef5013a4efd..70e41b93250 100644 --- a/protocol-designer/src/top-selectors/substeps.js +++ b/protocol-designer/src/top-selectors/substeps.js @@ -16,27 +16,26 @@ import type {SubstepItemData} from '../steplist/types' type AllSubsteps = {[StepIdType]: ?SubstepItemData} export const allSubsteps: Selector = createSelector( - steplistSelectors.validatedForms, + steplistSelectors.getArgsAndErrorsByStepId, pipetteSelectors.equippedPipettes, labwareIngredSelectors.getLabwareTypes, steplistSelectors.orderedSteps, fileDataSelectors.robotStateTimeline, fileDataSelectors.getInitialRobotState, ( - validatedForms, + allStepArgsAndErrors, allPipetteData, allLabwareTypes, orderedSteps, robotStateTimeline, _initialRobotState, ) => { - return orderedSteps - .reduce((acc: AllSubsteps, stepId, timelineIndex) => { + return orderedSteps.reduce((acc: AllSubsteps, stepId, timelineIndex) => { const timeline = [{robotState: _initialRobotState}, ...robotStateTimeline.timeline] const robotState = timeline[timelineIndex] && timeline[timelineIndex].robotState const substeps = generateSubsteps( - validatedForms[stepId], + allStepArgsAndErrors[stepId], allPipetteData, (labwareId: string) => allLabwareTypes[labwareId], robotState, diff --git a/protocol-designer/src/well-selection/reducers.js b/protocol-designer/src/well-selection/reducers.js index f4a79e1a54b..370f804e568 100644 --- a/protocol-designer/src/well-selection/reducers.js +++ b/protocol-designer/src/well-selection/reducers.js @@ -45,6 +45,7 @@ const selectedWells = handleActions({ SET_WELL_CONTENTS: () => selectedWellsInitialState, }, selectedWellsInitialState) +// TODO: BC 2018-11-01 unused, remove and all references to the actions type WellSelectionModalState = OpenWellSelectionModalPayload | null const wellSelectionModal = handleActions({ OPEN_WELL_SELECTION_MODAL: (state, action: {payload: OpenWellSelectionModalPayload}) => action.payload,