From 53e8ddf9be3b9cf04526d3e2bf7d685654ecae81 Mon Sep 17 00:00:00 2001 From: Ian London Date: Mon, 2 Mar 2020 16:26:10 -0500 Subject: [PATCH] feat(protocol-designer): save v3/v4 files correctly (#5100) Closes #4919 --- .../FileSidebar/__tests__/FileSidebar.test.js | 2 +- .../src/components/FileSidebar/index.js | 2 +- .../__fixtures__/createFile/commonFields.js | 66 +++++++ .../__fixtures__/createFile/engageMagnet.js | 90 +++++++++ .../__fixtures__/createFile/noModules.js | 176 ++++++++++++++++++ .../file-data/__tests__/createFile.test.js | 99 ++++++++++ .../__tests__/getIsV4Protocol.test.js | 59 ++++++ .../src/file-data/selectors/fileCreator.js | 72 +++++-- protocol-designer/src/file-types.js | 8 +- .../src/step-forms/selectors/index.js | 5 - shared-data/protocol/flowTypes/schemaV4.js | 4 +- 11 files changed, 556 insertions(+), 27 deletions(-) create mode 100644 protocol-designer/src/file-data/__fixtures__/createFile/commonFields.js create mode 100644 protocol-designer/src/file-data/__fixtures__/createFile/engageMagnet.js create mode 100644 protocol-designer/src/file-data/__fixtures__/createFile/noModules.js create mode 100644 protocol-designer/src/file-data/__tests__/createFile.test.js create mode 100644 protocol-designer/src/file-data/__tests__/getIsV4Protocol.test.js diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.js b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.js index 52b73978016..5e705dac00d 100644 --- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.js +++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.js @@ -37,7 +37,7 @@ describe('FileSidebar', () => { metadata: {}, pipettes: {}, robot: { model: 'OT-2 Standard' }, - schemaVersion: 4, + schemaVersion: 3, commands: [], }, fileName: 'protocol.json', diff --git a/protocol-designer/src/components/FileSidebar/index.js b/protocol-designer/src/components/FileSidebar/index.js index 2873ba857e7..2d1e9e97c3c 100644 --- a/protocol-designer/src/components/FileSidebar/index.js +++ b/protocol-designer/src/components/FileSidebar/index.js @@ -51,7 +51,7 @@ function mapStateToProps(state: BaseState): SP { // Ignore clicking 'CREATE NEW' button in these cases _canCreateNew: !selectors.getNewProtocolModal(state), _hasUnsavedChanges: loadFileSelectors.getHasUnsavedChanges(state), - isV4Protocol: stepFormSelectors.getIsV4Protocol(state), + isV4Protocol: fileDataSelectors.getIsV4Protocol(state), } } diff --git a/protocol-designer/src/file-data/__fixtures__/createFile/commonFields.js b/protocol-designer/src/file-data/__fixtures__/createFile/commonFields.js new file mode 100644 index 00000000000..37079731a57 --- /dev/null +++ b/protocol-designer/src/file-data/__fixtures__/createFile/commonFields.js @@ -0,0 +1,66 @@ +// @flow +// Named arguments to createFile selector. This data would be the result of several selectors. +import { fixtureP10Single } from '@opentrons/shared-data/pipette/fixtures/name' +import fixture_96_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json' +import fixture_tiprack_10_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul.json' +import fixture_trash from '@opentrons/shared-data/labware/fixtures/2/fixture_trash.json' +import type { DismissedWarningState } from '../../../dismiss/reducers' +import type { IngredientsState } from '../../../labware-ingred/reducers' +import type { LabwareDefByDefURI } from '../../../labware-defs' +import type { LabwareEntities, PipetteEntities } from '../../../step-forms' +import type { LabwareLiquidState } from '../../../step-generation' +import type { FileMetadataFields } from '../../types' + +export const fileMetadata: FileMetadataFields = { + protocolName: 'Test Protocol', + author: 'The Author', + description: 'Protocol description', + created: 1582667312515, +} + +export const dismissedWarnings: DismissedWarningState = { + form: {}, + timeline: {}, +} + +export const ingredients: IngredientsState = {} + +export const ingredLocations: LabwareLiquidState = {} + +export const labwareEntities: LabwareEntities = { + trashId: { + labwareDefURI: 'opentrons/opentrons_1_trash_1100ml_fixed/1', + id: 'trashId', + def: fixture_trash, + }, + tiprackId: { + labwareDefURI: 'opentrons/opentrons_96_tiprack_10ul/1', + id: 'tiprackId', + def: fixture_tiprack_10_ul, + }, + plateId: { + labwareDefURI: 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1', + id: 'plateId', + def: fixture_96_plate, + }, +} + +export const pipetteEntities: PipetteEntities = { + pipetteId: { + id: 'pipetteId', + name: 'p10_single', + spec: fixtureP10Single, + tiprackDefURI: 'opentrons/opentrons_96_tiprack_10ul/1', + tiprackLabwareDef: fixture_tiprack_10_ul, + }, +} +export const labwareNicknamesById: { [labwareId: string]: string } = { + trashId: 'Trash', + tiprackId: 'Opentrons 96 Tip Rack 10 µL', + plateId: 'NEST 96 Well Plate 100 µL PCR Full Skirt', +} +export const labwareDefsByURI: LabwareDefByDefURI = { + 'opentrons/opentrons_96_tiprack_10ul/1': fixture_tiprack_10_ul, + 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1': fixture_96_plate, + 'opentrons/opentrons_1_trash_1100ml_fixed/1': fixture_trash, +} diff --git a/protocol-designer/src/file-data/__fixtures__/createFile/engageMagnet.js b/protocol-designer/src/file-data/__fixtures__/createFile/engageMagnet.js new file mode 100644 index 00000000000..7e1b587216d --- /dev/null +++ b/protocol-designer/src/file-data/__fixtures__/createFile/engageMagnet.js @@ -0,0 +1,90 @@ +// @flow +// Named arguments to createFile selector. This data would be the result of several selectors. +import type { SavedStepFormState, ModuleEntities } from '../../../step-forms' +import type { RobotState, Timeline } from '../../../step-generation' +import type { StepIdType } from '../../../form-types' + +export const initialRobotState: RobotState = { + labware: { + trashId: { + slot: '12', + }, + tiprackId: { + slot: '2', + }, + plateId: { + slot: 'magneticModuleId', + }, + }, + modules: { + magneticModuleId: { + slot: '1', + moduleState: { + type: 'magneticModuleType', + engaged: true, + }, + }, + }, + pipettes: { + pipetteId: { + mount: 'left', + }, + }, + liquidState: { labware: {}, pipettes: {} }, + tipState: { tipracks: {}, pipettes: {} }, +} + +export const robotStateTimeline: Timeline = { + timeline: [ + { + commands: [ + { + command: 'magneticModule/engageMagnet', + params: { + module: 'magneticModuleId', + engageHeight: 16, + }, + }, + ], + robotState: initialRobotState, + }, + ], + errors: null, +} + +export const savedStepForms: SavedStepFormState = { + __INITIAL_DECK_SETUP_STEP__: { + stepType: 'manualIntervention', + id: '__INITIAL_DECK_SETUP_STEP__', + labwareLocationUpdate: { + trashId: '12', + tiprackId: '2', + plateId: 'magneticModuleId', + }, + pipetteLocationUpdate: { + pipetteId: 'left', + }, + moduleLocationUpdate: { + magneticModuleId: '1', + }, + }, + engageMagnetStepId: { + id: 'engageMagnetStepId', + stepType: 'magnet', + stepName: 'magnet', + stepDetails: '', + moduleId: 'magneticModuleId', + magnetAction: 'engage', + engageHeight: '16', + }, +} + +export const orderedStepIds: Array = ['engageMagnetStepId'] + +export const moduleEntities: ModuleEntities = { + magneticModuleId: { + id: 'magneticModuleId', + type: 'magneticModuleType', + model: 'magneticModuleV1', + }, +} diff --git a/protocol-designer/src/file-data/__fixtures__/createFile/noModules.js b/protocol-designer/src/file-data/__fixtures__/createFile/noModules.js new file mode 100644 index 00000000000..1f374d50133 --- /dev/null +++ b/protocol-designer/src/file-data/__fixtures__/createFile/noModules.js @@ -0,0 +1,176 @@ +// @flow +// Named arguments to createFile selector. This data would be the result of several selectors. +import type { SavedStepFormState, ModuleEntities } from '../../../step-forms' +import type { RobotState, Timeline } from '../../../step-generation' +import type { StepIdType } from '../../../form-types' + +export const initialRobotState: RobotState = { + labware: { + trashId: { + slot: '12', + }, + tiprackId: { + slot: '2', + }, + plateId: { + slot: '1', + }, + }, + modules: {}, + pipettes: { + pipetteId: { + mount: 'left', + }, + }, + liquidState: { + pipettes: {}, + labware: {}, + }, + tipState: { + pipettes: {}, + tipracks: {}, + }, +} + +export const robotStateTimeline: Timeline = { + timeline: [ + { + commands: [ + { + command: 'pickUpTip', + params: { + pipette: 'pipetteId', + labware: 'tiprackId', + well: 'A1', + }, + }, + { + command: 'aspirate', + params: { + pipette: 'pipetteId', + volume: 6, + labware: 'plateId', + well: 'A1', + offsetFromBottomMm: 1, + flowRate: 3.78, + }, + }, + { + command: 'dispense', + params: { + pipette: 'pipetteId', + volume: 6, + labware: 'trashId', + well: 'A1', + offsetFromBottomMm: 0.5, + flowRate: 3.78, + }, + }, + { + command: 'dropTip', + params: { + pipette: 'pipetteId', + labware: 'trashId', + well: 'A1', + }, + }, + { + command: 'pickUpTip', + params: { + pipette: 'pipetteId', + labware: 'tiprackId', + well: 'B1', + }, + }, + { + command: 'aspirate', + params: { + pipette: 'pipetteId', + volume: 6, + labware: 'plateId', + well: 'B1', + offsetFromBottomMm: 1, + flowRate: 3.78, + }, + }, + { + command: 'dispense', + params: { + pipette: 'pipetteId', + volume: 6, + labware: 'trashId', + well: 'A1', + offsetFromBottomMm: 0.5, + flowRate: 3.78, + }, + }, + { + command: 'dropTip', + params: { + pipette: 'pipetteId', + labware: 'trashId', + well: 'A1', + }, + }, + ], + robotState: initialRobotState, + warnings: [], + }, + ], + errors: null, +} + +export const savedStepForms: SavedStepFormState = { + __INITIAL_DECK_SETUP_STEP__: { + stepType: 'manualIntervention', + id: '__INITIAL_DECK_SETUP_STEP__', + labwareLocationUpdate: { + trashId: '12', + tiprackId: '2', + plateId: '1', + }, + pipetteLocationUpdate: { + pipetteId: 'left', + }, + moduleLocationUpdate: {}, + }, + moveLiquidStepId: { + id: 'moveLiquidStepId', + stepType: 'moveLiquid', + stepName: 'transfer', + stepDetails: '', + pipette: 'pipetteId', + volume: '6', + changeTip: 'always', + path: 'single', + aspirate_wells_grouped: false, + aspirate_flowRate: null, + aspirate_labware: 'plateId', + aspirate_wells: ['A1', 'B1'], + aspirate_wellOrder_first: 't2b', + aspirate_wellOrder_second: 'l2r', + aspirate_mix_checkbox: false, + aspirate_mix_times: null, + aspirate_mix_volume: null, + aspirate_mmFromBottom: 1, + aspirate_touchTip_checkbox: false, + dispense_flowRate: null, + dispense_labware: 'trashId', + dispense_wells: ['A1'], + dispense_wellOrder_first: 't2b', + dispense_wellOrder_second: 'l2r', + dispense_mix_checkbox: false, + dispense_mix_times: null, + dispense_mix_volume: null, + dispense_mmFromBottom: 0.5, + dispense_touchTip_checkbox: false, + disposalVolume_checkbox: true, + disposalVolume_volume: '1', + blowout_checkbox: false, + blowout_location: 'trashId', + preWetTip: false, + }, +} +export const orderedStepIds: Array = ['moveLiquidStepId'] + +export const moduleEntities: ModuleEntities = {} diff --git a/protocol-designer/src/file-data/__tests__/createFile.test.js b/protocol-designer/src/file-data/__tests__/createFile.test.js new file mode 100644 index 00000000000..587985135cb --- /dev/null +++ b/protocol-designer/src/file-data/__tests__/createFile.test.js @@ -0,0 +1,99 @@ +// @flow +import Ajv from 'ajv' +import isEmpty from 'lodash/isEmpty' +import protocolV3Schema from '@opentrons/shared-data/protocol/schemas/3.json' +import protocolV4Schema from '@opentrons/shared-data/protocol/schemas/4.json' +import labwareV2Schema from '@opentrons/shared-data/labware/schemas/2.json' +import { createFile } from '../selectors' +import { + fileMetadata, + dismissedWarnings, + ingredients, + ingredLocations, + labwareEntities, + labwareNicknamesById, + labwareDefsByURI, + pipetteEntities, +} from '../__fixtures__/createFile/commonFields' +import * as engageMagnet from '../__fixtures__/createFile/engageMagnet' +import * as noModules from '../__fixtures__/createFile/noModules' + +const getAjvValidator = _protocolSchema => { + const ajv = new Ajv({ + allErrors: true, + jsonPointers: true, + }) + // v3 and v4 protocol schema contain reference to v2 labware schema, so give AJV access to it + ajv.addSchema(labwareV2Schema) + const validateProtocol = ajv.compile(_protocolSchema) + return validateProtocol +} + +const expectResultToMatchSchema = (result, _protocolSchema): void => { + const validate = getAjvValidator(_protocolSchema) + const valid = validate(result) + const validationErrors = validate.errors + + if (validationErrors) { + console.log(JSON.stringify(validationErrors, null, 4)) + } + expect(valid).toBe(true) + expect(validationErrors).toBe(null) +} + +describe('createFile selector', () => { + it('should return a schema-valid JSON V3 protocol, if the protocol has NO modules', () => { + // $FlowFixMe TODO(IL, 2020-02-25): Flow doesn't have type for resultFunc + const result = createFile.resultFunc( + fileMetadata, + noModules.initialRobotState, + noModules.robotStateTimeline, + dismissedWarnings, + ingredients, + ingredLocations, + noModules.savedStepForms, + noModules.orderedStepIds, + labwareEntities, + noModules.moduleEntities, + pipetteEntities, + labwareNicknamesById, + labwareDefsByURI, + false // isV4Protocol + ) + + expectResultToMatchSchema(result, protocolV3Schema) + + // check for false positives: if the output is lacking these entities, we don't + // have the opportunity to validate their part of the schema + expect(!isEmpty(result.labware)).toBe(true) + expect(!isEmpty(result.pipettes)).toBe(true) + }) + + it('should return a schema-valid JSON V4 protocol, if the protocol does have modules', () => { + // $FlowFixMe TODO(IL, 2020-02-25): Flow doesn't have type for resultFunc + const result = createFile.resultFunc( + fileMetadata, + engageMagnet.initialRobotState, + engageMagnet.robotStateTimeline, + dismissedWarnings, + ingredients, + ingredLocations, + engageMagnet.savedStepForms, + engageMagnet.orderedStepIds, + labwareEntities, + engageMagnet.moduleEntities, + pipetteEntities, + labwareNicknamesById, + labwareDefsByURI, + true // isV4Protocol + ) + + expectResultToMatchSchema(result, protocolV4Schema) + + // check for false positives: if the output is lacking these entities, we don't + // have the opportunity to validate their part of the schema + expect(!isEmpty(result.modules)).toBe(true) + expect(!isEmpty(result.labware)).toBe(true) + expect(!isEmpty(result.pipettes)).toBe(true) + }) +}) diff --git a/protocol-designer/src/file-data/__tests__/getIsV4Protocol.test.js b/protocol-designer/src/file-data/__tests__/getIsV4Protocol.test.js new file mode 100644 index 00000000000..ad90938da04 --- /dev/null +++ b/protocol-designer/src/file-data/__tests__/getIsV4Protocol.test.js @@ -0,0 +1,59 @@ +// @flow +import { getIsV4Protocol } from '../selectors/fileCreator' +import { + MAGNETIC_MODULE_TYPE, + MAGNETIC_MODULE_V1, +} from '@opentrons/shared-data' +import type { ModuleEntities } from '../../step-forms' + +describe('getIsV4Protocol selector', () => { + const testCases: Array<{| + testName: string, + robotStateTimeline: { + // NOTE: this is a simplified version of Timeline type so we don't need a huge fixture + timeline: Array<{ commands: Array<{ command: string }> }>, + }, + moduleEntities: ModuleEntities, + expected: boolean, + |}> = [ + { + testName: 'should return true if there are modules', + expected: true, + robotStateTimeline: { timeline: [] }, + moduleEntities: { + someModule: { + id: 'moduleId', + type: MAGNETIC_MODULE_TYPE, + model: MAGNETIC_MODULE_V1, + }, + }, + }, + { + testName: 'should return true if there are non-v3 commands', + expected: true, + robotStateTimeline: { + timeline: [{ commands: [{ command: 'someNonV4Command' }] }], + }, + moduleEntities: {}, + }, + { + testName: + 'should return false if there are no modules and no v4-specific commands', + expected: false, + robotStateTimeline: { timeline: [] }, + moduleEntities: {}, + }, + ] + testCases.forEach( + ({ testName, robotStateTimeline, moduleEntities, expected }) => { + it(testName, () => { + // $FlowFixMe TODO(IL, 2020-02-25): Flow doesn't have type for resultFunc + const result = getIsV4Protocol.resultFunc( + robotStateTimeline, + moduleEntities + ) + expect(result).toBe(expected) + }) + } + ) +}) diff --git a/protocol-designer/src/file-data/selectors/fileCreator.js b/protocol-designer/src/file-data/selectors/fileCreator.js index d6687970ee3..9ea8379ae77 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.js +++ b/protocol-designer/src/file-data/selectors/fileCreator.js @@ -1,13 +1,12 @@ // @flow import { createSelector } from 'reselect' -import flatten from 'lodash/flatten' +import flatMap from 'lodash/flatMap' import isEmpty from 'lodash/isEmpty' import mapValues from 'lodash/mapValues' import uniq from 'lodash/uniq' import { getFileMetadata } from './fileFields' import { getInitialRobotState, getRobotStateTimeline } from './commands' import { selectors as dismissSelectors } from '../../dismiss' -import { selectors as featureFlagSelectors } from '../../feature-flags' import { selectors as labwareDefSelectors } from '../../labware-defs' import { selectors as ingredSelectors } from '../../labware-ingred/selectors' import { selectors as stepFormSelectors } from '../../step-forms' @@ -18,7 +17,6 @@ import { DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } from '../../constants' - import type { FilePipette, FileLabware, @@ -29,9 +27,6 @@ import type { ModuleEntity } from '../../step-forms' import type { Selector } from '../../types' import type { PDProtocolFile } from '../../file-types' -// TODO: Ian 2019-11-11 remove the `any` and bump to 4 when v4 is released -const protocolSchemaVersion: any = 3 - // TODO: BC: 2018-02-21 uncomment this assert, causes test failures // assert(!isEmpty(process.env.OT_PD_VERSION), 'Could not find application version!') if (isEmpty(process.env.OT_PD_VERSION)) @@ -43,7 +38,38 @@ const applicationVersion: string = process.env.OT_PD_VERSION || '' // when we look at saved protocols (without requiring us to trace thru git logs) const _internalAppBuildDate = process.env.OT_PD_BUILD_DATE -// $FlowFixMe: TODO IL 2020-02-24 type as 'schemaV3 | schemaV4' in #4919 +// NOTE: V3 commands are a subset of V4 commands. +const _isV3Command = (command: Command): boolean => + command.command === 'aspirate' || + command.command === 'dispense' || + command.command === 'airGap' || + command.command === 'blowout' || + command.command === 'touchTip' || + command.command === 'pickUpTip' || + command.command === 'dropTip' || + command.command === 'moveToSlot' || + command.command === 'delay' + +/** If there are any module entities or and v4-specific commands, + ** export as a v4 protocol. Otherwise, export as v3. + ** + ** NOTE: In real life, you shouldn't be able to have v4 atomic commands + ** without having module entities b/c this will produce "no module for this step" + ** form/timeline errors. Checking for v4 commands should be redundant, + ** we do it just in case non-V3 commands somehow sneak in despite having no modules. */ +export const getIsV4Protocol: Selector = createSelector( + getRobotStateTimeline, + stepFormSelectors.getModuleEntities, + (robotStateTimeline, moduleEntities) => { + const noModules = isEmpty(moduleEntities) + const hasOnlyV3Commands = robotStateTimeline.timeline.every(timelineFrame => + timelineFrame.commands.every(command => _isV3Command(command)) + ) + const isV3 = noModules && hasOnlyV3Commands + return !isV3 + } +) + export const createFile: Selector = createSelector( getFileMetadata, getInitialRobotState, @@ -58,7 +84,7 @@ export const createFile: Selector = createSelector( stepFormSelectors.getPipetteEntities, uiLabwareSelectors.getLabwareNicknamesById, labwareDefSelectors.getLabwareDefsByURI, - featureFlagSelectors.getEnableModules, + getIsV4Protocol, ( fileMetadata, initialRobotState, @@ -73,7 +99,7 @@ export const createFile: Selector = createSelector( pipetteEntities, labwareNicknamesById, labwareDefsByURI, - modulesEnabled + isV4Protocol ) => { const { author, description, created } = fileMetadata const name = fileMetadata.protocolName || 'untitled' @@ -131,9 +157,12 @@ export const createFile: Selector = createSelector( {} ) - return { - schemaVersion: protocolSchemaVersion, + const commands: Array = flatMap( + robotStateTimeline.timeline, + timelineFrame => timelineFrame.commands + ) + const protocolFile = { metadata: { protocolName: name, author, @@ -180,12 +209,23 @@ export const createFile: Selector = createSelector( pipettes, labware, labwareDefinitions, + } - commands: flatten( - robotStateTimeline.timeline.map(timelineFrame => timelineFrame.commands) - ), - - ...(modulesEnabled ? { modules } : {}), + if (isV4Protocol) { + return { + ...protocolFile, + $otSharedSchema: '#/protocol/schemas/4', + schemaVersion: 4, + modules, + commands, + } + } else { + return { + ...protocolFile, + schemaVersion: 3, + // $FlowFixMe: presence of non-v3 commands should make 'isV4Protocol' true + commands, + } } } ) diff --git a/protocol-designer/src/file-types.js b/protocol-designer/src/file-types.js index c70f466bae7..f046f7286fd 100644 --- a/protocol-designer/src/file-types.js +++ b/protocol-designer/src/file-types.js @@ -2,7 +2,8 @@ import type { RootState as IngredRoot } from './labware-ingred/reducers' import type { RootState as StepformRoot } from './step-forms' import type { RootState as DismissRoot } from './dismiss' -import type { ProtocolFile } from '@opentrons/shared-data/protocol/flowTypes/schemaV4' +import type { ProtocolFile as ProtocolFileV3 } from '@opentrons/shared-data/protocol/flowTypes/schemaV3' +import type { ProtocolFile as ProtocolFileV4 } from '@opentrons/shared-data/protocol/flowTypes/schemaV4' export type PDMetadata = { // pipetteId to tiprackModel @@ -24,7 +25,10 @@ export type PDMetadata = { }, } -export type PDProtocolFile = ProtocolFile +// NOTE: PD currently supports saving both v3 and v4, depending on whether it has modules +export type PDProtocolFile = + | ProtocolFileV3 + | ProtocolFileV4 export function getPDMetadata(file: PDProtocolFile): PDMetadata { const metadata = file.designerApplication?.data diff --git a/protocol-designer/src/step-forms/selectors/index.js b/protocol-designer/src/step-forms/selectors/index.js index d0d54557cb0..9988eefa957 100644 --- a/protocol-designer/src/step-forms/selectors/index.js +++ b/protocol-designer/src/step-forms/selectors/index.js @@ -119,11 +119,6 @@ export const getModuleEntities: Selector = createSelector( rs => rs.moduleInvariantProperties ) -export const getIsV4Protocol: Selector = createSelector( - getModuleEntities, - moduleEntities => !isEmpty(moduleEntities) -) - export const getPipetteEntities: Selector = createSelector( state => rootSelector(state).pipetteInvariantProperties, labwareDefSelectors.getLabwareDefsByURI, diff --git a/shared-data/protocol/flowTypes/schemaV4.js b/shared-data/protocol/flowTypes/schemaV4.js index 4abbcb8d94e..d8f0a9a6103 100644 --- a/shared-data/protocol/flowTypes/schemaV4.js +++ b/shared-data/protocol/flowTypes/schemaV4.js @@ -97,9 +97,9 @@ export type Command = // NOTE: must be kept in sync with '../schemas/4.json' export type ProtocolFile = {| ...V3ProtocolFile, + $otSharedSchema: '#/protocol/schemas/4', schemaVersion: 4, - // TODO: Ian 2019-11-11 make modules a required key when PD drops support for v3 - modules?: { + modules: { [moduleId: string]: FileModule, }, commands: Array,