From 74a8a3671721e096ee104c795da6a881d1db0d03 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Fri, 18 Oct 2024 12:22:47 -0400 Subject: [PATCH 1/9] refactor(app): add redux for green checks Add a redux substore for the setup step completion status that we'll use in place of react state. --- .../protocol-runs/__tests__/reducer.test.ts | 76 ++++++++++++++++ app/src/redux/protocol-runs/actions.ts | 18 ++++ app/src/redux/protocol-runs/constants.ts | 18 ++++ app/src/redux/protocol-runs/index.ts | 7 ++ app/src/redux/protocol-runs/reducer.ts | 72 +++++++++++++++ app/src/redux/protocol-runs/selectors.ts | 91 +++++++++++++++++++ app/src/redux/protocol-runs/types.ts | 61 +++++++++++++ app/src/redux/reducer.ts | 4 + app/src/redux/types.ts | 4 + 9 files changed, 351 insertions(+) create mode 100644 app/src/redux/protocol-runs/__tests__/reducer.test.ts create mode 100644 app/src/redux/protocol-runs/actions.ts create mode 100644 app/src/redux/protocol-runs/constants.ts create mode 100644 app/src/redux/protocol-runs/index.ts create mode 100644 app/src/redux/protocol-runs/reducer.ts create mode 100644 app/src/redux/protocol-runs/selectors.ts create mode 100644 app/src/redux/protocol-runs/types.ts diff --git a/app/src/redux/protocol-runs/__tests__/reducer.test.ts b/app/src/redux/protocol-runs/__tests__/reducer.test.ts new file mode 100644 index 00000000000..a9b1662ec16 --- /dev/null +++ b/app/src/redux/protocol-runs/__tests__/reducer.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from 'vitest' + +import { protocolRunReducer } from '../reducer' +import { + updateRunSetupStepsComplete, + updateRunSetupStepsRequired, +} from '../actions' +import * as Constants from '../constants' + +describe('protocol runs reducer', () => { + const INITIAL = { + [Constants.ROBOT_CALIBRATION_STEP_KEY]: { + required: true, + complete: false, + }, + [Constants.MODULE_SETUP_STEP_KEY]: { required: true, complete: false }, + [Constants.LPC_STEP_KEY]: { required: true, complete: false }, + [Constants.LABWARE_SETUP_STEP_KEY]: { + required: true, + complete: false, + }, + [Constants.LIQUID_SETUP_STEP_KEY]: { required: true, complete: false }, + } + it('establishes an empty state if you tell it one', () => { + const nextState = protocolRunReducer( + undefined, + updateRunSetupStepsComplete('some-run-id', {}) + ) + expect(nextState['some-run-id']?.setup).toEqual(INITIAL) + }) + it('updates complete based on an action', () => { + const nextState = protocolRunReducer( + { + ['some-run-id']: { + setup: { + ...INITIAL, + [Constants.LIQUID_SETUP_STEP_KEY]: { + complete: true, + required: true, + }, + }, + }, + }, + updateRunSetupStepsComplete('some-run-id', { + [Constants.LPC_STEP_KEY]: true, + }) + ) + expect(nextState['some-run-id']?.setup).toEqual({ + ...INITIAL, + [Constants.LIQUID_SETUP_STEP_KEY]: { + required: true, + complete: true, + }, + [Constants.LPC_STEP_KEY]: { required: true, complete: true }, + }) + }) + it('updates required based on an action', () => { + const nextState = protocolRunReducer( + { + ['some-run-id']: { + setup: INITIAL, + }, + }, + updateRunSetupStepsRequired('some-run-id', { + [Constants.LIQUID_SETUP_STEP_KEY]: false, + }) + ) + expect(nextState['some-run-id']?.setup).toEqual({ + ...INITIAL, + [Constants.LIQUID_SETUP_STEP_KEY]: { + required: false, + complete: false, + }, + }) + }) +}) diff --git a/app/src/redux/protocol-runs/actions.ts b/app/src/redux/protocol-runs/actions.ts new file mode 100644 index 00000000000..ef806041983 --- /dev/null +++ b/app/src/redux/protocol-runs/actions.ts @@ -0,0 +1,18 @@ +import * as Constants from './constants' +import type * as Types from './types' + +export const updateRunSetupStepsComplete = ( + runId: string, + complete: Types.UpdateRunSetupStepsCompleteAction['payload']['complete'] +): Types.UpdateRunSetupStepsCompleteAction => ({ + type: Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE, + payload: { runId, complete }, +}) + +export const updateRunSetupStepsRequired = ( + runId: string, + required: Types.UpdateRunSetupStepsRequiredAction['payload']['required'] +): Types.UpdateRunSetupStepsRequiredAction => ({ + type: Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED, + payload: { runId, required }, +}) diff --git a/app/src/redux/protocol-runs/constants.ts b/app/src/redux/protocol-runs/constants.ts new file mode 100644 index 00000000000..52a3abc8cd1 --- /dev/null +++ b/app/src/redux/protocol-runs/constants.ts @@ -0,0 +1,18 @@ +export const ROBOT_CALIBRATION_STEP_KEY: 'robot_calibration_step' = + 'robot_calibration_step' +export const MODULE_SETUP_STEP_KEY: 'module_setup_step' = 'module_setup_step' +export const LPC_STEP_KEY: 'labware_position_check_step' = + 'labware_position_check_step' +export const LABWARE_SETUP_STEP_KEY: 'labware_setup_step' = 'labware_setup_step' +export const LIQUID_SETUP_STEP_KEY: 'liquid_setup_step' = 'liquid_setup_step' + +export const SETUP_STEP_KEYS = [ + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, +] as const + +export const UPDATE_RUN_SETUP_STEPS_COMPLETE = 'protocolRuns:UPDATE_RUN_SETUP_STEPS_COMPLETE' as const +export const UPDATE_RUN_SETUP_STEPS_REQUIRED = 'protocolRuns:UPDATE_RUN_SETUP_STEPS_REQUIRED' as const diff --git a/app/src/redux/protocol-runs/index.ts b/app/src/redux/protocol-runs/index.ts new file mode 100644 index 00000000000..9f709c0dbcb --- /dev/null +++ b/app/src/redux/protocol-runs/index.ts @@ -0,0 +1,7 @@ +// runs constants, actions, selectors, and types + +export * from './actions' +export * from './constants' +export * from './selectors' + +export type * from './types' diff --git a/app/src/redux/protocol-runs/reducer.ts b/app/src/redux/protocol-runs/reducer.ts new file mode 100644 index 00000000000..c7915b23efe --- /dev/null +++ b/app/src/redux/protocol-runs/reducer.ts @@ -0,0 +1,72 @@ +import * as Constants from './constants' + +import type { Reducer } from 'redux' +import type { Action } from '../types' + +import type { + ProtocolRunState, + RunSetupStatus, + StepKey, + StepState, +} from './types' + +const INITIAL_STATE: ProtocolRunState = {} + +const INITIAL_SETUP_STEP_STATE = { complete: false, required: true } + +const INITIAL_RUN_SETUP_STATE: RunSetupStatus = { + [Constants.ROBOT_CALIBRATION_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.MODULE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LPC_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LABWARE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LIQUID_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, +} + +export const protocolRunReducer: Reducer = ( + state = INITIAL_STATE, + action +) => { + switch (action.type) { + case Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE: { + return { + ...state, + [action.payload.runId]: { + setup: Constants.SETUP_STEP_KEYS.reduce( + (currentState, step) => ({ + ...currentState, + [step]: { + complete: + action.payload.complete[step] ?? + currentState[step].complete, + required: currentState[step].required, + }, + }), + state[action.payload.runId]?.setup ?? + INITIAL_RUN_SETUP_STATE + ), + }, + } + } + case Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED: { + return { + ...state, + [action.payload.runId]: { + setup: Constants.SETUP_STEP_KEYS.reduce( + (currentState, step) => ({ + ...currentState, + [step]: { + required: + action.payload.required[step] ?? + currentState[step].required, + complete: currentState[step].complete, + }, + }), + state[action.payload.runId]?.setup ?? + INITIAL_RUN_SETUP_STATE + ), + }, + } + } + } + return state +} diff --git a/app/src/redux/protocol-runs/selectors.ts b/app/src/redux/protocol-runs/selectors.ts new file mode 100644 index 00000000000..32c50fe25dd --- /dev/null +++ b/app/src/redux/protocol-runs/selectors.ts @@ -0,0 +1,91 @@ +import type { State } from '../types' +import type * as Types from './types' + +export const getSetupStepComplete: ( + state: State, + runId: string, + step: Types.StepKey +) => boolean | null = (state, runId, step) => + getSetupStepsComplete(state, runId)?.[step] ?? null + +export const getSetupStepsComplete: ( + state: State, + runId: string +) => Types.StepMap | null = (state, runId) => { + const setup = state.protocolRuns[runId]?.setup + if (setup == null) { + return null + } + return (Object.entries(setup) as Array< + [Types.StepKey, Types.StepState] + >).reduce>( + (acc, [step, state]) => ({ + ...acc, + [step]: state.complete, + }), + {} as Types.StepMap + ) +} + +export const getSetupStepRequired: ( + state: State, + runId: string, + step: Types.StepKey +) => boolean | null = (state, runId, step) => + getSetupStepsRequired(state, runId)?.[step] ?? null + +export const getSetupStepsRequired: ( + state: State, + runId: string +) => Types.StepMap | null = (state, runId) => { + const setup = state.protocolRuns[runId]?.setup + if (setup == null) { + return null + } + return (Object.entries(setup) as Array< + [Types.StepKey, Types.StepState] + >).reduce>( + (acc, [step, state]) => ({ ...acc, [step]: state.required }), + {} as Types.StepMap + ) +} + +export const getSetupStepMissing: ( + state: State, + runId: string, + step: Types.StepKey +) => boolean | null = (state, runId, step) => + getSetupStepsMissing(state, runId)?.[step] || null + +export const getSetupStepsMissing: ( + state: State, + runId: string +) => Types.StepMap | null = (state, runId) => { + const setup = state.protocolRuns[runId]?.setup + if (setup == null) { + return null + } + return (Object.entries(setup) as Array< + [Types.StepKey, Types.StepState] + >).reduce>( + (acc, [step, state]) => ({ + ...acc, + [step]: state.required && !state.complete, + }), + {} as Types.StepMap + ) +} + +export const getMissingSetupSteps: ( + state: State, + runId: string +) => Types.StepKey[] = (state, runId) => { + const missingStepMap = getSetupStepsMissing(state, runId) + if (missingStepMap == null) return [] + const missingStepList = (Object.entries(missingStepMap) as Array< + [Types.StepKey, boolean] + >) + .map(([step, missing]) => (missing ? step : null)) + .filter(stepName => stepName != null) + return missingStepList as Types.StepKey[] +} diff --git a/app/src/redux/protocol-runs/types.ts b/app/src/redux/protocol-runs/types.ts new file mode 100644 index 00000000000..7e10cf7fd96 --- /dev/null +++ b/app/src/redux/protocol-runs/types.ts @@ -0,0 +1,61 @@ +import { + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, + UPDATE_RUN_SETUP_STEPS_COMPLETE, + UPDATE_RUN_SETUP_STEPS_REQUIRED, +} from './constants' + +export type RobotCalibrationStepKey = typeof ROBOT_CALIBRATION_STEP_KEY +export type ModuleSetupStepKey = typeof MODULE_SETUP_STEP_KEY +export type LPCStepKey = typeof LPC_STEP_KEY +export type LabwareSetupStepKey = typeof LABWARE_SETUP_STEP_KEY +export type LiquidSetupStepKey = typeof LIQUID_SETUP_STEP_KEY + +export type StepKey = + | RobotCalibrationStepKey + | ModuleSetupStepKey + | LPCStepKey + | LabwareSetupStepKey + | LiquidSetupStepKey + +export interface StepState { + required: boolean + complete: boolean +} + +export type StepMap = { [Step in StepKey]: V } + +export type RunSetupStatus = { + [Step in StepKey]: StepState +} + +export interface PerRunUIState { + setup: RunSetupStatus +} + +export type ProtocolRunState = Partial<{ + readonly [runId: string]: PerRunUIState +}> + +export interface UpdateRunSetupStepsCompleteAction { + type: typeof UPDATE_RUN_SETUP_STEPS_COMPLETE + payload: { + runId: string + complete: Partial<{ [Step in StepKey]: boolean }> + } +} + +export interface UpdateRunSetupStepsRequiredAction { + type: typeof UPDATE_RUN_SETUP_STEPS_REQUIRED + payload: { + runId: string + required: Partial<{ [Step in StepKey]: boolean }> + } +} + +export type ProtocolRunAction = + | UpdateRunSetupStepsCompleteAction + | UpdateRunSetupStepsRequiredAction diff --git a/app/src/redux/reducer.ts b/app/src/redux/reducer.ts index 44831b0d70e..e21dbded781 100644 --- a/app/src/redux/reducer.ts +++ b/app/src/redux/reducer.ts @@ -48,6 +48,9 @@ import { calibrationReducer } from './calibration/reducer' // local protocol storage from file system state import { protocolStorageReducer } from './protocol-storage/reducer' +// local protocol run state +import { protocolRunReducer } from './protocol-runs/reducer' + import type { Reducer } from 'redux' import type { State, Action } from './types' @@ -68,4 +71,5 @@ export const rootReducer: Reducer = combineReducers({ sessions: sessionReducer, calibration: calibrationReducer, protocolStorage: protocolStorageReducer, + protocolRuns: protocolRunReducer, }) diff --git a/app/src/redux/types.ts b/app/src/redux/types.ts index 9ed69c3e71f..d3f502cdc40 100644 --- a/app/src/redux/types.ts +++ b/app/src/redux/types.ts @@ -37,6 +37,8 @@ import type { AlertsState, AlertsAction } from './alerts/types' import type { SessionState, SessionsAction } from './sessions/types' import type { AnalyticsTriggerAction } from './analytics/types' +import type { ProtocolRunState, ProtocolRunAction } from './protocol-runs/types' + export interface State { readonly robotApi: RobotApiState readonly robotAdmin: RobotAdminState @@ -54,6 +56,7 @@ export interface State { readonly sessions: SessionState readonly calibration: CalibrationState readonly protocolStorage: ProtocolStorageState + readonly protocolRuns: ProtocolRunState } export type Action = @@ -78,6 +81,7 @@ export type Action = | CalibrationAction | AnalyticsTriggerAction | AddCustomLabwareFromCreatorAction + | ProtocolRunAction export type GetState = () => State From df38522749e7602fc4ef5ca16766b7ce73330269 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Fri, 18 Oct 2024 15:07:23 -0400 Subject: [PATCH 2/9] ui integration wip --- .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx index 0f48d0bb833..63cc7b9c12b 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -40,6 +40,13 @@ import { useModuleCalibrationStatus, useProtocolAnalysisErrors, } from '/app/resources/runs' +import { + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, +} from '/app/redux/protocol-runs' import { SetupLabware } from './SetupLabware' import { SetupLabwarePositionCheck } from './SetupLabwarePositionCheck' import { SetupRobotCalibration } from './SetupRobotCalibration' @@ -49,18 +56,7 @@ import { SetupLiquids } from './SetupLiquids' import { EmptySetupStep } from './EmptySetupStep' import { HowLPCWorksModal } from './SetupLabwarePositionCheck/HowLPCWorksModal' -const ROBOT_CALIBRATION_STEP_KEY = 'robot_calibration_step' as const -const MODULE_SETUP_KEY = 'module_setup_step' as const -const LPC_KEY = 'labware_position_check_step' as const -const LABWARE_SETUP_KEY = 'labware_setup_step' as const -const LIQUID_SETUP_KEY = 'liquid_setup_step' as const - -export type StepKey = - | typeof ROBOT_CALIBRATION_STEP_KEY - | typeof MODULE_SETUP_KEY - | typeof LPC_KEY - | typeof LABWARE_SETUP_KEY - | typeof LIQUID_SETUP_KEY +import type { StepKey } from '/app/redux/protocol-runs' export type MissingStep = | 'applied_labware_offsets' @@ -134,31 +130,35 @@ export function ProtocolRunSetup({ protocolAnalysis != null ? [ ROBOT_CALIBRATION_STEP_KEY, - MODULE_SETUP_KEY, - LPC_KEY, - LABWARE_SETUP_KEY, - LIQUID_SETUP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, ] - : [ROBOT_CALIBRATION_STEP_KEY, LPC_KEY, LABWARE_SETUP_KEY] + : [ROBOT_CALIBRATION_STEP_KEY, LPC_STEP_KEY, LABWARE_SETUP_STEP_KEY] const targetStepKeyInOrder = stepsKeysInOrder.filter((stepKey: StepKey) => { if (protocolAnalysis == null) { - return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY + return ( + stepKey !== MODULE_SETUP_STEP_KEY && stepKey !== LIQUID_SETUP_STEP_KEY + ) } if ( protocolAnalysis.modules.length === 0 && protocolAnalysis.liquids.length === 0 ) { - return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY + return ( + stepKey !== MODULE_SETUP_STEP_KEY && stepKey !== LIQUID_SETUP_STEP_KEY + ) } if (protocolAnalysis.modules.length === 0) { - return stepKey !== MODULE_SETUP_KEY + return stepKey !== MODULE_SETUP_STEP_KEY } if (protocolAnalysis.liquids.length === 0) { - return stepKey !== LIQUID_SETUP_KEY + return stepKey !== LIQUID_SETUP_STEP_KEY } return true }) @@ -240,11 +240,11 @@ export function ProtocolRunSetup({ incompleteElement: null, }, }, - [MODULE_SETUP_KEY]: { + [MODULE_SETUP_STEP_KEY]: { stepInternals: ( { - setExpandedStepKey(LPC_KEY) + setExpandedStepKey(LPC_STEP_KEY) }} robotName={robotName} runId={runId} @@ -256,7 +256,7 @@ export function ProtocolRunSetup({ ? flexDeckHardwareDescription : ot2DeckHardwareDescription, rightElProps: { - stepKey: MODULE_SETUP_KEY, + stepKey: MODULE_SETUP_STEP_KEY, complete: calibrationStatusModules.complete && !isMissingModule && @@ -272,14 +272,14 @@ export function ProtocolRunSetup({ incompleteElement: null, }, }, - [LPC_KEY]: { + [LPC_STEP_KEY]: { stepInternals: ( { setLpcComplete(confirmed) if (confirmed) { - setExpandedStepKey(LABWARE_SETUP_KEY) + setExpandedStepKey(LABWARE_SETUP_STEP_KEY) setMissingSteps( missingSteps.filter(step => step !== 'applied_labware_offsets') ) @@ -290,14 +290,14 @@ export function ProtocolRunSetup({ ), description: t('labware_position_check_step_description'), rightElProps: { - stepKey: LPC_KEY, + stepKey: LPC_STEP_KEY, complete: lpcComplete, completeText: t('offsets_ready'), incompleteText: null, incompleteElement: , }, }, - [LABWARE_SETUP_KEY]: { + [LABWARE_SETUP_STEP_KEY]: { stepInternals: ( step !== 'labware_placement') ) const nextStep = - targetStepKeyInOrder.findIndex(v => v === LABWARE_SETUP_KEY) === + targetStepKeyInOrder.findIndex( + v => v === LABWARE_SETUP_STEP_KEY + ) === targetStepKeyInOrder.length - 1 ? null - : LIQUID_SETUP_KEY + : LIQUID_SETUP_STEP_KEY setExpandedStepKey(nextStep) } }} /> ), - description: t(`${LABWARE_SETUP_KEY}_description`), + description: t(`${LABWARE_SETUP_STEP_KEY}_description`), rightElProps: { - stepKey: LABWARE_SETUP_KEY, + stepKey: LABWARE_SETUP_STEP_KEY, complete: labwareSetupComplete, completeText: t('placements_ready'), incompleteText: null, incompleteElement: null, }, }, - [LIQUID_SETUP_KEY]: { + [LIQUID_SETUP_STEP_KEY]: { stepInternals: ( ), description: hasLiquids - ? t(`${LIQUID_SETUP_KEY}_description`) + ? t(`${LIQUID_SETUP_STEP_KEY}_description`) : i18n.format(t('liquids_not_in_the_protocol'), 'capitalize'), rightElProps: { - stepKey: LIQUID_SETUP_KEY, + stepKey: LIQUID_SETUP_STEP_KEY, complete: liquidSetupComplete, completeText: t('liquids_ready'), incompleteText: null, @@ -431,7 +433,7 @@ export function ProtocolRunSetup({ interface NoHardwareRequiredStepCompletion { stepKey: Exclude< StepKey, - typeof ROBOT_CALIBRATION_STEP_KEY | typeof MODULE_SETUP_KEY + typeof ROBOT_CALIBRATION_STEP_KEY | typeof MODULE_SETUP_STEP_KEY > complete: boolean incompleteText: string | null @@ -440,7 +442,7 @@ interface NoHardwareRequiredStepCompletion { } interface HardwareRequiredStepCompletion { - stepKey: typeof ROBOT_CALIBRATION_STEP_KEY | typeof MODULE_SETUP_KEY + stepKey: typeof ROBOT_CALIBRATION_STEP_KEY | typeof MODULE_SETUP_STEP_KEY complete: boolean missingHardware: boolean incompleteText: string | null @@ -457,7 +459,7 @@ const stepRequiresHW = ( props: StepRightElementProps ): props is HardwareRequiredStepCompletion => props.stepKey === ROBOT_CALIBRATION_STEP_KEY || - props.stepKey === MODULE_SETUP_KEY + props.stepKey === MODULE_SETUP_STEP_KEY function StepRightElement(props: StepRightElementProps): JSX.Element | null { if (props.complete) { From 400e59cebd00515d4446c81286f0c033a36d8e0f Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 21 Oct 2024 11:07:31 -0400 Subject: [PATCH 3/9] new hook --- .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 54 ++------- .../__tests__/ProtocolRunSetup.test.tsx | 20 ++++ app/src/redux-resources/runs/hooks/index.ts | 1 + .../hooks/useRequiredSetupStepsInOrder.ts | 107 ++++++++++++++++++ app/src/redux-resources/runs/index.ts | 1 + app/src/redux/protocol-runs/constants.ts | 14 +-- 6 files changed, 147 insertions(+), 50 deletions(-) create mode 100644 app/src/redux-resources/runs/hooks/index.ts create mode 100644 app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts create mode 100644 app/src/redux-resources/runs/index.ts diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx index 63cc7b9c12b..ca7aaf4a41d 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -30,6 +30,7 @@ import { } from '/app/resources/deck_configuration/utils' import { useDeckConfigurationCompatibility } from '/app/resources/deck_configuration/hooks' import { useRobot, useIsFlex } from '/app/redux-resources/robots' +import { useRequiredSetupStepsInOrder } from '/app/redux-resources/runs' import { useStoredProtocolAnalysis } from '/app/resources/analysis' import { useMostRecentCompletedAnalysis, @@ -90,6 +91,10 @@ export function ProtocolRunSetup({ const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis + const { + orderedSteps, + orderedApplicableSteps, + } = useRequiredSetupStepsInOrder({ runId, protocolAnalysis }) const modules = parseAllRequiredModuleModels(protocolAnalysis?.commands ?? []) const robot = useRobot(robotName) @@ -126,43 +131,6 @@ export function ProtocolRunSetup({ const isMissingModule = missingModuleIds.length > 0 - const stepsKeysInOrder = - protocolAnalysis != null - ? [ - ROBOT_CALIBRATION_STEP_KEY, - MODULE_SETUP_STEP_KEY, - LPC_STEP_KEY, - LABWARE_SETUP_STEP_KEY, - LIQUID_SETUP_STEP_KEY, - ] - : [ROBOT_CALIBRATION_STEP_KEY, LPC_STEP_KEY, LABWARE_SETUP_STEP_KEY] - - const targetStepKeyInOrder = stepsKeysInOrder.filter((stepKey: StepKey) => { - if (protocolAnalysis == null) { - return ( - stepKey !== MODULE_SETUP_STEP_KEY && stepKey !== LIQUID_SETUP_STEP_KEY - ) - } - - if ( - protocolAnalysis.modules.length === 0 && - protocolAnalysis.liquids.length === 0 - ) { - return ( - stepKey !== MODULE_SETUP_STEP_KEY && stepKey !== LIQUID_SETUP_STEP_KEY - ) - } - - if (protocolAnalysis.modules.length === 0) { - return stepKey !== MODULE_SETUP_STEP_KEY - } - - if (protocolAnalysis.liquids.length === 0) { - return stepKey !== LIQUID_SETUP_STEP_KEY - } - return true - }) - const liquids = protocolAnalysis?.liquids ?? [] const hasLiquids = liquids.length > 0 const hasModules = protocolAnalysis != null && modules.length > 0 @@ -216,8 +184,8 @@ export function ProtocolRunSetup({ robotName={robotName} runId={runId} nextStep={ - targetStepKeyInOrder[ - targetStepKeyInOrder.findIndex( + orderedApplicableSteps[ + orderedApplicableSteps.findIndex( v => v === ROBOT_CALIBRATION_STEP_KEY ) + 1 ] @@ -310,10 +278,10 @@ export function ProtocolRunSetup({ missingSteps.filter(step => step !== 'labware_placement') ) const nextStep = - targetStepKeyInOrder.findIndex( + orderedApplicableSteps.findIndex( v => v === LABWARE_SETUP_STEP_KEY ) === - targetStepKeyInOrder.length - 1 + orderedApplicableSteps.length - 1 ? null : LIQUID_SETUP_STEP_KEY setExpandedStepKey(nextStep) @@ -375,7 +343,7 @@ export function ProtocolRunSetup({ {t('protocol_analysis_failed')} ) : ( - stepsKeysInOrder.map((stepKey, index) => { + orderedSteps.map((stepKey, index) => { const setupStepTitle = t(`${stepKey}_title`) const showEmptySetupStep = (stepKey === 'liquid_setup_step' && !hasLiquids) || @@ -413,7 +381,7 @@ export function ProtocolRunSetup({ {StepDetailMap[stepKey].stepInternals} )} - {index !== stepsKeysInOrder.length - 1 ? ( + {index !== orderedSteps.length - 1 ? ( ) : null} diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 64aa0d094ae..a1c26a896d5 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -30,6 +30,7 @@ import { } from '/app/resources/runs' import { useDeckConfigurationCompatibility } from '/app/resources/deck_configuration/hooks' import { useRobot, useIsFlex } from '/app/redux-resources/robots' +import { useRequiredSetupStepsInOrder } from '/app/redux-resources/runs' import { useStoredProtocolAnalysis } from '/app/resources/analysis' import { SetupLabware } from '../SetupLabware' @@ -38,6 +39,8 @@ import { SetupLiquids } from '../SetupLiquids' import { SetupModuleAndDeck } from '../SetupModuleAndDeck' import { EmptySetupStep } from '../EmptySetupStep' import { ProtocolRunSetup } from '../ProtocolRunSetup' +import * as ReduxRuns from '/app/redux/protocol-runs' + import type { MissingSteps } from '../ProtocolRunSetup' import type * as SharedData from '@opentrons/shared-data' @@ -59,6 +62,7 @@ vi.mock('/app/redux/config') vi.mock('/app/resources/deck_configuration/utils') vi.mock('/app/resources/deck_configuration/hooks') vi.mock('/app/redux-resources/robots') +vi.mock('/app/redux-resources/runs') vi.mock('/app/resources/analysis') vi.mock('@opentrons/shared-data', async importOriginal => { const actualSharedData = await importOriginal() @@ -112,6 +116,15 @@ describe('ProtocolRunSetup', () => { ...noModulesProtocol, ...MOCK_PROTOCOL_LIQUID_KEY, } as unknown) as SharedData.ProtocolAnalysisOutput) + when(vi.mocked(useRequiredSetupStepsInOrder)) + .calledWith({ + runId: RUN_ID, + protocolAnalysis: { ...noModulesProtocol, ...MOCK_PROTOCOL_LIQUID_KEY }, + }) + .thenReturn({ + orderedSteps: ReduxRuns.SETUP_STEP_KEYS, + orderedApplicableSteps: ReduxRuns.SETUP_STEP_KEYS, + }) vi.mocked(parseAllRequiredModuleModels).mockReturnValue([]) vi.mocked(parseLiquidsInLoadOrder).mockReturnValue([]) when(vi.mocked(useRobot)) @@ -179,6 +192,13 @@ describe('ProtocolRunSetup', () => { when(vi.mocked(useStoredProtocolAnalysis)) .calledWith(RUN_ID) .thenReturn(null) + when(vi.mocked(useRequiredSetupStepsInOrder)) + .calledWith({ runId: RUN_ID, protocolAnalysis: null }) + .thenReturn([ + ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, + ReduxRuns.LPC_STEP_KEY, + ReduxRuns.LABWARE_SETUP_STEP_KEY, + ]) render() screen.getByText('Loading data...') }) diff --git a/app/src/redux-resources/runs/hooks/index.ts b/app/src/redux-resources/runs/hooks/index.ts new file mode 100644 index 00000000000..7427ca864da --- /dev/null +++ b/app/src/redux-resources/runs/hooks/index.ts @@ -0,0 +1 @@ +export * from './useRequiredSetupStepsInOrder' diff --git a/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts b/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts new file mode 100644 index 00000000000..314829c0892 --- /dev/null +++ b/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts @@ -0,0 +1,107 @@ +import { useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { + updateRunSetupStepsRequired, + getSetupStepsRequired, + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, +} from '/app/redux/protocol-runs' + +import type { + StepKey, + StepMap, + UpdateRunSetupStepsRequiredAction, +} from '/app/redux/protocol-runs' +import type { Dispatch, State } from '/app/redux/types' +import type { + CompletedProtocolAnalysis, + ProtocolAnalysisOutput, +} from '@opentrons/shared-data' + +export interface UseRequiredSetupStepsInOrderProps { + runId: string + protocolAnalysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null +} + +export interface UseRequiredSetupStepsInOrderReturn { + orderedSteps: StepKey[] + orderedApplicableSteps: StepKey[] +} + +const ALL_STEPS_IN_ORDER = [ + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, +] as const + +const NO_ANALYSIS_STEPS_IN_ORDER = [ + ROBOT_CALIBRATION_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, +] + +const keysInOrder = ( + protocolAnalysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null +): UseRequiredSetupStepsInOrderReturn => { + const orderedSteps = + protocolAnalysis == null ? NO_ANALYSIS_STEPS_IN_ORDER : ALL_STEPS_IN_ORDER + + const orderedApplicableSteps = + protocolAnalysis == null + ? NO_ANALYSIS_STEPS_IN_ORDER + : ALL_STEPS_IN_ORDER.filter((stepKey: StepKey) => { + if (protocolAnalysis.modules.length === 0) { + return stepKey !== MODULE_SETUP_STEP_KEY + } + + if (protocolAnalysis.liquids.length === 0) { + return stepKey !== LIQUID_SETUP_STEP_KEY + } + return true + }) + return { orderedSteps: orderedSteps as StepKey[], orderedApplicableSteps } +} + +export function useRequiredSetupStepsInOrder({ + runId, + protocolAnalysis, +}: UseRequiredSetupStepsInOrderProps) { + const dispatch = useDispatch() + const requiredSteps = useSelector(state => + getSetupStepsRequired(state, runId) + ) + + useEffect(() => { + const applicable = keysInOrder(protocolAnalysis) + dispatch( + updateRunSetupStepsRequired(runId, { + ...ALL_STEPS_IN_ORDER.reduce< + UpdateRunSetupStepsRequiredAction['payload']['required'] + >( + (acc, thiskey) => ({ + ...acc, + [thiskey]: applicable.orderedApplicableSteps.includes(thiskey), + }), + {} + ), + }) + ) + }, [runId, protocolAnalysis, dispatch]) + return protocolAnalysis == null + ? { + orderedSteps: NO_ANALYSIS_STEPS_IN_ORDER, + orderedApplicableSteps: NO_ANALYSIS_STEPS_IN_ORDER, + } + : { + orderedSteps: ALL_STEPS_IN_ORDER, + orderedApplicableSteps: ALL_STEPS_IN_ORDER.filter( + step => (requiredSteps as Required> | null)?.[step] + ), + } +} diff --git a/app/src/redux-resources/runs/index.ts b/app/src/redux-resources/runs/index.ts new file mode 100644 index 00000000000..fc78d35129c --- /dev/null +++ b/app/src/redux-resources/runs/index.ts @@ -0,0 +1 @@ +export * from './hooks' diff --git a/app/src/redux/protocol-runs/constants.ts b/app/src/redux/protocol-runs/constants.ts index 52a3abc8cd1..04f28f760d3 100644 --- a/app/src/redux/protocol-runs/constants.ts +++ b/app/src/redux/protocol-runs/constants.ts @@ -1,17 +1,17 @@ export const ROBOT_CALIBRATION_STEP_KEY: 'robot_calibration_step' = - 'robot_calibration_step' + 'robot_calibration_step' export const MODULE_SETUP_STEP_KEY: 'module_setup_step' = 'module_setup_step' export const LPC_STEP_KEY: 'labware_position_check_step' = - 'labware_position_check_step' + 'labware_position_check_step' export const LABWARE_SETUP_STEP_KEY: 'labware_setup_step' = 'labware_setup_step' export const LIQUID_SETUP_STEP_KEY: 'liquid_setup_step' = 'liquid_setup_step' export const SETUP_STEP_KEYS = [ - ROBOT_CALIBRATION_STEP_KEY, - MODULE_SETUP_STEP_KEY, - LPC_STEP_KEY, - LABWARE_SETUP_STEP_KEY, - LIQUID_SETUP_STEP_KEY, + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, ] as const export const UPDATE_RUN_SETUP_STEPS_COMPLETE = 'protocolRuns:UPDATE_RUN_SETUP_STEPS_COMPLETE' as const From 2b94028cfaad383d6271621b7da81ced077a3e0c Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 21 Oct 2024 12:21:45 -0400 Subject: [PATCH 4/9] use for missing steps --- .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 68 +++++++------------ 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx index ca7aaf4a41d..ff44af72b41 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' import { ALIGN_CENTER, @@ -47,6 +48,8 @@ import { LPC_STEP_KEY, LABWARE_SETUP_STEP_KEY, LIQUID_SETUP_STEP_KEY, + updateRunSetupStepsComplete, + getMissingSetupSteps, } from '/app/redux/protocol-runs' import { SetupLabware } from './SetupLabware' import { SetupLabwarePositionCheck } from './SetupLabwarePositionCheck' @@ -57,37 +60,30 @@ import { SetupLiquids } from './SetupLiquids' import { EmptySetupStep } from './EmptySetupStep' import { HowLPCWorksModal } from './SetupLabwarePositionCheck/HowLPCWorksModal' +import type { Dispatch, State } from '/app/redux' import type { StepKey } from '/app/redux/protocol-runs' -export type MissingStep = - | 'applied_labware_offsets' - | 'labware_placement' - | 'liquids' - -export type MissingSteps = MissingStep[] - -export const initialMissingSteps = (): MissingSteps => [ - 'applied_labware_offsets', - 'labware_placement', - 'liquids', -] +const STEP_KEY_TO_I18N_KEY = { + LPC_STEP_KEY: 'applied_labware_offsets', + LABWARE_SETUP_STEP_KEY: 'labware_placement', + LIQUID_SETUP_STEP_KEY: 'liquids', + MODULE_SETUP_STEP_KEY: 'module_setup', + ROBOT_CALIBRATION_STEP_KEY: 'robot_calibrtion', +} interface ProtocolRunSetupProps { protocolRunHeaderRef: React.RefObject | null robotName: string runId: string - setMissingSteps: (missingSteps: MissingSteps) => void - missingSteps: MissingSteps } export function ProtocolRunSetup({ protocolRunHeaderRef, robotName, runId, - setMissingSteps, - missingSteps, }: ProtocolRunSetupProps): JSX.Element | null { const { t, i18n } = useTranslation('protocol_setup') + const dispatch = useDispatch() const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis @@ -147,26 +143,8 @@ export function ProtocolRunSetup({ ? t('install_modules', { count: modules.length }) : t('no_deck_hardware_specified') - const [ - labwareSetupComplete, - setLabwareSetupComplete, - ] = React.useState(false) - const [liquidSetupComplete, setLiquidSetupComplete] = React.useState( - false - ) - React.useEffect(() => { - if ((robotProtocolAnalysis || storedProtocolAnalysis) && !hasLiquids) { - setLiquidSetupComplete(true) - } - }, [robotProtocolAnalysis, storedProtocolAnalysis, hasLiquids]) - if ( - !hasLiquids && - protocolAnalysis != null && - missingSteps.includes('liquids') - ) { - setMissingSteps(missingSteps.filter(step => step !== 'liquids')) - } - const [lpcComplete, setLpcComplete] = React.useState(false) + const missingSteps = useSelector(state => getMissingSetupSteps(state, runId)) + if (robot == null) { return null } @@ -245,21 +223,20 @@ export function ProtocolRunSetup({ { - setLpcComplete(confirmed) + dispatch( + updateRunSetupStepsComplete(runId, { [LPC_STEP_KEY]: true }) + ) if (confirmed) { setExpandedStepKey(LABWARE_SETUP_STEP_KEY) - setMissingSteps( - missingSteps.filter(step => step !== 'applied_labware_offsets') - ) } }} - offsetsConfirmed={lpcComplete} + offsetsConfirmed={!missingSteps.includes(LPC_STEP_KEY)} /> ), description: t('labware_position_check_step_description'), rightElProps: { stepKey: LPC_STEP_KEY, - complete: lpcComplete, + complete: !missingSteps.includes(LPC_STEP_KEY), completeText: t('offsets_ready'), incompleteText: null, incompleteElement: , @@ -270,8 +247,13 @@ export function ProtocolRunSetup({ { + dispatch( + updateRunSetupStepsComplete(runId, { + [LABWARE_SETUP_STEP_KEY]: true, + }) + ) setLabwareSetupComplete(confirmed) if (confirmed) { setMissingSteps( From d1ac7871a5bd8353942fe3a29267c407664da1c4 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 22 Oct 2024 14:47:39 -0400 Subject: [PATCH 5/9] typings and such --- .../hooks/useActionButtonProperties.ts | 8 +- .../hooks/useMissingStepsModal.ts | 12 +- .../modals/ConfirmMissingStepsModal.tsx | 20 ++- .../useRunHeaderModalContainer.ts | 3 +- .../__tests__/ProtocolRunHeader.test.tsx | 2 +- .../ProtocolRun/ProtocolRunHeader/index.tsx | 1 - .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 27 ++-- .../ProtocolRun/SetupRobotCalibration.tsx | 2 +- .../__tests__/ProtocolRunSetup.test.tsx | 33 +++-- .../Devices/ProtocolRunDetails/index.tsx | 12 +- .../protocol-runs/__tests__/reducer.test.ts | 124 ++++++++--------- app/src/redux/protocol-runs/actions.ts | 16 +-- app/src/redux/protocol-runs/reducer.ts | 102 +++++++------- app/src/redux/protocol-runs/selectors.ts | 128 +++++++++--------- app/src/redux/protocol-runs/types.ts | 58 ++++---- 15 files changed, 282 insertions(+), 266 deletions(-) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts index cd16d2467b6..4c5486eb6e7 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderContent/ActionButton/hooks/useActionButtonProperties.ts @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' +import { useSelector } from 'react-redux' import { RUN_STATUS_IDLE, @@ -14,6 +15,7 @@ import { useTrackEvent, } from '/app/redux/analytics' import { useTrackProtocolRunEvent } from '/app/redux-resources/analytics' +import { getMissingSetupSteps } from '/app/redux/protocol-runs' import { useIsHeaterShakerInProtocol } from '/app/organisms/ModuleCard/hooks' import { isAnyHeaterShakerShaking } from '../../../RunHeaderModalContainer/modals' import { @@ -24,6 +26,8 @@ import { import type { IconName } from '@opentrons/components' import type { BaseActionButtonProps } from '..' +import type { State } from '/app/redux/types' +import type { StepKey } from '/app/redux/protocol-runs' interface UseButtonPropertiesProps extends BaseActionButtonProps { isProtocolNotReady: boolean @@ -42,7 +46,6 @@ interface UseButtonPropertiesProps extends BaseActionButtonProps { export function useActionButtonProperties({ isProtocolNotReady, runStatus, - missingSetupSteps, robotName, runId, confirmAttachment, @@ -66,6 +69,9 @@ export function useActionButtonProperties({ const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() const isHeaterShakerShaking = isAnyHeaterShakerShaking(attachedModules) const trackEvent = useTrackEvent() + const missingSetupSteps = useSelector((state: State) => + getMissingSetupSteps(state, runId) + ) let buttonText = '' let handleButtonClick = (): void => {} diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts index 4bf28bc049f..d0506c55153 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useMissingStepsModal.ts @@ -1,17 +1,21 @@ +import { useSelector } from 'react-redux' import { RUN_STATUS_IDLE, RUN_STATUS_STOPPED } from '@opentrons/api-client' import { useConditionalConfirm } from '@opentrons/components' import { useIsHeaterShakerInProtocol } from '/app/organisms/ModuleCard/hooks' import { isAnyHeaterShakerShaking } from '../modals' +import { getMissingSetupSteps } from '/app/redux/protocol-runs' import type { UseConditionalConfirmResult } from '@opentrons/components' import type { RunStatus, AttachedModule } from '@opentrons/api-client' import type { ConfirmMissingStepsModalProps } from '../modals' +import type { State } from '/app/redux/types' +import type { StepKey } from '/app/redux/protocol-runs' interface UseMissingStepsModalProps { runStatus: RunStatus | null attachedModules: AttachedModule[] - missingSetupSteps: string[] + runId: string handleProceedToRunClick: () => void } @@ -30,12 +34,14 @@ export type UseMissingStepsModalResult = export function useMissingStepsModal({ attachedModules, runStatus, - missingSetupSteps, + runId, handleProceedToRunClick, }: UseMissingStepsModalProps): UseMissingStepsModalResult { const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() const isHeaterShakerShaking = isAnyHeaterShakerShaking(attachedModules) - + const missingSetupSteps = useSelector((state: State) => + getMissingSetupSteps(state, runId) + ) const shouldShowHSConfirm = isHeaterShakerInProtocol && !isHeaterShakerShaking && diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx index 978efdbab48..8203e126a2d 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmMissingStepsModal.tsx @@ -12,11 +12,27 @@ import { TYPOGRAPHY, Modal, } from '@opentrons/components' +import { + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, + MODULE_SETUP_STEP_KEY, + ROBOT_CALIBRATION_STEP_KEY, +} from '/app/redux/protocol-runs' +import type { StepKey } from '/app/redux/protocol-runs' + +const STEP_KEY_TO_I18N_KEY = { + [LPC_STEP_KEY]: 'applied_labware_offsets', + [LABWARE_SETUP_STEP_KEY]: 'labware_placement', + [LIQUID_SETUP_STEP_KEY]: 'liquids', + [MODULE_SETUP_STEP_KEY]: 'module_setup', + [ROBOT_CALIBRATION_STEP_KEY]: 'robot_calibration', +} export interface ConfirmMissingStepsModalProps { onCloseClick: () => void onConfirmClick: () => void - missingSteps: string[] + missingSteps: StepKey[] } export const ConfirmMissingStepsModal = ( props: ConfirmMissingStepsModalProps @@ -41,7 +57,7 @@ export const ConfirmMissingStepsModal = ( missingSteps: new Intl.ListFormat('en', { style: 'short', type: 'conjunction', - }).format(missingSteps.map(step => t(step))), + }).format(missingSteps.map(step => t(STEP_KEY_TO_I18N_KEY[step]))), })} diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts index 17d81c1f18e..48eda0ebfa5 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/useRunHeaderModalContainer.ts @@ -62,7 +62,6 @@ export function useRunHeaderModalContainer({ runStatus, runRecord, attachedModules, - missingSetupSteps, protocolRunControls, runErrors, }: UseRunHeaderModalContainerProps): UseRunHeaderModalContainerResult { @@ -102,7 +101,7 @@ export function useRunHeaderModalContainer({ const missingStepsModalUtils = useMissingStepsModal({ attachedModules, runStatus, - missingSetupSteps, + runId, handleProceedToRunClick, }) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx index 9cc357d0565..e82d58cb75e 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/__tests__/ProtocolRunHeader.test.tsx @@ -30,6 +30,7 @@ vi.mock('react-router-dom') vi.mock('@opentrons/react-api-client') vi.mock('/app/redux-resources/robots') vi.mock('/app/resources/runs') +vi.mock('/app/redux/protocol-runs') vi.mock('../RunHeaderModalContainer') vi.mock('../RunHeaderBannerContainer') vi.mock('../RunHeaderContent') @@ -51,7 +52,6 @@ describe('ProtocolRunHeader', () => { robotName: MOCK_ROBOT, runId: MOCK_RUN_ID, makeHandleJumpToStep: vi.fn(), - missingSetupSteps: [], } vi.mocked(useNavigate).mockReturnValue(mockNavigate) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx index b9641fcc96b..493a93aa85e 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx @@ -35,7 +35,6 @@ export interface ProtocolRunHeaderProps { robotName: string runId: string makeHandleJumpToStep: (index: number) => () => void - missingSetupSteps: string[] } export function ProtocolRunHeader( diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx index ff44af72b41..6a5f94a2db0 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -60,7 +60,7 @@ import { SetupLiquids } from './SetupLiquids' import { EmptySetupStep } from './EmptySetupStep' import { HowLPCWorksModal } from './SetupLabwarePositionCheck/HowLPCWorksModal' -import type { Dispatch, State } from '/app/redux' +import type { Dispatch, State } from '/app/redux/types' import type { StepKey } from '/app/redux/protocol-runs' const STEP_KEY_TO_I18N_KEY = { @@ -143,7 +143,9 @@ export function ProtocolRunSetup({ ? t('install_modules', { count: modules.length }) : t('no_deck_hardware_specified') - const missingSteps = useSelector(state => getMissingSetupSteps(state, runId)) + const missingSteps = useSelector( + (state: State): StepKey[] => getMissingSetupSteps(state, runId) + ) if (robot == null) { return null @@ -224,7 +226,7 @@ export function ProtocolRunSetup({ {...{ runId, robotName }} setOffsetsConfirmed={confirmed => { dispatch( - updateRunSetupStepsComplete(runId, { [LPC_STEP_KEY]: true }) + updateRunSetupStepsComplete(runId, { [LPC_STEP_KEY]: confirmed }) ) if (confirmed) { setExpandedStepKey(LABWARE_SETUP_STEP_KEY) @@ -251,14 +253,10 @@ export function ProtocolRunSetup({ setLabwareConfirmed={(confirmed: boolean) => { dispatch( updateRunSetupStepsComplete(runId, { - [LABWARE_SETUP_STEP_KEY]: true, + [LABWARE_SETUP_STEP_KEY]: confirmed, }) ) - setLabwareSetupComplete(confirmed) if (confirmed) { - setMissingSteps( - missingSteps.filter(step => step !== 'labware_placement') - ) const nextStep = orderedApplicableSteps.findIndex( v => v === LABWARE_SETUP_STEP_KEY @@ -274,7 +272,7 @@ export function ProtocolRunSetup({ description: t(`${LABWARE_SETUP_STEP_KEY}_description`), rightElProps: { stepKey: LABWARE_SETUP_STEP_KEY, - complete: labwareSetupComplete, + complete: !missingSteps.includes(LABWARE_SETUP_STEP_KEY), completeText: t('placements_ready'), incompleteText: null, incompleteElement: null, @@ -286,11 +284,14 @@ export function ProtocolRunSetup({ robotName={robotName} runId={runId} protocolAnalysis={protocolAnalysis} - isLiquidSetupConfirmed={liquidSetupComplete} + isLiquidSetupConfirmed={!missingSteps.includes(LIQUID_SETUP_STEP_KEY)} setLiquidSetupConfirmed={(confirmed: boolean) => { - setLiquidSetupComplete(confirmed) + dispatch( + updateRunSetupStepsComplete(runId, { + [LIQUID_SETUP_STEP_KEY]: confirmed, + }) + ) if (confirmed) { - setMissingSteps(missingSteps.filter(step => step !== 'liquids')) setExpandedStepKey(null) } }} @@ -301,7 +302,7 @@ export function ProtocolRunSetup({ : i18n.format(t('liquids_not_in_the_protocol'), 'capitalize'), rightElProps: { stepKey: LIQUID_SETUP_STEP_KEY, - complete: liquidSetupComplete, + complete: !missingSteps.includes(LIQUID_SETUP_STEP_KEY), completeText: t('liquids_ready'), incompleteText: null, incompleteElement: null, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupRobotCalibration.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupRobotCalibration.tsx index 90745500149..5202419e290 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupRobotCalibration.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupRobotCalibration.tsx @@ -23,7 +23,7 @@ import { RUN_STATUS_STOPPED } from '@opentrons/api-client' import { useIsFlex } from '/app/redux-resources/robots' import type { ProtocolCalibrationStatus } from '/app/redux/calibration/types' -import type { StepKey } from './ProtocolRunSetup' +import type { StepKey } from '/app/redux/protocol-runs' interface SetupRobotCalibrationProps { robotName: string diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index a1c26a896d5..70cbaa74791 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -41,7 +41,7 @@ import { EmptySetupStep } from '../EmptySetupStep' import { ProtocolRunSetup } from '../ProtocolRunSetup' import * as ReduxRuns from '/app/redux/protocol-runs' -import type { MissingSteps } from '../ProtocolRunSetup' +import type { StepKey } from '/app/redux/protocol-runs' import type * as SharedData from '@opentrons/shared-data' @@ -78,18 +78,12 @@ vi.mock('@opentrons/shared-data', async importOriginal => { const ROBOT_NAME = 'otie' const RUN_ID = '1' const MOCK_PROTOCOL_LIQUID_KEY = { liquids: [] } -let mockMissingSteps: MissingSteps = [] -const mockSetMissingSteps = vi.fn((missingSteps: MissingSteps) => { - mockMissingSteps = missingSteps -}) const render = () => { return renderWithProviders( , { i18nInstance: i18n, @@ -99,7 +93,6 @@ const render = () => { describe('ProtocolRunSetup', () => { beforeEach(() => { - mockMissingSteps = [] when(vi.mocked(useIsFlex)).calledWith(ROBOT_NAME).thenReturn(false) when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) @@ -119,11 +112,14 @@ describe('ProtocolRunSetup', () => { when(vi.mocked(useRequiredSetupStepsInOrder)) .calledWith({ runId: RUN_ID, - protocolAnalysis: { ...noModulesProtocol, ...MOCK_PROTOCOL_LIQUID_KEY }, + protocolAnalysis: ({ + ...noModulesProtocol, + ...MOCK_PROTOCOL_LIQUID_KEY, + } as any) as SharedData.CompletedProtocolAnalysis, }) .thenReturn({ orderedSteps: ReduxRuns.SETUP_STEP_KEYS, - orderedApplicableSteps: ReduxRuns.SETUP_STEP_KEYS, + orderedApplicableSteps: (ReduxRuns.SETUP_STEP_KEYS as any) as StepKey[], }) vi.mocked(parseAllRequiredModuleModels).mockReturnValue([]) vi.mocked(parseLiquidsInLoadOrder).mockReturnValue([]) @@ -194,11 +190,18 @@ describe('ProtocolRunSetup', () => { .thenReturn(null) when(vi.mocked(useRequiredSetupStepsInOrder)) .calledWith({ runId: RUN_ID, protocolAnalysis: null }) - .thenReturn([ - ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, - ReduxRuns.LPC_STEP_KEY, - ReduxRuns.LABWARE_SETUP_STEP_KEY, - ]) + .thenReturn({ + orderedSteps: [ + ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, + ReduxRuns.LPC_STEP_KEY, + ReduxRuns.LABWARE_SETUP_STEP_KEY, + ], + orderedApplicableSteps: [ + ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, + ReduxRuns.LPC_STEP_KEY, + ReduxRuns.LABWARE_SETUP_STEP_KEY, + ], + }) render() screen.getByText('Loading data...') }) diff --git a/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx b/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx index 7ea4ceae7c6..4abb609c4fc 100644 --- a/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx +++ b/app/src/pages/Desktop/Devices/ProtocolRunDetails/index.tsx @@ -25,10 +25,7 @@ import { ApiHostProvider } from '@opentrons/react-api-client' import { useSyncRobotClock } from '/app/organisms/Desktop/Devices/hooks' import { ProtocolRunHeader } from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader' import { RunPreview } from '/app/organisms/Desktop/Devices/RunPreview' -import { - ProtocolRunSetup, - initialMissingSteps, -} from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup' +import { ProtocolRunSetup } from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup' import { BackToTopButton } from '/app/organisms/Desktop/Devices/ProtocolRun/BackToTopButton' import { ProtocolRunModuleControls } from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunModuleControls' import { ProtocolRunRuntimeParameters } from '/app/organisms/Desktop/Devices/ProtocolRun/ProtocolRunRunTimeParameters' @@ -187,10 +184,6 @@ function PageContents(props: PageContentsProps): JSX.Element { } }, [jumpedIndex]) - const [missingSteps, setMissingSteps] = useState< - ReturnType - >(initialMissingSteps()) - const makeHandleScrollToStep = (i: number) => () => { listRef.current?.scrollToIndex(i, true, -1 * JUMP_OFFSET_FROM_TOP_PX) } @@ -210,8 +203,6 @@ function PageContents(props: PageContentsProps): JSX.Element { protocolRunHeaderRef={protocolRunHeaderRef} robotName={robotName} runId={runId} - setMissingSteps={setMissingSteps} - missingSteps={missingSteps} /> ), backToTop: ( @@ -269,7 +260,6 @@ function PageContents(props: PageContentsProps): JSX.Element { robotName={robotName} runId={runId} makeHandleJumpToStep={makeHandleJumpToStep} - missingSetupSteps={missingSteps} /> { - const INITIAL = { - [Constants.ROBOT_CALIBRATION_STEP_KEY]: { - required: true, - complete: false, - }, - [Constants.MODULE_SETUP_STEP_KEY]: { required: true, complete: false }, - [Constants.LPC_STEP_KEY]: { required: true, complete: false }, - [Constants.LABWARE_SETUP_STEP_KEY]: { - required: true, - complete: false, - }, - [Constants.LIQUID_SETUP_STEP_KEY]: { required: true, complete: false }, - } - it('establishes an empty state if you tell it one', () => { - const nextState = protocolRunReducer( - undefined, - updateRunSetupStepsComplete('some-run-id', {}) - ) - expect(nextState['some-run-id']?.setup).toEqual(INITIAL) - }) - it('updates complete based on an action', () => { - const nextState = protocolRunReducer( - { - ['some-run-id']: { - setup: { - ...INITIAL, - [Constants.LIQUID_SETUP_STEP_KEY]: { - complete: true, - required: true, - }, - }, - }, - }, - updateRunSetupStepsComplete('some-run-id', { - [Constants.LPC_STEP_KEY]: true, - }) - ) - expect(nextState['some-run-id']?.setup).toEqual({ + const INITIAL = { + [Constants.ROBOT_CALIBRATION_STEP_KEY]: { + required: true, + complete: false, + }, + [Constants.MODULE_SETUP_STEP_KEY]: { required: true, complete: false }, + [Constants.LPC_STEP_KEY]: { required: true, complete: false }, + [Constants.LABWARE_SETUP_STEP_KEY]: { + required: true, + complete: false, + }, + [Constants.LIQUID_SETUP_STEP_KEY]: { required: true, complete: false }, + } + it('establishes an empty state if you tell it one', () => { + const nextState = protocolRunReducer( + undefined, + updateRunSetupStepsComplete('some-run-id', {}) + ) + expect(nextState['some-run-id']?.setup).toEqual(INITIAL) + }) + it('updates complete based on an action', () => { + const nextState = protocolRunReducer( + { + ['some-run-id']: { + setup: { ...INITIAL, [Constants.LIQUID_SETUP_STEP_KEY]: { - required: true, - complete: true, + complete: true, + required: true, }, - [Constants.LPC_STEP_KEY]: { required: true, complete: true }, - }) + }, + }, + }, + updateRunSetupStepsComplete('some-run-id', { + [Constants.LPC_STEP_KEY]: true, + }) + ) + expect(nextState['some-run-id']?.setup).toEqual({ + ...INITIAL, + [Constants.LIQUID_SETUP_STEP_KEY]: { + required: true, + complete: true, + }, + [Constants.LPC_STEP_KEY]: { required: true, complete: true }, }) - it('updates required based on an action', () => { - const nextState = protocolRunReducer( - { - ['some-run-id']: { - setup: INITIAL, - }, - }, - updateRunSetupStepsRequired('some-run-id', { - [Constants.LIQUID_SETUP_STEP_KEY]: false, - }) - ) - expect(nextState['some-run-id']?.setup).toEqual({ - ...INITIAL, - [Constants.LIQUID_SETUP_STEP_KEY]: { - required: false, - complete: false, - }, - }) + }) + it('updates required based on an action', () => { + const nextState = protocolRunReducer( + { + ['some-run-id']: { + setup: INITIAL, + }, + }, + updateRunSetupStepsRequired('some-run-id', { + [Constants.LIQUID_SETUP_STEP_KEY]: false, + }) + ) + expect(nextState['some-run-id']?.setup).toEqual({ + ...INITIAL, + [Constants.LIQUID_SETUP_STEP_KEY]: { + required: false, + complete: false, + }, }) + }) }) diff --git a/app/src/redux/protocol-runs/actions.ts b/app/src/redux/protocol-runs/actions.ts index ef806041983..378ee297ed2 100644 --- a/app/src/redux/protocol-runs/actions.ts +++ b/app/src/redux/protocol-runs/actions.ts @@ -2,17 +2,17 @@ import * as Constants from './constants' import type * as Types from './types' export const updateRunSetupStepsComplete = ( - runId: string, - complete: Types.UpdateRunSetupStepsCompleteAction['payload']['complete'] + runId: string, + complete: Types.UpdateRunSetupStepsCompleteAction['payload']['complete'] ): Types.UpdateRunSetupStepsCompleteAction => ({ - type: Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE, - payload: { runId, complete }, + type: Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE, + payload: { runId, complete }, }) export const updateRunSetupStepsRequired = ( - runId: string, - required: Types.UpdateRunSetupStepsRequiredAction['payload']['required'] + runId: string, + required: Types.UpdateRunSetupStepsRequiredAction['payload']['required'] ): Types.UpdateRunSetupStepsRequiredAction => ({ - type: Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED, - payload: { runId, required }, + type: Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED, + payload: { runId, required }, }) diff --git a/app/src/redux/protocol-runs/reducer.ts b/app/src/redux/protocol-runs/reducer.ts index c7915b23efe..2a9ed5a9677 100644 --- a/app/src/redux/protocol-runs/reducer.ts +++ b/app/src/redux/protocol-runs/reducer.ts @@ -4,10 +4,10 @@ import type { Reducer } from 'redux' import type { Action } from '../types' import type { - ProtocolRunState, - RunSetupStatus, - StepKey, - StepState, + ProtocolRunState, + RunSetupStatus, + StepKey, + StepState, } from './types' const INITIAL_STATE: ProtocolRunState = {} @@ -15,58 +15,54 @@ const INITIAL_STATE: ProtocolRunState = {} const INITIAL_SETUP_STEP_STATE = { complete: false, required: true } const INITIAL_RUN_SETUP_STATE: RunSetupStatus = { - [Constants.ROBOT_CALIBRATION_STEP_KEY]: INITIAL_SETUP_STEP_STATE, - [Constants.MODULE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, - [Constants.LPC_STEP_KEY]: INITIAL_SETUP_STEP_STATE, - [Constants.LABWARE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, - [Constants.LIQUID_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.ROBOT_CALIBRATION_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.MODULE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LPC_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LABWARE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LIQUID_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, } export const protocolRunReducer: Reducer = ( - state = INITIAL_STATE, - action + state = INITIAL_STATE, + action ) => { - switch (action.type) { - case Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE: { - return { - ...state, - [action.payload.runId]: { - setup: Constants.SETUP_STEP_KEYS.reduce( - (currentState, step) => ({ - ...currentState, - [step]: { - complete: - action.payload.complete[step] ?? - currentState[step].complete, - required: currentState[step].required, - }, - }), - state[action.payload.runId]?.setup ?? - INITIAL_RUN_SETUP_STATE - ), - }, - } - } - case Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED: { - return { - ...state, - [action.payload.runId]: { - setup: Constants.SETUP_STEP_KEYS.reduce( - (currentState, step) => ({ - ...currentState, - [step]: { - required: - action.payload.required[step] ?? - currentState[step].required, - complete: currentState[step].complete, - }, - }), - state[action.payload.runId]?.setup ?? - INITIAL_RUN_SETUP_STATE - ), - }, - } - } + switch (action.type) { + case Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE: { + return { + ...state, + [action.payload.runId]: { + setup: Constants.SETUP_STEP_KEYS.reduce( + (currentState, step) => ({ + ...currentState, + [step]: { + complete: + action.payload.complete[step] ?? currentState[step].complete, + required: currentState[step].required, + }, + }), + state[action.payload.runId]?.setup ?? INITIAL_RUN_SETUP_STATE + ), + }, + } } - return state + case Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED: { + return { + ...state, + [action.payload.runId]: { + setup: Constants.SETUP_STEP_KEYS.reduce( + (currentState, step) => ({ + ...currentState, + [step]: { + required: + action.payload.required[step] ?? currentState[step].required, + complete: currentState[step].complete, + }, + }), + state[action.payload.runId]?.setup ?? INITIAL_RUN_SETUP_STATE + ), + }, + } + } + } + return state } diff --git a/app/src/redux/protocol-runs/selectors.ts b/app/src/redux/protocol-runs/selectors.ts index 32c50fe25dd..411e12895f0 100644 --- a/app/src/redux/protocol-runs/selectors.ts +++ b/app/src/redux/protocol-runs/selectors.ts @@ -2,90 +2,90 @@ import type { State } from '../types' import type * as Types from './types' export const getSetupStepComplete: ( - state: State, - runId: string, - step: Types.StepKey + state: State, + runId: string, + step: Types.StepKey ) => boolean | null = (state, runId, step) => - getSetupStepsComplete(state, runId)?.[step] ?? null + getSetupStepsComplete(state, runId)?.[step] ?? null export const getSetupStepsComplete: ( - state: State, - runId: string + state: State, + runId: string ) => Types.StepMap | null = (state, runId) => { - const setup = state.protocolRuns[runId]?.setup - if (setup == null) { - return null - } - return (Object.entries(setup) as Array< - [Types.StepKey, Types.StepState] - >).reduce>( - (acc, [step, state]) => ({ - ...acc, - [step]: state.complete, - }), - {} as Types.StepMap - ) + const setup = state.protocolRuns[runId]?.setup + if (setup == null) { + return null + } + return (Object.entries(setup) as Array< + [Types.StepKey, Types.StepState] + >).reduce>( + (acc, [step, state]) => ({ + ...acc, + [step]: state.complete, + }), + {} as Types.StepMap + ) } export const getSetupStepRequired: ( - state: State, - runId: string, - step: Types.StepKey + state: State, + runId: string, + step: Types.StepKey ) => boolean | null = (state, runId, step) => - getSetupStepsRequired(state, runId)?.[step] ?? null + getSetupStepsRequired(state, runId)?.[step] ?? null export const getSetupStepsRequired: ( - state: State, - runId: string + state: State, + runId: string ) => Types.StepMap | null = (state, runId) => { - const setup = state.protocolRuns[runId]?.setup - if (setup == null) { - return null - } - return (Object.entries(setup) as Array< - [Types.StepKey, Types.StepState] - >).reduce>( - (acc, [step, state]) => ({ ...acc, [step]: state.required }), - {} as Types.StepMap - ) + const setup = state.protocolRuns[runId]?.setup + if (setup == null) { + return null + } + return (Object.entries(setup) as Array< + [Types.StepKey, Types.StepState] + >).reduce>( + (acc, [step, state]) => ({ ...acc, [step]: state.required }), + {} as Types.StepMap + ) } export const getSetupStepMissing: ( - state: State, - runId: string, - step: Types.StepKey + state: State, + runId: string, + step: Types.StepKey ) => boolean | null = (state, runId, step) => - getSetupStepsMissing(state, runId)?.[step] || null + getSetupStepsMissing(state, runId)?.[step] || null export const getSetupStepsMissing: ( - state: State, - runId: string + state: State, + runId: string ) => Types.StepMap | null = (state, runId) => { - const setup = state.protocolRuns[runId]?.setup - if (setup == null) { - return null - } - return (Object.entries(setup) as Array< - [Types.StepKey, Types.StepState] - >).reduce>( - (acc, [step, state]) => ({ - ...acc, - [step]: state.required && !state.complete, - }), - {} as Types.StepMap - ) + const setup = state.protocolRuns[runId]?.setup + if (setup == null) { + return null + } + return (Object.entries(setup) as Array< + [Types.StepKey, Types.StepState] + >).reduce>( + (acc, [step, state]) => ({ + ...acc, + [step]: state.required && !state.complete, + }), + {} as Types.StepMap + ) } export const getMissingSetupSteps: ( - state: State, - runId: string + state: State, + runId: string ) => Types.StepKey[] = (state, runId) => { - const missingStepMap = getSetupStepsMissing(state, runId) - if (missingStepMap == null) return [] - const missingStepList = (Object.entries(missingStepMap) as Array< - [Types.StepKey, boolean] - >) - .map(([step, missing]) => (missing ? step : null)) - .filter(stepName => stepName != null) - return missingStepList as Types.StepKey[] + const missingStepMap = getSetupStepsMissing(state, runId) + if (missingStepMap == null) return [] + const missingStepList = (Object.entries(missingStepMap) as Array< + [Types.StepKey, boolean] + >) + .map(([step, missing]) => (missing ? step : null)) + .filter(stepName => stepName != null) + return missingStepList as Types.StepKey[] } diff --git a/app/src/redux/protocol-runs/types.ts b/app/src/redux/protocol-runs/types.ts index 7e10cf7fd96..8967879a5bf 100644 --- a/app/src/redux/protocol-runs/types.ts +++ b/app/src/redux/protocol-runs/types.ts @@ -1,11 +1,11 @@ import { - ROBOT_CALIBRATION_STEP_KEY, - MODULE_SETUP_STEP_KEY, - LPC_STEP_KEY, - LABWARE_SETUP_STEP_KEY, - LIQUID_SETUP_STEP_KEY, - UPDATE_RUN_SETUP_STEPS_COMPLETE, - UPDATE_RUN_SETUP_STEPS_REQUIRED, + ROBOT_CALIBRATION_STEP_KEY, + MODULE_SETUP_STEP_KEY, + LPC_STEP_KEY, + LABWARE_SETUP_STEP_KEY, + LIQUID_SETUP_STEP_KEY, + UPDATE_RUN_SETUP_STEPS_COMPLETE, + UPDATE_RUN_SETUP_STEPS_REQUIRED, } from './constants' export type RobotCalibrationStepKey = typeof ROBOT_CALIBRATION_STEP_KEY @@ -15,47 +15,47 @@ export type LabwareSetupStepKey = typeof LABWARE_SETUP_STEP_KEY export type LiquidSetupStepKey = typeof LIQUID_SETUP_STEP_KEY export type StepKey = - | RobotCalibrationStepKey - | ModuleSetupStepKey - | LPCStepKey - | LabwareSetupStepKey - | LiquidSetupStepKey + | RobotCalibrationStepKey + | ModuleSetupStepKey + | LPCStepKey + | LabwareSetupStepKey + | LiquidSetupStepKey export interface StepState { - required: boolean - complete: boolean + required: boolean + complete: boolean } export type StepMap = { [Step in StepKey]: V } export type RunSetupStatus = { - [Step in StepKey]: StepState + [Step in StepKey]: StepState } export interface PerRunUIState { - setup: RunSetupStatus + setup: RunSetupStatus } export type ProtocolRunState = Partial<{ - readonly [runId: string]: PerRunUIState + readonly [runId: string]: PerRunUIState }> export interface UpdateRunSetupStepsCompleteAction { - type: typeof UPDATE_RUN_SETUP_STEPS_COMPLETE - payload: { - runId: string - complete: Partial<{ [Step in StepKey]: boolean }> - } + type: typeof UPDATE_RUN_SETUP_STEPS_COMPLETE + payload: { + runId: string + complete: Partial<{ [Step in StepKey]: boolean }> + } } export interface UpdateRunSetupStepsRequiredAction { - type: typeof UPDATE_RUN_SETUP_STEPS_REQUIRED - payload: { - runId: string - required: Partial<{ [Step in StepKey]: boolean }> - } + type: typeof UPDATE_RUN_SETUP_STEPS_REQUIRED + payload: { + runId: string + required: Partial<{ [Step in StepKey]: boolean }> + } } export type ProtocolRunAction = - | UpdateRunSetupStepsCompleteAction - | UpdateRunSetupStepsRequiredAction + | UpdateRunSetupStepsCompleteAction + | UpdateRunSetupStepsRequiredAction From 9641102a17cdd593d421d8019cd4a93d9eaaf7fd Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 22 Oct 2024 15:22:12 -0400 Subject: [PATCH 6/9] fixup tests and lint --- .../__tests__/ProtocolRunSetup.test.tsx | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 70cbaa74791..46663a77800 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -32,6 +32,10 @@ import { useDeckConfigurationCompatibility } from '/app/resources/deck_configura import { useRobot, useIsFlex } from '/app/redux-resources/robots' import { useRequiredSetupStepsInOrder } from '/app/redux-resources/runs' import { useStoredProtocolAnalysis } from '/app/resources/analysis' +import { + updateRunSetupStepsComplete, + getMissingSetupSteps, +} from '/app/redux/protocol-runs' import { SetupLabware } from '../SetupLabware' import { SetupRobotCalibration } from '../SetupRobotCalibration' @@ -41,6 +45,8 @@ import { EmptySetupStep } from '../EmptySetupStep' import { ProtocolRunSetup } from '../ProtocolRunSetup' import * as ReduxRuns from '/app/redux/protocol-runs' +import type { State } from '/app/redux/types' + import type { StepKey } from '/app/redux/protocol-runs' import type * as SharedData from '@opentrons/shared-data' @@ -59,6 +65,8 @@ vi.mock('/app/resources/runs/useUnmatchedModulesForProtocol') vi.mock('/app/resources/runs/useModuleCalibrationStatus') vi.mock('/app/resources/runs/useProtocolAnalysisErrors') vi.mock('/app/redux/config') +vi.mock('/app/redux/protocol-runs') +vi.mock('/app/resources/protocol-runs') vi.mock('/app/resources/deck_configuration/utils') vi.mock('/app/resources/deck_configuration/hooks') vi.mock('/app/redux-resources/robots') @@ -79,13 +87,14 @@ const ROBOT_NAME = 'otie' const RUN_ID = '1' const MOCK_PROTOCOL_LIQUID_KEY = { liquids: [] } const render = () => { - return renderWithProviders( + return renderWithProviders( , { + initialState: {}, i18nInstance: i18n, } )[0] @@ -100,6 +109,9 @@ describe('ProtocolRunSetup', () => { ...noModulesProtocol, ...MOCK_PROTOCOL_LIQUID_KEY, } as any) + when(vi.mocked(getMissingSetupSteps)) + .calledWith(expect.any(Object), RUN_ID) + .thenReturn([]) when(vi.mocked(useProtocolAnalysisErrors)).calledWith(RUN_ID).thenReturn({ analysisErrors: null, }) @@ -112,14 +124,23 @@ describe('ProtocolRunSetup', () => { when(vi.mocked(useRequiredSetupStepsInOrder)) .calledWith({ runId: RUN_ID, - protocolAnalysis: ({ - ...noModulesProtocol, - ...MOCK_PROTOCOL_LIQUID_KEY, - } as any) as SharedData.CompletedProtocolAnalysis, + protocolAnalysis: expect.any(Object), }) .thenReturn({ - orderedSteps: ReduxRuns.SETUP_STEP_KEYS, - orderedApplicableSteps: (ReduxRuns.SETUP_STEP_KEYS as any) as StepKey[], + orderedSteps: [ + ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, + ReduxRuns.MODULE_SETUP_STEP_KEY, + ReduxRuns.LPC_STEP_KEY, + ReduxRuns.LABWARE_SETUP_STEP_KEY, + ReduxRuns.LIQUID_SETUP_STEP_KEY, + ], + orderedApplicableSteps: [ + ReduxRuns.ROBOT_CALIBRATION_STEP_KEY, + ReduxRuns.MODULE_SETUP_STEP_KEY, + ReduxRuns.LPC_STEP_KEY, + ReduxRuns.LABWARE_SETUP_STEP_KEY, + ReduxRuns.LIQUID_SETUP_STEP_KEY, + ], }) vi.mocked(parseAllRequiredModuleModels).mockReturnValue([]) vi.mocked(parseLiquidsInLoadOrder).mockReturnValue([]) From 6deeb0f07aec5c5b39426a9d0572fdbe977951f8 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 23 Oct 2024 10:09:24 -0400 Subject: [PATCH 7/9] fix typechecks --- .../Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 46663a77800..0657a0b1e0d 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -94,7 +94,7 @@ const render = () => { runId={RUN_ID} />, { - initialState: {}, + initialState: {} as State, i18nInstance: i18n, } )[0] From bb81c7848417a1f3c16ca2f3f96021aa1f9221f4 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 23 Oct 2024 10:09:31 -0400 Subject: [PATCH 8/9] fix: remove render loop protocol analyses are absolutely not safe to use as hook dependencies --- .../runs/hooks/useRequiredSetupStepsInOrder.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts b/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts index 314829c0892..1471a5dacb2 100644 --- a/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts +++ b/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts @@ -68,6 +68,11 @@ const keysInOrder = ( return { orderedSteps: orderedSteps as StepKey[], orderedApplicableSteps } } +const keyFor = ( + analysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null + // @ts-expect-error(sf, 2024-10-23): purposeful weak object typing +): string | null => analysis?.id ?? analysis?.metadata?.id ?? null + export function useRequiredSetupStepsInOrder({ runId, protocolAnalysis, @@ -92,7 +97,7 @@ export function useRequiredSetupStepsInOrder({ ), }) ) - }, [runId, protocolAnalysis, dispatch]) + }, [runId, keyFor(protocolAnalysis), dispatch]) return protocolAnalysis == null ? { orderedSteps: NO_ANALYSIS_STEPS_IN_ORDER, From 833f5f742f462b019f563b4c78c3f16347c4301f Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Wed, 23 Oct 2024 10:35:40 -0400 Subject: [PATCH 9/9] lints --- .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 8 -------- .../__tests__/ProtocolRunSetup.test.tsx | 7 +------ .../runs/hooks/useRequiredSetupStepsInOrder.ts | 6 +++--- .../protocol-runs/__tests__/reducer.test.ts | 4 ++-- app/src/redux/protocol-runs/reducer.ts | 7 +------ app/src/redux/protocol-runs/selectors.ts | 18 +++++++++--------- app/src/redux/protocol-runs/types.ts | 2 +- 7 files changed, 17 insertions(+), 35 deletions(-) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx index 6a5f94a2db0..8e10948795a 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -63,14 +63,6 @@ import { HowLPCWorksModal } from './SetupLabwarePositionCheck/HowLPCWorksModal' import type { Dispatch, State } from '/app/redux/types' import type { StepKey } from '/app/redux/protocol-runs' -const STEP_KEY_TO_I18N_KEY = { - LPC_STEP_KEY: 'applied_labware_offsets', - LABWARE_SETUP_STEP_KEY: 'labware_placement', - LIQUID_SETUP_STEP_KEY: 'liquids', - MODULE_SETUP_STEP_KEY: 'module_setup', - ROBOT_CALIBRATION_STEP_KEY: 'robot_calibrtion', -} - interface ProtocolRunSetupProps { protocolRunHeaderRef: React.RefObject | null robotName: string diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 0657a0b1e0d..84e7fb82e65 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -32,10 +32,7 @@ import { useDeckConfigurationCompatibility } from '/app/resources/deck_configura import { useRobot, useIsFlex } from '/app/redux-resources/robots' import { useRequiredSetupStepsInOrder } from '/app/redux-resources/runs' import { useStoredProtocolAnalysis } from '/app/resources/analysis' -import { - updateRunSetupStepsComplete, - getMissingSetupSteps, -} from '/app/redux/protocol-runs' +import { getMissingSetupSteps } from '/app/redux/protocol-runs' import { SetupLabware } from '../SetupLabware' import { SetupRobotCalibration } from '../SetupRobotCalibration' @@ -47,8 +44,6 @@ import * as ReduxRuns from '/app/redux/protocol-runs' import type { State } from '/app/redux/types' -import type { StepKey } from '/app/redux/protocol-runs' - import type * as SharedData from '@opentrons/shared-data' vi.mock('../SetupLabware') diff --git a/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts b/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts index 1471a5dacb2..481a3622f05 100644 --- a/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts +++ b/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts @@ -28,8 +28,8 @@ export interface UseRequiredSetupStepsInOrderProps { } export interface UseRequiredSetupStepsInOrderReturn { - orderedSteps: StepKey[] - orderedApplicableSteps: StepKey[] + orderedSteps: readonly StepKey[] + orderedApplicableSteps: readonly StepKey[] } const ALL_STEPS_IN_ORDER = [ @@ -76,7 +76,7 @@ const keyFor = ( export function useRequiredSetupStepsInOrder({ runId, protocolAnalysis, -}: UseRequiredSetupStepsInOrderProps) { +}: UseRequiredSetupStepsInOrderProps): UseRequiredSetupStepsInOrderReturn { const dispatch = useDispatch() const requiredSteps = useSelector(state => getSetupStepsRequired(state, runId) diff --git a/app/src/redux/protocol-runs/__tests__/reducer.test.ts b/app/src/redux/protocol-runs/__tests__/reducer.test.ts index 9c51c6c0e77..e10ce306f7d 100644 --- a/app/src/redux/protocol-runs/__tests__/reducer.test.ts +++ b/app/src/redux/protocol-runs/__tests__/reducer.test.ts @@ -31,7 +31,7 @@ describe('protocol runs reducer', () => { it('updates complete based on an action', () => { const nextState = protocolRunReducer( { - ['some-run-id']: { + 'some-run-id': { setup: { ...INITIAL, [Constants.LIQUID_SETUP_STEP_KEY]: { @@ -57,7 +57,7 @@ describe('protocol runs reducer', () => { it('updates required based on an action', () => { const nextState = protocolRunReducer( { - ['some-run-id']: { + 'some-run-id': { setup: INITIAL, }, }, diff --git a/app/src/redux/protocol-runs/reducer.ts b/app/src/redux/protocol-runs/reducer.ts index 2a9ed5a9677..0b2d8378a67 100644 --- a/app/src/redux/protocol-runs/reducer.ts +++ b/app/src/redux/protocol-runs/reducer.ts @@ -3,12 +3,7 @@ import * as Constants from './constants' import type { Reducer } from 'redux' import type { Action } from '../types' -import type { - ProtocolRunState, - RunSetupStatus, - StepKey, - StepState, -} from './types' +import type { ProtocolRunState, RunSetupStatus } from './types' const INITIAL_STATE: ProtocolRunState = {} diff --git a/app/src/redux/protocol-runs/selectors.ts b/app/src/redux/protocol-runs/selectors.ts index 411e12895f0..ca91c7a71ab 100644 --- a/app/src/redux/protocol-runs/selectors.ts +++ b/app/src/redux/protocol-runs/selectors.ts @@ -18,13 +18,13 @@ export const getSetupStepsComplete: ( } return (Object.entries(setup) as Array< [Types.StepKey, Types.StepState] - >).reduce>( + >).reduce>>( (acc, [step, state]) => ({ ...acc, [step]: state.complete, }), - {} as Types.StepMap - ) + {} + ) as Types.StepMap } export const getSetupStepRequired: ( @@ -44,10 +44,10 @@ export const getSetupStepsRequired: ( } return (Object.entries(setup) as Array< [Types.StepKey, Types.StepState] - >).reduce>( + >).reduce>>( (acc, [step, state]) => ({ ...acc, [step]: state.required }), - {} as Types.StepMap - ) + {} + ) as Types.StepMap } export const getSetupStepMissing: ( @@ -67,13 +67,13 @@ export const getSetupStepsMissing: ( } return (Object.entries(setup) as Array< [Types.StepKey, Types.StepState] - >).reduce>( + >).reduce>>( (acc, [step, state]) => ({ ...acc, [step]: state.required && !state.complete, }), - {} as Types.StepMap - ) + {} + ) as Types.StepMap } export const getMissingSetupSteps: ( diff --git a/app/src/redux/protocol-runs/types.ts b/app/src/redux/protocol-runs/types.ts index 8967879a5bf..c14d556d495 100644 --- a/app/src/redux/protocol-runs/types.ts +++ b/app/src/redux/protocol-runs/types.ts @@ -1,4 +1,4 @@ -import { +import type { ROBOT_CALIBRATION_STEP_KEY, MODULE_SETUP_STEP_KEY, LPC_STEP_KEY,