diff --git a/protocol-designer/src/assets/localization/en/alert.json b/protocol-designer/src/assets/localization/en/alert.json index b8f73cc290b..66a0a406a47 100644 --- a/protocol-designer/src/assets/localization/en/alert.json +++ b/protocol-designer/src/assets/localization/en/alert.json @@ -97,75 +97,75 @@ "timeline": { "error": { "LABWARE_DISCARDED_IN_WASTE_CHUTE": { - "title": "The labware has been previously discarded into the waste chute", - "body": "Please select a different labware to move." + "title": "Labware not available", + "body": "This step uses labware that has previously been discarded into a Waste Chute." }, "LABWARE_ON_ANOTHER_ENTITY": { - "title": "Attempting to move a labware on top of another entity", + "title": "Attempting to move labware on top of another entity", "body": "Please reselect which slot your labware should move to." }, "INSUFFICIENT_TIPS": { "title": "Not enough tips to complete action", - "body": "Add another tip rack to an empty slot in " + "body": "Add another tip rack to your deck or change your tip management during transfer and mix steps.", + "link": "Edit starting deck" }, "NO_TIP_SELECTED": { "title": "No tip rack was selected to complete action", "body": "Add a tip rack in the step" }, "NO_TIP_ON_PIPETTE": { - "title": "No tip on pipette at the start of step", - "body1": "Choose a different Change Tip setting. Change Tip cannot be \"Never\" the first time a pipette is used in a protocol, or following a step that used the ", - "link": "Air gap dispense setting", - "body2": ". Pipetting steps must begin with a tip on." + "title": "No tip on pipette at start of step", + "body": "Use a different tip handling setting. Don't set it to Never the first time a pipette is used in a protocol, or following a step that air gaps when dispensing." }, "MODULE_PIPETTE_COLLISION_DANGER": { - "title": "Pipette cannot access labware", - "body": "Gen 1 8-Channel pipettes cannot access labware or tip racks in slot 4 or 6 because they are adjacent to modules. Read more " + "title": "GEN1 8-Channel Pipettes can't move adjacent to modules", + "body": "Move labware and modules or use a different pipette." }, "MISSING_MODULE": { - "title": "Missing module for step", - "body": "A step requires a module that does not exist" + "title": "Module not in protocol", + "body": "This step tries to use a module not in the protocol. Add the module to your protocol or remove this step." }, "MISSING_TEMPERATURE_STEP": { - "title": "Missing Temperature step", - "body": "Add a Temperature step prior to this Pause step. The module is not currently changing temperature because it has either been deactivated or is holding a temperature" + "title": "Unreachable target temperature", + "body": "The protocol can't proceed beyond this pause step, because the module is not changing temperature. Add or modify a temperature step before this step." }, "THERMOCYCLER_LID_CLOSED": { - "title": "Thermocycler lid is closed", - "body": "Before the robot can interact with labware in the Thermocycler, the lid must be open. To resolve this error, please add a thermocycler step ahead of the current step, and set the lid status to \"open\"." + "title": "Thermocycler lid closed", + "body": "This step tries to use labware in the Thermocycler. Open the lid before this step." }, "HEATER_SHAKER_LATCH_OPEN": { - "title": "Heater-Shaker labware latch is open", + "title": "Heater-Shaker latch open", "body": "Before the robot can interact with labware on the Heater-Shaker module, the labware latch must be closed. To resolve this error, please add a Heater-Shaker step ahead of the current step, and set the labware latch status to \"closed\"." }, "HEATER_SHAKER_IS_SHAKING": { "title": "Heater-Shaker is shaking", - "body": "the robot cannot interact with labware on the Heater-Shaker Module while it is shaking." + "body": "The robot cannot interact with labware on the Heater-Shaker Module while it is shaking. Add a step to stop shaking to interact with the labware." }, "TALL_LABWARE_EAST_WEST_OF_HEATER_SHAKER": { "body": "The Heater-Shaker labware latch will collide with labware over 53 mm. Move labware to a different slot." }, "HEATER_SHAKER_EAST_WEST_LATCH_OPEN": { + "title": "Heater-Shaker labware latch open", "body": "Pipettes cannot access labware on, or to the left or right of, the Heater-Shaker while the labware latch is open. Create a step before this one that closes the latch." }, "HEATER_SHAKER_NORTH_SOUTH_EAST_WEST_SHAKING": { - "title": "The Heater-Shaker is shaking", - "body": "Pipettes cannot access labware on or adjacent to the Heater-Shaker while it is shaking. Create a step before this one that deactivates the shaker." + "title": "Robot unable to perform step", + "body": "The robot cannot interact with labware on or next to the Heater-Shaker Module while it is shaking. Add a heater-shaker step to stop shaking." }, "HEATER_SHAKER_EAST_WEST_MULTI_CHANNEL": { - "title": "8-Channel pipette cannot access labware", - "body": "8-Channel pipettes cannot access labware or tip racks to the left or right of a Heater-Shaker GEN1 module. Move labware to a different slot to access it with an 8-Channel pipette." + "title": "8-Channel unable to access slot", + "body": "8-Channel pipettes cannot access labware to the left or right of a Heater-Shaker GEN1 module. Move labware to a different slot to access it with an 8-Channel pipette." }, "HEATER_SHAKER_NORTH_SOUTH__OF_NON_TIPRACK_WITH_MULTI_CHANNEL": { - "title": "8-Channel pipette cannot access labware", - "body": "8-Channel pipettes cannot access labware in front of or behind a Heater-Shaker. They can access Opentrons Tip Racks in this slot. Move labware to a different slot." + "title": "8-Channel unable to access slot", + "body": "8-Channel pipettes cannot access non-tiprack labware to the top or bottom of a Heater-Shaker GEN1 module. Move labware to a different slot to access it with an 8-Channel pipette." }, "LABWARE_OFF_DECK": { - "title": "Labware is off-deck", + "title": "Labware not on deck", "body": "The robot can only perform steps on labware that is on the deck. Add or change a Move Labware step to put it on the deck before this step." }, "HEATER_SHAKER_LATCH_CLOSED": { - "title": "Heater-Shaker labware latch is closed", + "title": "Heater-Shaker latch closed", "body": "The Heater-Shaker’s labware latch must be open when moving labware to or from the module. Add a Heater-Shaker step that opens the latch before this step." }, "DROP_TIP_LOCATION_DOES_NOT_EXIST": { @@ -173,46 +173,52 @@ "body": "The Waste Chute or Trash Bin to drop tip in does not exist." }, "MISSING_96_CHANNEL_TIPRACK_ADAPTER": { - "title": "Missing 96-channel tip rack adapter", - "body": "The tip rack must be placed in an adapter when picking up 96 tips simultaneously." + "title": "Tip rack adapter required", + "body": "The 96-channel pipette uses a tip rack adapter to pick up a full rack of tips. Add one to your starting deck or use partial tip pickup.", + "link": "Edit starting deck" }, "EQUIPMENT_DOES_NOT_EXIST": { - "title": "Attempting to interact with an unknown entity", + "title": "Unable to perform step", "body": "An entity you are interacting with does not exist." }, "GRIPPER_REQUIRED": { - "title": "A gripper is required to complete this action", - "body": "Attempting to move a labware without a gripper into the waste chute. Please add a gripper to this step." + "title": "Cannot move with gripper", + "body": "The gripper cannot move aluminum blocks. Deselect the 'Use Gripper' checkbox." }, "REMOVE_96_CHANNEL_TIPRACK_ADAPTER": { - "title": "Do not use tip rack adapter for partial tip pickup", - "body": "Partial tip pickup requires a tip rack placed directly on the deck. Remove the adapter, or add a new tip rack without an adapter." + "title": "Extra tip rack adapter", + "body": "When picking up fewer than 96 tips, the tip rack must be placed directly on the deck, not in the tip rack adapter.", + "link": "Edit starting deck" }, "CANNOT_MOVE_WITH_GRIPPER": { "title": "Cannot move with gripper", "body": "The gripper cannot move aluminum blocks. Edit the step and deselect the 'Use Gripper' checkbox." }, "PIPETTE_HAS_TIP": { - "title": "Possible collision with tip", - "body": "The gripper cannot pick up labware while pipettes have tips attached. Drop all tips before this move labware step." + "title": "Gripper movement with tips attached", + "body": "Picking up labware with the gripper while tips are on an adjacent pipette can cause collisions. Drop tips from all pipettes before this step." + }, + "POSSIBLE_PIPETTE_COLLISION": { + "title": "Pipette collisions likely", + "body": "There is a possibility that the pipette will collide with the adjascent labware or module for partial tip pick up." } }, "warning": { "ASPIRATE_MORE_THAN_WELL_CONTENTS": { - "title": "Not enough liquid in well(s)", - "body": "You are trying to aspirate more than the current volume of one of your well(s)" + "title": "Not enough liquid", + "body": "This step tries to aspirate more than the current volume of a source well." }, "ASPIRATE_FROM_PRISTINE_WELL": { "title": "Source well is empty", - "body": "The well(s) you're trying to aspirate from are empty. To add liquids, hover over labware in " + "body": "This step tries to aspirate from an empty well." }, "LABWARE_IN_WASTE_CHUTE_HAS_LIQUID": { - "title": "Moving labware into waste chute", - "body": "This labware has remaining liquid, be advised that once you dispose of it, there is no way to get it back later in the protocol." + "title": "Disposing liquid-filled labware", + "body": "This step moves a labware that contains liquid to the waste chute. There is no way to retrieve the liquid after disposal." }, "TIPRACK_IN_WASTE_CHUTE_HAS_TIPS": { - "title": "Moving tiprack into waste chute", - "body": "This tiprack has remaining tips, be advised that once you dispose of it, there is no way to get it back later in the protocol. " + "title": "Disposing unused tips", + "body": "This step moves a tip rack that contains unused tips to the waste chute. There is no way to retrieve the tips after disposal." } } }, diff --git a/protocol-designer/src/file-data/actions.ts b/protocol-designer/src/file-data/actions.ts index 0e175493baf..639966b1501 100644 --- a/protocol-designer/src/file-data/actions.ts +++ b/protocol-designer/src/file-data/actions.ts @@ -1,4 +1,8 @@ -import type { FileMetadataFields, SaveFileMetadataAction } from './types' +import type { + FileMetadataFields, + SaveFileMetadataAction, + SelectDesignerTabAction, +} from './types' import type { WorkerResponse } from '../timelineMiddleware/types' export const saveFileMetadata = ( payload: FileMetadataFields @@ -22,3 +26,14 @@ export const computeRobotStateTimelineSuccess = ( type: 'COMPUTE_ROBOT_STATE_TIMELINE_SUCCESS', payload, }) + +export interface DesignerTabPayload { + tab: 'protocolSteps' | 'startingDeck' +} + +export const selectDesignerTab = ( + payload: DesignerTabPayload +): SelectDesignerTabAction => ({ + type: 'SELECT_DESIGNER_TAB', + payload, +}) diff --git a/protocol-designer/src/file-data/reducers/index.ts b/protocol-designer/src/file-data/reducers/index.ts index 7f4d010e8ec..0d2b50ea773 100644 --- a/protocol-designer/src/file-data/reducers/index.ts +++ b/protocol-designer/src/file-data/reducers/index.ts @@ -7,8 +7,15 @@ import type { RobotType } from '@opentrons/shared-data' import type { Action } from '../../types' import type { LoadFileAction, NewProtocolFields } from '../../load-file' import type { Substeps } from '../../steplist/types' -import type { ComputeRobotStateTimelineSuccessAction } from '../actions' -import type { FileMetadataFields, SaveFileMetadataAction } from '../types' +import type { + ComputeRobotStateTimelineSuccessAction, + DesignerTabPayload, +} from '../actions' +import type { + FileMetadataFields, + SaveFileMetadataAction, + SelectDesignerTabAction, +} from '../types' export const timelineIsBeingComputed: Reducer = handleActions( { @@ -110,6 +117,18 @@ const robotTypeReducer = ( } return state } + +const designerTabReducer = ( + state: DesignerTabPayload['tab'] = 'startingDeck', + action: SelectDesignerTabAction +): DesignerTabPayload['tab'] => { + if (action.type === 'SELECT_DESIGNER_TAB') { + return action.payload.tab + } else { + return state + } +} + export interface RootState { computedRobotStateTimeline: Timeline computedSubsteps: Substeps @@ -117,6 +136,7 @@ export interface RootState { fileMetadata: FileMetadataFields timelineIsBeingComputed: boolean robotType: RobotType + designerTab: DesignerTabPayload['tab'] } const _allReducers = { computedRobotStateTimeline, @@ -125,6 +145,7 @@ const _allReducers = { fileMetadata, timelineIsBeingComputed, robotType: robotTypeReducer, + designerTab: designerTabReducer, } export const rootReducer: Reducer = combineReducers( _allReducers diff --git a/protocol-designer/src/file-data/selectors/fileFields.ts b/protocol-designer/src/file-data/selectors/fileFields.ts index 33824dfff79..5b66170f222 100644 --- a/protocol-designer/src/file-data/selectors/fileFields.ts +++ b/protocol-designer/src/file-data/selectors/fileFields.ts @@ -3,6 +3,7 @@ import type { BaseState, Selector } from '../../types' import type { RootState } from '../reducers' import type { FileMetadataFields } from '../types' import type { RobotType } from '@opentrons/shared-data' +import type { DesignerTabPayload } from '../actions' export const rootSelector = (state: BaseState): RootState => state.fileData export const getCurrentProtocolExists: Selector = createSelector( @@ -21,3 +22,7 @@ export const getRobotType: Selector = createSelector( rootSelector, state => state.robotType ) + +export const getDesignerTab: Selector< + DesignerTabPayload['tab'] +> = createSelector(rootSelector, state => state.designerTab) diff --git a/protocol-designer/src/file-data/types.ts b/protocol-designer/src/file-data/types.ts index dc784b51c75..9b64f3e26b6 100644 --- a/protocol-designer/src/file-data/types.ts +++ b/protocol-designer/src/file-data/types.ts @@ -1,7 +1,13 @@ import type { ProtocolFile } from '@opentrons/shared-data' +import type { DesignerTabPayload } from './actions' export type FileMetadataFields = ProtocolFile<{}>['metadata'] export type FileMetadataFieldAccessors = keyof FileMetadataFields export interface SaveFileMetadataAction { type: 'SAVE_FILE_METADATA' payload: FileMetadataFields } + +export interface SelectDesignerTabAction { + type: 'SELECT_DESIGNER_TAB' + payload: DesignerTabPayload +} diff --git a/protocol-designer/src/organisms/Alerts/ErrorContents.tsx b/protocol-designer/src/organisms/Alerts/ErrorContents.tsx index 23754599ff4..73dfa60f995 100644 --- a/protocol-designer/src/organisms/Alerts/ErrorContents.tsx +++ b/protocol-designer/src/organisms/Alerts/ErrorContents.tsx @@ -1,7 +1,14 @@ import { useTranslation } from 'react-i18next' -import { START_TERMINAL_ITEM_ID } from '../../steplist' -import { KnowledgeBaseLink } from '../../components/KnowledgeBaseLink' -import { TerminalItemLink } from './TerminalItemLink' +import { useDispatch } from 'react-redux' +import { + Btn, + Flex, + JUSTIFY_SPACE_BETWEEN, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' +import { BUTTON_LINK_STYLE } from '../../atoms' +import { selectDesignerTab } from '../../file-data/actions' import type { AlertLevel } from './types' @@ -14,6 +21,7 @@ export const ErrorContents = ( ): JSX.Element | null => { const { errorType, level } = props const { t } = useTranslation(['alert', 'shared']) + const dispatch = useDispatch() if (level === 'timeline') { const bodyText = t(`timeline.error.${errorType}.body`, { @@ -22,29 +30,42 @@ export const ErrorContents = ( switch (errorType) { case 'INSUFFICIENT_TIPS': return ( - <> + {bodyText} - - - ) - case 'MODULE_PIPETTE_COLLISION_DANGER': - return ( - <> - {bodyText} - - {t('shared:here')} - - + { + dispatch(selectDesignerTab({ tab: 'startingDeck' })) + }} + > + {t(`timeline.error.${errorType}.link`)} + + ) - case 'NO_TIP_ON_PIPETTE': + case 'REMOVE_96_CHANNEL_TIPRACK_ADAPTER': + case 'MISSING_96_CHANNEL_TIPRACK_ADAPTER': return ( - <> - {t(`timeline.error.${errorType}.body1`)} - + + {t(`timeline.error.${errorType}.body`)} + { + dispatch(selectDesignerTab({ tab: 'startingDeck' })) + }} + > {t(`timeline.error.${errorType}.link`)} - - {t(`timeline.error.${errorType}.body2`)} - + + ) default: return bodyText diff --git a/protocol-designer/src/organisms/Alerts/TimelineAlerts.tsx b/protocol-designer/src/organisms/Alerts/TimelineAlerts.tsx index 7e793723acf..4df442b44f7 100644 --- a/protocol-designer/src/organisms/Alerts/TimelineAlerts.tsx +++ b/protocol-designer/src/organisms/Alerts/TimelineAlerts.tsx @@ -3,7 +3,6 @@ import { memo } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { - ALIGN_CENTER, Banner, DIRECTION_COLUMN, Flex, @@ -31,13 +30,9 @@ function TimelineAlertsComponent(): JSX.Element { - + {data.title} {data.description} diff --git a/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx b/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx index edbb682b38c..b069da5534f 100644 --- a/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx +++ b/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx @@ -54,9 +54,9 @@ describe('FormAlerts', () => { }, ]) render(props) - screen.getByText('Moving labware into waste chute') + screen.getByText('Disposing liquid-filled labware') screen.getByText( - 'This labware has remaining liquid, be advised that once you dispose of it, there is no way to get it back later in the protocol.' + 'This step moves a labware that contains liquid to the waste chute. There is no way to retrieve the liquid after disposal.' ) fireEvent.click(screen.getByTestId('Banner_close-button')) expect(vi.mocked(dismissTimelineWarning)).toHaveBeenCalled() diff --git a/protocol-designer/src/organisms/Alerts/__tests__/TimelineAlerts.test.tsx b/protocol-designer/src/organisms/Alerts/__tests__/TimelineAlerts.test.tsx index bf73edde76a..d2e0667ac17 100644 --- a/protocol-designer/src/organisms/Alerts/__tests__/TimelineAlerts.test.tsx +++ b/protocol-designer/src/organisms/Alerts/__tests__/TimelineAlerts.test.tsx @@ -4,11 +4,11 @@ import { fireEvent, screen } from '@testing-library/react' import { i18n } from '../../../assets/localization' import { renderWithProviders } from '../../../__testing-utils__' import { getRobotStateTimeline } from '../../../file-data/selectors' -import { selectTerminalItem } from '../../../ui/steps/actions/actions' +import { selectDesignerTab } from '../../../file-data/actions' import { TimelineAlerts } from '../TimelineAlerts' vi.mock('../../../file-data/selectors') -vi.mock('../../../ui/steps/actions/actions') +vi.mock('../../../file-data/actions') const render = () => { return renderWithProviders(, { @@ -27,9 +27,11 @@ describe('TimelineAlerts', () => { it('renders the insufficient tips timeline error and clicking on the button turns it into the starting deck state terminal id ', () => { render() screen.getByText('Not enough tips to complete action') - screen.getByText('Add another tip rack to an empty slot in') - fireEvent.click(screen.getByText('Starting Deck State')) - expect(vi.mocked(selectTerminalItem)).toHaveBeenCalled() + screen.getByText( + 'Add another tip rack to your deck or change your tip management during transfer and mix steps.' + ) + fireEvent.click(screen.getByText('Edit starting deck')) + expect(vi.mocked(selectDesignerTab)).toHaveBeenCalled() }) it('renders the no tip on pipette timeline error and the knowledge link', () => { vi.mocked(getRobotStateTimeline).mockReturnValue({ @@ -37,7 +39,6 @@ describe('TimelineAlerts', () => { errors: [{ message: 'mockMessage', type: 'NO_TIP_ON_PIPETTE' }], }) render() - screen.getByText('No tip on pipette at the start of step') - screen.getByText('Air gap dispense setting') + screen.getByText('No tip on pipette at start of step') }) }) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx index a8d0915a59d..20ad1307919 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx @@ -30,7 +30,7 @@ import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locati import { getDisableModuleRestrictions } from '../../../feature-flags/selectors' import { getRobotType } from '../../../file-data/selectors' import { getHasGen1MultiChannelPipette } from '../../../step-forms' -import { SlotDetailsContainer, TimelineAlerts } from '../../../organisms' +import { SlotDetailsContainer } from '../../../organisms' import { selectZoomedIntoSlot } from '../../../labware-ingred/actions' import { selectors } from '../../../labware-ingred/selectors' import { DeckSetupDetails } from './DeckSetupDetails' @@ -171,182 +171,175 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { ) return ( - - {tab === 'protocolSteps' ? ( - - - - ) : null} - + + - - - {() => ( - <> - {robotType === OT2_ROBOT_TYPE ? ( - - ) : ( - <> - {filteredAddressableAreas.map(addressableArea => { - const cutoutId = getCutoutIdForAddressableArea( - addressableArea.id, - deckDef.cutoutFixtures + {() => ( + <> + {robotType === OT2_ROBOT_TYPE ? ( + + ) : ( + <> + {filteredAddressableAreas.map(addressableArea => { + const cutoutId = getCutoutIdForAddressableArea( + addressableArea.id, + deckDef.cutoutFixtures + ) + return cutoutId != null ? ( + + ) : null + })} + {stagingAreaFixtures.map(fixture => { + if ( + zoomIn.cutout == null || + zoomIn.cutout !== fixture.location + ) { + return ( + + ) + } + })} + {trash != null + ? trashBinFixtures.map(({ cutoutId }) => + cutoutId != null && + (zoomIn.cutout == null || + zoomIn.cutout !== cutoutId) ? ( + + + + + ) : null ) - return cutoutId != null ? ( - { + if ( + zoomIn.cutout == null || + zoomIn.cutout !== fixture.location + ) { + return ( + - ) : null - })} - {stagingAreaFixtures.map(fixture => { - if ( - zoomIn.cutout == null || - zoomIn.cutout !== fixture.location - ) { - return ( - - ) - } - })} - {trash != null - ? trashBinFixtures.map(({ cutoutId }) => - cutoutId != null && - (zoomIn.cutout == null || - zoomIn.cutout !== cutoutId) ? ( - - - - - ) : null - ) - : null} - {wasteChuteFixtures.map(fixture => { - if ( - zoomIn.cutout == null || - zoomIn.cutout !== fixture.location - ) { - return ( - - ) - } - })} - {wasteChuteStagingAreaFixtures.map(fixture => { - if ( - zoomIn.cutout == null || - zoomIn.cutout !== fixture.location - ) { - return ( - - ) - } - })} - + ) + } + })} + {wasteChuteStagingAreaFixtures.map(fixture => { + if ( + zoomIn.cutout == null || + zoomIn.cutout !== fixture.location + ) { + return ( + + ) + } + })} + + )} + areas.location as CutoutId )} - areas.location as CutoutId - )} - {...{ - deckDef, - showGen1MultichannelCollisionWarnings, - }} - /> - + 0} + /> + {hoverSlot != null ? ( + 0} + slot={hoverSlot} /> - {hoverSlot != null ? ( - - ) : null} - - )} - - + ) : null} + + )} + - {zoomIn.slot != null && zoomIn.cutout != null ? ( - { - dispatch(selectZoomedIntoSlot({ slot: null, cutout: null })) - animateZoom({ - targetViewBox: initialViewBox, - viewBox, - setViewBox, - }) - }} - setHoveredLabware={setHoveredLabware} - /> - ) : null} + {zoomIn.slot != null && zoomIn.cutout != null ? ( + { + dispatch(selectZoomedIntoSlot({ slot: null, cutout: null })) + animateZoom({ + targetViewBox: initialViewBox, + viewBox, + setViewBox, + }) + }} + setHoveredLabware={setHoveredLabware} + /> + ) : null} ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx index 1dafa1d2c71..f68928c3488 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx @@ -11,6 +11,7 @@ import { getSelectedStepId, getSelectedSubstep, } from '../../../../ui/steps/selectors' +import { getDesignerTab } from '../../../../file-data/selectors' import { getEnableHotKeysDisplay } from '../../../../feature-flags/selectors' import { DeckSetupContainer } from '../../DeckSetup' import { OffDeck } from '../../Offdeck' @@ -27,7 +28,8 @@ vi.mock('../../DeckSetup') vi.mock('../StepSummary.tsx') vi.mock('../Timeline') vi.mock('../../../../feature-flags/selectors') - +vi.mock('../../../../file-data/selectors') +vi.mock('../../../../organisms/Alerts') const render = () => { return renderWithProviders(, { i18nInstance: i18n, @@ -53,6 +55,7 @@ const MOCK_STEP_FORMS = { describe('ProtocolSteps', () => { beforeEach(() => { + vi.mocked(getDesignerTab).mockReturnValue('protocolSteps') vi.mocked(TimelineToolbox).mockReturnValue(
mock TimelineToolbox
) vi.mocked(DeckSetupContainer).mockReturnValue(
mock DeckSetupContainer
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx index c2ca9ced4ee..3d51d8ef1f1 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx @@ -7,6 +7,7 @@ import { COLORS, DIRECTION_COLUMN, Flex, + JUSTIFY_CENTER, JUSTIFY_FLEX_END, JUSTIFY_FLEX_START, JUSTIFY_SPACE_BETWEEN, @@ -33,6 +34,8 @@ import { TimelineToolbox, SubstepsToolbox } from './Timeline' import { StepForm } from './StepForm' import { StepSummary } from './StepSummary' import { BatchEditToolbox } from './BatchEditToolbox' +import { getDesignerTab } from '../../../file-data/selectors' +import { TimelineAlerts } from '../../../organisms' export function ProtocolSteps(): JSX.Element { const { i18n, t } = useTranslation('starting_deck_state') @@ -40,6 +43,7 @@ export function ProtocolSteps(): JSX.Element { const isMultiSelectMode = useSelector(getIsMultiSelectMode) const selectedSubstep = useSelector(getSelectedSubstep) const enableHoyKeyDisplay = useSelector(getEnableHotKeysDisplay) + const tab = useSelector(getDesignerTab) const leftString = t('onDeck') const rightString = t('offDeck') const [deckView, setDeckView] = useState< @@ -76,6 +80,16 @@ export function ProtocolSteps(): JSX.Element { justifyContent={JUSTIFY_FLEX_START} > + {tab === 'protocolSteps' ? ( + + + + ) : null} { describe('Designer', () => { beforeEach(() => { + vi.mocked(getDesignerTab).mockReturnValue('startingDeck') vi.mocked(ProtocolSteps).mockReturnValue(
mock ProtocolSteps
) vi.mocked(getFileMetadata).mockReturnValue({ protocolName: 'mockProtocolName', @@ -103,6 +104,7 @@ describe('Designer', () => { }) it('renders the protocol steps page', () => { + vi.mocked(getDesignerTab).mockReturnValue('protocolSteps') render() fireEvent.click(screen.getByText('Protocol steps')) screen.getByText('mock ProtocolSteps') diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx index f57fe016076..6dfd952ba97 100644 --- a/protocol-designer/src/pages/Designer/index.tsx +++ b/protocol-designer/src/pages/Designer/index.tsx @@ -23,7 +23,8 @@ import { useKitchen } from '../../organisms/Kitchen/hooks' import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations' import { generateNewProtocol } from '../../labware-ingred/actions' import { DefineLiquidsModal, ProtocolMetadataNav } from '../../organisms' -import { getFileMetadata } from '../../file-data/selectors' +import { selectDesignerTab } from '../../file-data/actions' +import { getDesignerTab, getFileMetadata } from '../../file-data/selectors' import { DeckSetupContainer } from './DeckSetup' import { selectors } from '../../labware-ingred/selectors' import { OffDeck } from './Offdeck' @@ -53,9 +54,7 @@ export function Designer(): JSX.Element { const isNewProtocol = useSelector(selectors.getIsNewProtocol) const [liquidOverflowMenu, showLiquidOverflowMenu] = useState(false) const [showDefineLiquidModal, setDefineLiquidModal] = useState(false) - const [tab, setTab] = useState<'startingDeck' | 'protocolSteps'>( - 'startingDeck' - ) + const tab = useSelector(getDesignerTab) const leftString = t('onDeck') const rightString = t('offDeck') @@ -73,7 +72,7 @@ export function Designer(): JSX.Element { text: t('protocol_starting_deck'), isActive: tab === 'startingDeck', onClick: () => { - setTab('startingDeck') + dispatch(selectDesignerTab({ tab: 'startingDeck' })) }, } const protocolStepTab = { @@ -81,7 +80,7 @@ export function Designer(): JSX.Element { isActive: tab === 'protocolSteps', onClick: () => { if (hasTrashEntity) { - setTab('protocolSteps') + dispatch(selectDesignerTab({ tab: 'protocolSteps' })) } else { makeSnackbar(t('trash_required') as string) } diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index 663dac9388b..3d6726b8be2 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -64,7 +64,6 @@ export const aspirate: CommandCreator = ( if (!pipetteSpec) { errors.push( errorCreators.pipetteDoesNotExist({ - actionName, pipette, }) ) diff --git a/step-generation/src/commandCreators/atomic/blowout.ts b/step-generation/src/commandCreators/atomic/blowout.ts index b56c57fc9db..8e97fc3114b 100644 --- a/step-generation/src/commandCreators/atomic/blowout.ts +++ b/step-generation/src/commandCreators/atomic/blowout.ts @@ -27,7 +27,6 @@ export const blowout: CommandCreator = ( if (!pipetteData) { errors.push( errorCreators.pipetteDoesNotExist({ - actionName, pipette: pipetteId, }) ) diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index 31adbe7a5ab..c06e0035f7b 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -61,7 +61,6 @@ export const dispense: CommandCreator = ( if (!pipetteSpec) { errors.push( errorCreators.pipetteDoesNotExist({ - actionName, pipette, }) ) diff --git a/step-generation/src/commandCreators/atomic/moveToWell.ts b/step-generation/src/commandCreators/atomic/moveToWell.ts index 34c36a1eb01..03ed52231bb 100644 --- a/step-generation/src/commandCreators/atomic/moveToWell.ts +++ b/step-generation/src/commandCreators/atomic/moveToWell.ts @@ -44,7 +44,6 @@ export const moveToWell: CommandCreator = ( if (!pipetteSpec) { errors.push( errorCreators.pipetteDoesNotExist({ - actionName, pipette, }) ) diff --git a/step-generation/src/commandCreators/atomic/replaceTip.ts b/step-generation/src/commandCreators/atomic/replaceTip.ts index 57828fe61e5..c516a1a4012 100644 --- a/step-generation/src/commandCreators/atomic/replaceTip.ts +++ b/step-generation/src/commandCreators/atomic/replaceTip.ts @@ -162,7 +162,6 @@ export const replaceTip: CommandCreator = ( return { errors: [ errorCreators.pipetteDoesNotExist({ - actionName: 'replaceTip', pipette, }), ], diff --git a/step-generation/src/commandCreators/atomic/touchTip.ts b/step-generation/src/commandCreators/atomic/touchTip.ts index c84ccd61762..75047c0b610 100644 --- a/step-generation/src/commandCreators/atomic/touchTip.ts +++ b/step-generation/src/commandCreators/atomic/touchTip.ts @@ -18,7 +18,6 @@ export const touchTip: CommandCreator = ( if (!pipetteData) { errors.push( pipetteDoesNotExist({ - actionName, pipette, }) ) diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index f60fa7b0ab1..94d20e255f0 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -85,7 +85,6 @@ export const consolidate: CommandCreator = ( nozzles, } = args - const actionName = 'consolidate' const pipetteData = prevRobotState.pipettes[args.pipette] const is96Channel = invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96 @@ -95,7 +94,6 @@ export const consolidate: CommandCreator = ( return { errors: [ errorCreators.pipetteDoesNotExist({ - actionName, pipette: args.pipette, }), ], diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index 824435aeaaf..9c4c2cc865e 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -88,7 +88,6 @@ export const distribute: CommandCreator = ( ) { errors.push( errorCreators.pipetteDoesNotExist({ - actionName, pipette: args.pipette, }) ) diff --git a/step-generation/src/commandCreators/compound/mix.ts b/step-generation/src/commandCreators/compound/mix.ts index dbfedcbf337..15ceb73221c 100644 --- a/step-generation/src/commandCreators/compound/mix.ts +++ b/step-generation/src/commandCreators/compound/mix.ts @@ -164,7 +164,6 @@ export const mix: CommandCreator = ( return { errors: [ errorCreators.pipetteDoesNotExist({ - actionName, pipette, }), ], diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index f2bc471b911..b6d8a508ec2 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -118,7 +118,6 @@ export const transfer: CommandCreator = ( // bail out before doing anything else errors.push( errorCreators.pipetteDoesNotExist({ - actionName, pipette: args.pipette, }) ) diff --git a/step-generation/src/errorCreators.ts b/step-generation/src/errorCreators.ts index a541c65e4ea..1684a129900 100644 --- a/step-generation/src/errorCreators.ts +++ b/step-generation/src/errorCreators.ts @@ -47,12 +47,11 @@ export function pipetteHasTip(): CommandCreatorError { } export function pipetteDoesNotExist(args: { - actionName: string pipette: string }): CommandCreatorError { - const { actionName, pipette } = args + const { pipette } = args return { - message: `Attempted to ${actionName} with pipette id "${pipette}", this pipette was not found under "pipettes"`, + message: `This step tries to use the ${pipette}. Add the pipette to your protocol or change the step to use a different pipette.`, type: 'PIPETTE_DOES_NOT_EXIST', } } @@ -77,7 +76,7 @@ export function labwareDoesNotExist(args: { `Attempted to ${actionName} with labware id "${labware}", this labware was not found under "labware"` ) return { - message: 'A step involves labware that has been deleted', + message: `This step tries to use ${labware}. Add the labware to your protocol or change the step to use a different labware.`, type: 'LABWARE_DOES_NOT_EXIST', } } @@ -102,9 +101,9 @@ export function tipVolumeExceeded(args: { volume: string | number maxVolume: string | number }): CommandCreatorError { - const { actionName, volume, maxVolume } = args + const { volume, maxVolume, actionName } = args return { - message: `Attempted to ${actionName} volume greater than tip max volume (${volume} > ${maxVolume})`, + message: `This step tries to ${actionName} ${volume}μL, but the tip can only hold ${maxVolume}μL.`, type: 'TIP_VOLUME_EXCEEDED', } } @@ -119,7 +118,7 @@ export function pipetteVolumeExceeded(args: { const message = disposalVolume != null ? `Attemped to ${actionName} volume + disposal volume greater than pipette max volume (${volume} + ${disposalVolume} > ${maxVolume})` - : `Attempted to ${actionName} volume greater than pipette max volume (${volume} > ${maxVolume})` + : `This step tries to ${actionName} ${volume}μL, but the tip can only hold ${maxVolume}μL.` return { message, type: 'PIPETTE_VOLUME_EXCEEDED', @@ -236,7 +235,7 @@ export const dropTipLocationDoesNotExist = (): CommandCreatorError => { export const equipmentDoesNotExist = (): CommandCreatorError => { return { type: 'EQUIPMENT_DOES_NOT_EXIST', - message: `The equipment does not exist`, + message: `Equipment does not exist.`, } }