diff --git a/components/src/atoms/ListButton/__tests__/ListButton.test.tsx b/components/src/atoms/ListButton/__tests__/ListButton.test.tsx index fe4e3fb7349..2d65b14c61c 100644 --- a/components/src/atoms/ListButton/__tests__/ListButton.test.tsx +++ b/components/src/atoms/ListButton/__tests__/ListButton.test.tsx @@ -25,7 +25,7 @@ describe('ListButton', () => { it('should render correct style - noActive', () => { render(props) const listButton = screen.getByTestId('ListButton_noActive') - expect(listButton).toHaveStyle(`backgroundColor: ${COLORS.grey30}`) + expect(listButton).toHaveStyle(`backgroundColor: ${COLORS.grey20}`) }) it('should render correct style - connected', () => { props.type = 'connected' diff --git a/components/src/atoms/ListButton/index.tsx b/components/src/atoms/ListButton/index.tsx index 610c76bb7a9..3125f190ec1 100644 --- a/components/src/atoms/ListButton/index.tsx +++ b/components/src/atoms/ListButton/index.tsx @@ -23,8 +23,8 @@ const LISTBUTTON_PROPS_BY_TYPE: Record< { backgroundColor: string; hoverBackgroundColor: string } > = { noActive: { - backgroundColor: COLORS.grey30, - hoverBackgroundColor: COLORS.grey35, + backgroundColor: COLORS.grey20, + hoverBackgroundColor: COLORS.grey30, }, connected: { backgroundColor: COLORS.green30, @@ -60,6 +60,11 @@ export function ListButton(props: ListButtonProps): JSX.Element { ? COLORS.grey20 : listButtonProps.hoverBackgroundColor}; } + + &:focus-visible { + outline: 2px solid ${COLORS.blue50}; + outline-offset: 0.25rem; + } ` return ( @@ -67,6 +72,7 @@ export function ListButton(props: ListButtonProps): JSX.Element { data-testid={`ListButton_${type}`} onClick={onClick} css={LIST_BUTTON_STYLE} + tabIndex={0} {...styleProps} > {children} diff --git a/components/src/atoms/buttons/EmptySelectorButton.tsx b/components/src/atoms/buttons/EmptySelectorButton.tsx index da34a8ba710..119ff041ba9 100644 --- a/components/src/atoms/buttons/EmptySelectorButton.tsx +++ b/components/src/atoms/buttons/EmptySelectorButton.tsx @@ -4,7 +4,6 @@ import { ALIGN_CENTER, CURSOR_DEFAULT, CURSOR_POINTER, - FLEX_MAX_CONTENT, Icon, JUSTIFY_CENTER, JUSTIFY_START, @@ -62,8 +61,8 @@ interface ButtonProps { const StyledButton = styled.button` border: none; - width: ${FLEX_MAX_CONTENT}; - height: ${FLEX_MAX_CONTENT}; + width: 100%; + height: 100%; cursor: ${CURSOR_POINTER}; background-color: ${COLORS.blue30}; border-radius: ${BORDERS.borderRadius8}; diff --git a/components/src/atoms/buttons/LargeButton.tsx b/components/src/atoms/buttons/LargeButton.tsx index cea665221fe..cccf1c0fd09 100644 --- a/components/src/atoms/buttons/LargeButton.tsx +++ b/components/src/atoms/buttons/LargeButton.tsx @@ -206,7 +206,9 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { border: ${buttonType === 'stroke' ? `2px solid ${COLORS.blue55}` - : `${computedBorderStyle()}`}; + : buttonType === 'primary' + ? `4px solid ${COLORS.blue55}` + : computedBorderStyle()}; } &:focus-visible { diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx index 276be51511a..3a367dcf2e4 100644 --- a/components/src/molecules/DropdownMenu/index.tsx +++ b/components/src/molecules/DropdownMenu/index.tsx @@ -27,6 +27,7 @@ import { LiquidIcon } from '../LiquidIcon' import { DeckInfoLabel } from '../DeckInfoLabel' import type { FocusEventHandler } from 'react' +import type { FlattenSimpleInterpolation } from 'styled-components' export interface DropdownOption { name: string @@ -268,7 +269,7 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { > {currentOption.name} @@ -327,12 +328,15 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { flexDirection={DIRECTION_COLUMN} gridGap={option.subtext != null ? SPACING.spacing4 : '0'} > - + {option.name} {option.subtext} @@ -363,12 +367,17 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { ) } -const LINE_CLAMP_TEXT_STYLE = css` +export const LINE_CLAMP_TEXT_STYLE = ( + lineClamp?: number, + title?: boolean +): FlattenSimpleInterpolation => css` display: -webkit-box; -webkit-box-orient: vertical; overflow: ${OVERFLOW_HIDDEN}; text-overflow: ellipsis; word-wrap: break-word; - -webkit-line-clamp: 1; - word-break: break-all; + -webkit-line-clamp: ${lineClamp ?? 1}; + word-break: ${title === true + ? 'normal' + : 'break-all'}; // normal for tile and break-all for a non word case like aaaaaaaa ` diff --git a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json index ea1d1cb31f3..b6edc2e321a 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json @@ -25,6 +25,10 @@ "form": [], "timeline": [] }, + "dismissedWarnings": { + "form": [], + "timeline": [] + }, "ingredients": { "0": { "displayName": "Water", @@ -116,6 +120,86 @@ "volume": 100 } } + "A1": { + "0": { + "volume": 100 + } + }, + "B1": { + "0": { + "volume": 100 + } + }, + "C1": { + "0": { + "volume": 100 + } + }, + "D1": { + "0": { + "volume": 100 + } + }, + "E1": { + "0": { + "volume": 100 + } + }, + "F1": { + "0": { + "volume": 100 + } + }, + "G1": { + "0": { + "volume": 100 + } + }, + "H1": { + "0": { + "volume": 100 + } + }, + "A2": { + "0": { + "volume": 100 + } + }, + "B2": { + "0": { + "volume": 100 + } + }, + "C2": { + "0": { + "volume": 100 + } + }, + "D2": { + "0": { + "volume": 100 + } + }, + "E2": { + "0": { + "volume": 100 + } + }, + "F2": { + "0": { + "volume": 100 + } + }, + "G2": { + "0": { + "volume": 100 + } + }, + "H2": { + "0": { + "volume": 100 + } + } } }, "savedStepForms": { @@ -1336,6 +1420,11 @@ "y": 0, "z": 0 } + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } }, "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1": { "ordering": [ @@ -2236,6 +2325,9 @@ }, "groups": [ { + "metadata": { + "wellBottomShape": "v" + }, "metadata": { "wellBottomShape": "v" }, @@ -2354,6 +2446,11 @@ "y": 0, "z": 0 } + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } }, "opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1": { "ordering": [ @@ -2647,6 +2744,11 @@ "brandId": [], "links": [] } + "brand": { + "brand": "generic", + "brandId": [], + "links": [] + } } ], "cornerOffsetFromSlot": { @@ -2654,6 +2756,11 @@ "y": 0, "z": 0 } + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } } }, "liquidSchemaId": "opentronsLiquidSchemaV1", @@ -2683,6 +2790,9 @@ "location": { "slotName": "1" }, + "location": { + "slotName": "1" + }, "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType" } }, @@ -2694,6 +2804,9 @@ "location": { "slotName": "3" }, + "location": { + "slotName": "3" + }, "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType" } }, @@ -2709,6 +2822,9 @@ "location": { "slotName": "2" } + "location": { + "slotName": "2" + } } }, { @@ -2813,6 +2929,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 46.43 } @@ -2832,6 +2953,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 0.5, + "x": 0, + "y": 0 + } }, "flowRate": 46.43 } @@ -2847,6 +2973,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -2881,6 +3012,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 46.43 } @@ -2900,6 +3036,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 0.5, + "x": 0, + "y": 0 + } }, "flowRate": 46.43 } @@ -2915,6 +3056,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, diff --git a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json index 5ceb1bedabb..5e4110ac6d0 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json @@ -28,6 +28,10 @@ "form": [], "timeline": [] }, + "dismissedWarnings": { + "form": [], + "timeline": [] + }, "ingredients": { "0": { "displayName": "Water", @@ -86,6 +90,46 @@ "volume": 100 } } + "A1": { + "0": { + "volume": 100 + } + }, + "B1": { + "0": { + "volume": 100 + } + }, + "C1": { + "0": { + "volume": 100 + } + }, + "D1": { + "0": { + "volume": 100 + } + }, + "E1": { + "0": { + "volume": 100 + } + }, + "F1": { + "0": { + "volume": 100 + } + }, + "G1": { + "0": { + "volume": 100 + } + }, + "H1": { + "0": { + "volume": 100 + } + } }, "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1": { "A1": { @@ -93,6 +137,11 @@ "volume": 1000 } } + "A1": { + "1": { + "volume": 1000 + } + } } }, "savedStepForms": { @@ -475,6 +524,10 @@ "brand": "Opentrons", "brandId": [] }, + "brand": { + "brand": "Opentrons", + "brandId": [] + }, "metadata": { "displayName": "Opentrons Flex 96 Filter Tip Rack 50 µL", "displayCategory": "tipRack", @@ -1474,12 +1527,22 @@ "y": 0, "z": 0 }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, "stackingOffsetWithLabware": { "opentrons_flex_96_tiprack_adapter": { "x": 0, "y": 0, "z": 121 } + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } } }, "opentrons/opentrons_96_flat_bottom_adapter/1": { @@ -1488,6 +1551,10 @@ "brand": "Opentrons", "brandId": [] }, + "brand": { + "brand": "Opentrons", + "brandId": [] + }, "metadata": { "displayName": "Opentrons 96 Flat Bottom Heater-Shaker Adapter", "displayCategory": "adapter", @@ -1499,6 +1566,11 @@ "yDimension": 75, "zDimension": 7.9 }, + "dimensions": { + "xDimension": 111, + "yDimension": 75, + "zDimension": 7.9 + }, "wells": {}, "groups": [ { @@ -1506,6 +1578,12 @@ "wells": [] } ], + "groups": [ + { + "metadata": {}, + "wells": [] + } + ], "parameters": { "format": "96Standard", "quirks": [], @@ -1522,6 +1600,11 @@ "y": 5.5, "z": 0 } + "cornerOffsetFromSlot": { + "x": 8.5, + "y": 5.5, + "z": 0 + } }, "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2": { "ordering": [ @@ -2424,6 +2507,9 @@ }, "groups": [ { + "metadata": { + "wellBottomShape": "v" + }, "metadata": { "wellBottomShape": "v" }, @@ -2542,6 +2628,11 @@ "y": 0, "z": 0 }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, "stackingOffsetWithLabware": { "opentrons_96_pcr_adapter": { "x": 0, @@ -2553,6 +2644,16 @@ "y": 0, "z": 12.66 } + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } }, "stackingOffsetWithModule": { "thermocyclerModuleV2": { @@ -2560,6 +2661,11 @@ "y": 0, "z": 10.8 } + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } } }, "opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1": { @@ -2862,6 +2968,11 @@ "y": 0, "z": 0 } + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } }, "opentrons/nest_96_wellplate_200ul_flat/2": { "ordering": [ @@ -3766,6 +3877,9 @@ }, "groups": [ { + "metadata": { + "wellBottomShape": "flat" + }, "metadata": { "wellBottomShape": "flat" }, @@ -3883,6 +3997,11 @@ "y": 0, "z": 0 }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, "stackingOffsetWithLabware": { "opentrons_96_flat_bottom_adapter": { "x": 0, @@ -3894,6 +4013,16 @@ "y": 0, "z": 5.55 } + "opentrons_96_flat_bottom_adapter": { + "x": 0, + "y": 0, + "z": 6.7 + }, + "opentrons_aluminum_flat_bottom_plate": { + "x": 0, + "y": 0, + "z": 5.55 + } } } }, @@ -3938,6 +4067,9 @@ "location": { "slotName": "D2" }, + "location": { + "slotName": "D2" + }, "moduleId": "1be16305-74e7-4bdb-9737-61ec726d2b44:magneticBlockType" } }, @@ -3949,6 +4081,9 @@ "location": { "slotName": "D1" }, + "location": { + "slotName": "D1" + }, "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, @@ -3960,6 +4095,9 @@ "location": { "slotName": "D3" }, + "location": { + "slotName": "D3" + }, "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType" } }, @@ -3971,6 +4109,9 @@ "location": { "slotName": "B1" }, + "location": { + "slotName": "B1" + }, "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, @@ -4000,6 +4141,9 @@ "location": { "slotName": "C1" } + "location": { + "slotName": "C1" + } } }, { @@ -4053,6 +4197,9 @@ "volumeByWell": { "A1": 1000 } + "volumeByWell": { + "A1": 1000 + } } }, { @@ -4125,6 +4272,14 @@ "holdSeconds": 120, "celsius": 10 } + { + "holdSeconds": 60, + "celsius": 4 + }, + { + "holdSeconds": 120, + "celsius": 10 + } ], "blockMaxVolumeUl": 10 } @@ -4174,6 +4329,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4193,6 +4353,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4208,6 +4373,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4242,6 +4412,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4261,6 +4436,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4276,6 +4456,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4310,6 +4495,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4329,6 +4519,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4344,6 +4539,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4378,6 +4578,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4397,6 +4602,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4412,6 +4622,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4446,6 +4661,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4465,6 +4685,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4480,6 +4705,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4514,6 +4744,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4533,6 +4768,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4548,6 +4788,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4582,6 +4827,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4601,6 +4851,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4616,6 +4871,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4650,6 +4910,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4669,6 +4934,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4684,6 +4954,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4718,6 +4993,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4737,6 +5017,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4752,6 +5037,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4786,6 +5076,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4805,6 +5100,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4820,6 +5120,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4854,6 +5159,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4873,6 +5183,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4888,6 +5203,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4922,6 +5242,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4941,6 +5266,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -4956,6 +5286,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -4990,6 +5325,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -5009,6 +5349,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -5024,6 +5369,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -5058,6 +5408,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -5077,6 +5432,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -5092,6 +5452,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -5126,6 +5491,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -5145,6 +5515,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -5160,6 +5535,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -5194,6 +5574,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -5213,6 +5598,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 478 } @@ -5228,6 +5618,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -5270,6 +5665,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 0.5, + "x": 0, + "y": 0 + } }, "flowRate": 35 } @@ -5289,6 +5689,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 0.5, + "x": 0, + "y": 0 + } }, "flowRate": 57 } @@ -5308,6 +5713,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 0.5, + "x": 0, + "y": 0 + } }, "flowRate": 35 } @@ -5327,6 +5737,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 0.5, + "x": 0, + "y": 0 + } }, "flowRate": 57 } @@ -5342,6 +5757,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -5361,6 +5781,9 @@ "newLocation": { "slotName": "B2" } + "newLocation": { + "slotName": "B2" + } } }, { @@ -5380,6 +5803,9 @@ "newLocation": { "slotName": "C3" } + "newLocation": { + "slotName": "C3" + } } }, { @@ -5434,6 +5860,13 @@ "newLocation": "offDeck" } }, + { + "commandType": "temperatureModule/deactivate", + "key": "90961514-df6e-4884-9eb7-cf8e03e0f6ae", + "params": { + "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType" + } + }, { "commandType": "moveLabware", "key": "2c067101-8353-4bfa-ad4b-869569ce4923", @@ -5443,6 +5876,9 @@ "newLocation": { "slotName": "C2" } + "newLocation": { + "slotName": "C2" + } } } ], diff --git a/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json b/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json index e41bc40b181..7af2bca994f 100644 --- a/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json +++ b/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json @@ -25,6 +25,10 @@ "form": [], "timeline": [] }, + "dismissedWarnings": { + "form": [], + "timeline": [] + }, "ingredients": {}, "ingredLocations": {}, "savedStepForms": { @@ -239,6 +243,10 @@ "brand": "Opentrons", "brandId": [] }, + "brand": { + "brand": "Opentrons", + "brandId": [] + }, "metadata": { "displayName": "Opentrons Flex 96 Tip Rack 50 µL", "displayCategory": "tipRack", @@ -1238,12 +1246,22 @@ "y": 0, "z": 0 }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, "stackingOffsetWithLabware": { "opentrons_flex_96_tiprack_adapter": { "x": 0, "y": 0, "z": 121 } + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } } }, "opentrons/opentrons_96_well_aluminum_block/1": { @@ -1265,6 +1283,10 @@ "brand": "Opentrons", "brandId": [] }, + "brand": { + "brand": "Opentrons", + "brandId": [] + }, "metadata": { "displayName": "Opentrons 96 Well Aluminum Block", "displayCategory": "aluminumBlock", @@ -2144,6 +2166,9 @@ }, "groups": [ { + "metadata": { + "wellBottomShape": "v" + }, "metadata": { "wellBottomShape": "v" }, @@ -2263,6 +2288,11 @@ "y": 0, "z": 0 }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, "gripperOffsets": { "default": { "pickUpOffset": { @@ -2275,6 +2305,16 @@ "y": 0, "z": 1 } + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 1 + } } } }, @@ -2576,6 +2616,11 @@ "y": 0, "z": 0 } + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } }, "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2": { "ordering": [ @@ -3478,6 +3523,9 @@ }, "groups": [ { + "metadata": { + "wellBottomShape": "v" + }, "metadata": { "wellBottomShape": "v" }, @@ -3596,6 +3644,11 @@ "y": 0, "z": 0 }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, "stackingOffsetWithLabware": { "opentrons_96_pcr_adapter": { "x": 0, @@ -3607,6 +3660,16 @@ "y": 0, "z": 12.66 } + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } }, "stackingOffsetWithModule": { "thermocyclerModuleV2": { @@ -3614,6 +3677,11 @@ "y": 0, "z": 10.8 } + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } } } }, @@ -3638,6 +3706,9 @@ "location": { "slotName": "D3" }, + "location": { + "slotName": "D3" + }, "moduleId": "d6966555-6c0e-45e0-8056-428d7c486401:temperatureModuleType" } }, @@ -3649,6 +3720,9 @@ "location": { "slotName": "C3" }, + "location": { + "slotName": "C3" + }, "moduleId": "b9c56153-9026-42d1-8113-949e15254571:temperatureModuleType" } }, @@ -3678,6 +3752,9 @@ "location": { "slotName": "C2" } + "location": { + "slotName": "C2" + } } }, { @@ -3740,6 +3817,11 @@ "x": 2, "y": -2 } + "offset": { + "z": 29, + "x": 2, + "y": -2 + } }, "flowRate": 35 } @@ -3759,6 +3841,11 @@ "x": 0, "y": 0 } + "offset": { + "z": 1, + "x": 0, + "y": 0 + } }, "flowRate": 57 } @@ -3777,6 +3864,12 @@ "z": -12 } } + "wellLocation": { + "origin": "top", + "offset": { + "z": -12 + } + } } }, { @@ -3790,6 +3883,11 @@ "y": 0, "z": 0 }, + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, "alternateDropLocation": true } }, @@ -3831,6 +3929,20 @@ "moduleId": "b9c56153-9026-42d1-8113-949e15254571:temperatureModuleType", "celsius": 4 } + }, + { + "commandType": "temperatureModule/deactivate", + "key": "385c6783-55de-4c96-878d-7aeb2585b37f", + "params": { + "moduleId": "d6966555-6c0e-45e0-8056-428d7c486401:temperatureModuleType" + } + }, + { + "commandType": "temperatureModule/deactivate", + "key": "6c7a02c0-96e0-4dba-a763-78cd669a43f8", + "params": { + "moduleId": "b9c56153-9026-42d1-8113-949e15254571:temperatureModuleType" + } } ], "commandAnnotationSchemaId": "opentronsCommandAnnotationSchemaV1", diff --git a/protocol-designer/src/ProtocolEditor.tsx b/protocol-designer/src/ProtocolEditor.tsx index ec0a130abd5..770e3ee0a42 100644 --- a/protocol-designer/src/ProtocolEditor.tsx +++ b/protocol-designer/src/ProtocolEditor.tsx @@ -38,7 +38,7 @@ export function ProtocolEditor(): JSX.Element { id="protocol-editor" > - + diff --git a/protocol-designer/src/ProtocolRoutes.tsx b/protocol-designer/src/ProtocolRoutes.tsx index 854eacfa993..63c31842bbe 100644 --- a/protocol-designer/src/ProtocolRoutes.tsx +++ b/protocol-designer/src/ProtocolRoutes.tsx @@ -73,7 +73,7 @@ export function ProtocolRoutes(): JSX.Element { > - + diff --git a/protocol-designer/src/molecules/DropdownStepFormField/index.tsx b/protocol-designer/src/molecules/DropdownStepFormField/index.tsx index 98ac2d99164..ac6bac171a3 100644 --- a/protocol-designer/src/molecules/DropdownStepFormField/index.tsx +++ b/protocol-designer/src/molecules/DropdownStepFormField/index.tsx @@ -7,6 +7,7 @@ import { DeckInfoLabel, DropdownMenu, Flex, + LINE_CLAMP_TEXT_STYLE, ListItem, SPACING, StyledText, @@ -122,7 +123,10 @@ export function DropdownStepFormField( flexDirection={DIRECTION_COLUMN} gridGap={options[0].subtext != null ? SPACING.spacing4 : '0'} > - + {options[0].name} diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/PipetteOverview.tsx b/protocol-designer/src/organisms/EditInstrumentsModal/PipetteOverview.tsx index 67fefb2d40c..4703f9b4639 100644 --- a/protocol-designer/src/organisms/EditInstrumentsModal/PipetteOverview.tsx +++ b/protocol-designer/src/organisms/EditInstrumentsModal/PipetteOverview.tsx @@ -9,6 +9,7 @@ import { DIRECTION_COLUMN, DIRECTION_ROW, EmptySelectorButton, + FLEX_MAX_CONTENT, Flex, Icon, JUSTIFY_SPACE_BETWEEN, @@ -186,15 +187,17 @@ export function PipetteOverview({ ) : null} {has96Channel || (leftPipette != null && rightPipette != null) ? null : ( - { - setPage('add') - setMount(targetPipetteMount) - }} - text={t('add_pipette')} - textAlignment="left" - iconName="plus" - /> + + { + setPage('add') + setMount(targetPipetteMount) + }} + text={t('add_pipette')} + textAlignment="left" + iconName="plus" + /> + )} @@ -245,14 +248,16 @@ export function PipetteOverview({ ) : ( - { - dispatch(toggleIsGripperRequired()) - }} - text={t('protocol_overview:add_gripper')} - textAlignment="left" - iconName="plus" - /> + + { + dispatch(toggleIsGripperRequired()) + }} + text={t('protocol_overview:add_gripper')} + textAlignment="left" + iconName="plus" + /> + )} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 19eb12cb2db..0cb0039dbe5 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -8,6 +8,7 @@ import { DIRECTION_COLUMN, EmptySelectorButton, Flex, + FLEX_MAX_CONTENT, ListItem, SPACING, StyledText, @@ -99,23 +100,24 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { ) return ( - { - if (numSlotsAvailable === 0) { - makeSnackbar(t('slots_limit_reached') as string) - } else { - setValue('additionalEquipment', [ - ...additionalEquipment, - equipment, - ]) - } - }} - /> + + { + if (numSlotsAvailable === 0) { + makeSnackbar(t('slots_limit_reached') as string) + } else { + setValue('additionalEquipment', [ + ...additionalEquipment, + equipment, + ]) + } + }} + /> + ) })} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 717a6b2b762..fc09ad03517 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -5,6 +5,7 @@ import { COLORS, DIRECTION_COLUMN, EmptySelectorButton, + FLEX_MAX_CONTENT, Flex, ListItem, SPACING, @@ -320,7 +321,7 @@ function AddModuleEmptySelectorButton( return ( <> - + { - setPage('add') - setMount(targetPipetteMount) - resetFields() - }} - text={t('add_pipette')} - textAlignment="left" - iconName="plus" - /> + + { + setPage('add') + setMount(targetPipetteMount) + resetFields() + }} + text={t('add_pipette')} + textAlignment="left" + iconName="plus" + /> + )} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts index a167217bf25..c255fc56ad5 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts @@ -237,8 +237,8 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, 'stagingArea' ) - // Note: the return value is 2 because trashBin can be placed slot1 - expect(result).toBe(2) + + expect(result).toBe(1) }) it('should return 1 when there are 8 modules with 2 magnetic blocks and one trash for staging area', () => { @@ -285,14 +285,14 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, 'stagingArea' ) - expect(result).toBe(2) + expect(result).toBe(1) }) - it('should return 4 when there are 12 magnetic blocks for staging area', () => { + it('should return 0 when there are 11 magnetic blocks for staging area', () => { const mockModules = { 0: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', + slot: 'D2', }, 1: { model: MAGNETIC_BLOCK_V1, @@ -302,52 +302,47 @@ describe('getNumSlotsAvailable', () => { 2: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', + slot: 'B2', }, 3: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', + slot: 'A2', }, 4: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', + slot: 'D3', }, 5: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', + slot: 'C3', }, 6: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'D2', + slot: 'B3', }, 7: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', + slot: 'D1', }, 8: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', + slot: 'C1', }, 9: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', + slot: 'B1', }, 10: { model: MAGNETIC_BLOCK_V1, type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', - }, - 11: { - model: MAGNETIC_BLOCK_V1, - type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', + slot: 'A1', }, } as any const mockAdditionalEquipment: AdditionalEquipment[] = [] @@ -356,9 +351,39 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, 'stagingArea' ) - expect(result).toBe(4) + // Note: the return value is 0 because trashBin A3 + expect(result).toBe(0) + }) + + it('should return 3 when slots in column 1 are occupied', () => { + const mockModules = { + 0: { + model: TEMPERATURE_MODULE_V2, + type: TEMPERATURE_MODULE_TYPE, + slot: 'D1', + }, + 1: { + model: HEATERSHAKER_MODULE_V1, + type: HEATERSHAKER_MODULE_TYPE, + slot: 'C1', + }, + 2: { + model: THERMOCYCLER_MODULE_V2, + type: THERMOCYCLER_MODULE_TYPE, + slot: 'B1', + }, + } as any + const mockAdditionalEquipment: AdditionalEquipment[] = ['trashBin'] + const result = getNumSlotsAvailable( + mockModules, + mockAdditionalEquipment, + 'stagingArea' + ) + + expect(result).toBe(3) }) - it('should return 12 when there are 4 staging area for magnetic block', () => { + + it('should return 11 when there are 4 staging area for magnetic block', () => { const mockAdditionalEquipment: AdditionalEquipment[] = [ 'stagingArea', 'stagingArea', @@ -370,7 +395,7 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, MAGNETIC_BLOCK_V1 ) - expect(result).toBe(12) + expect(result).toBe(11) }) it('should return 8 when there are 4 modules, 4 staging area for magnetic block since magnetic blocks can now go on staging areas', () => { const mockModules = { @@ -401,7 +426,7 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, MAGNETIC_BLOCK_V1 ) - expect(result).toBe(8) + expect(result).toBe(7) }) }) diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx index 69abb7e6ae7..1ae5f54ac2d 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx @@ -1,13 +1,16 @@ import { + ABSORBANCE_READER_TYPE, ABSORBANCE_READER_V1, getLabwareDefURI, getLabwareDisplayName, getPipetteSpecsV2, + HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_TYPE, MAGNETIC_BLOCK_V1, MAGNETIC_MODULE_V1, MAGNETIC_MODULE_V2, + TEMPERATURE_MODULE_TYPE, TEMPERATURE_MODULE_V1, TEMPERATURE_MODULE_V2, THERMOCYCLER_MODULE_TYPE, @@ -32,8 +35,9 @@ import type { AdditionalEquipment, WizardFormState } from './types' const NUM_SLOTS_OUTER = 8 const NUM_SLOTS_MIDDLE = 4 -const NUM_SLOTS_COLUMN3 = 4 -const NUM_SLOTS_MAGNETIC_BLOCK = 12 +const NUM_SLOTS_COLUMN1 = 4 +// Note (1/31/25): change the max from 12 to 11 because of a fixture(trash bin/waste chute) +const NUM_SLOTS_MAGNETIC_BLOCK = 11 export const getNumOptions = (length: number): DropdownOption[] => { return Array.from({ length }, (_, i) => ({ @@ -42,6 +46,43 @@ export const getNumOptions = (length: number): DropdownOption[] => { })) } +// Note (1/31/25): at this moment, users allow to set one about thermocycler and need to count 2 +interface ModuleCounts { + magneticBlockCount: number + heaterShakerCount: number + temperatureCount: number + plateReaderCount: number +} + +const countModules = (modules: WizardFormState['modules']): ModuleCounts => { + return Object.values(modules || {}).reduce( + (acc, module) => { + switch (module.type) { + case MAGNETIC_BLOCK_TYPE: + acc.magneticBlockCount += 1 + break + case HEATERSHAKER_MODULE_TYPE: + acc.heaterShakerCount += 1 + break + case TEMPERATURE_MODULE_TYPE: + acc.temperatureCount += 1 + break + case ABSORBANCE_READER_TYPE: + acc.plateReaderCount += 1 + break + default: + break + } + return acc + }, + { + magneticBlockCount: 0, + heaterShakerCount: 0, + temperatureCount: 0, + plateReaderCount: 0, + } + ) +} export const getNumSlotsAvailable = ( modules: WizardFormState['modules'], additionalEquipment: WizardFormState['additionalEquipment'], @@ -115,16 +156,42 @@ export const getNumSlotsAvailable = ( } case 'stagingArea': { - const modulesWithColumn3 = - modules !== null - ? Object.values(modules).filter(module => module.slot?.includes('3')) - .length - : 0 - const fixtureSlotsWithColumn3 = - additionalEquipment !== null - ? additionalEquipment.filter(slot => slot.includes('3')).length + const { + magneticBlockCount, + heaterShakerCount, + temperatureCount, + plateReaderCount, + } = countModules(modules) + + // Note (kk: 1/31/25) magnetic modules are placed in the middle slots first + // then it will be placed in the column 1 slots and column 3 slots + // the way to distribute magnetic modules like D1 -> D3 -> C1 -> C3 + const adjustMagneticBlockCount = + magneticBlockCount - NUM_SLOTS_MIDDLE > 0 + ? magneticBlockCount - NUM_SLOTS_MIDDLE : 0 - return NUM_SLOTS_COLUMN3 - modulesWithColumn3 - fixtureSlotsWithColumn3 + + const thermocyclerModuleCount = hasTC ? 2 : 0 + + const totalModules = + adjustMagneticBlockCount + + heaterShakerCount + + temperatureCount + + thermocyclerModuleCount + + // if the following is more than 0, pd will need to keep one slot in column 3 for trash bin/waste chute + const requiredSlotInColumn3 = + totalModules - NUM_SLOTS_COLUMN1 >= 0 ? 1 : 0 + + // there is two cases pd considers + // 1. stating area can slots in column 3 because trash bin can be a slot in column 1 + // 2. not case 1 which is very limited + return totalModules <= NUM_SLOTS_COLUMN1 + ? NUM_SLOTS_COLUMN1 - plateReaderCount - requiredSlotInColumn3 + : NUM_SLOTS_OUTER - + totalModules - + plateReaderCount - + requiredSlotInColumn3 } case 'wasteChute': { diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx index 53874bb8dc0..c9c351aebb1 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx @@ -6,15 +6,15 @@ import { BORDERS, Box, COLORS, - DIRECTION_COLUMN, DeckFromLayers, + DIRECTION_COLUMN, Flex, FlexTrash, JUSTIFY_CENTER, RobotCoordinateSpaceWithRef, - SPACING, SingleSlotFixture, SlotLabels, + SPACING, StagingAreaFixture, WasteChuteFixture, WasteChuteStagingAreaFixture, @@ -59,6 +59,9 @@ import type { Fixture } from './constants' const WASTE_CHUTE_SPACE = 30 const DETAILS_HOVER_SPACE = 60 +// Note (02/02/25:kk) the size is different from the design but the product team requested keep the current size +const STARTING_DECK_VIEW_MIN_WIDTH = '75%' + const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [ 'calibrationMarkings', 'fixedBase', @@ -213,15 +216,24 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { aa => isAddressableAreaStandardSlot(aa.id, deckDef) ) + let containerPadding = '0' + if (!isZoomed) { + if (tab === 'startingDeck') { + containerPadding = SPACING.spacing40 + } else { + containerPadding = SPACING.spacing60 + } + } + return ( - + <> - {zoomIn.slot == null ? ( + {zoomIn.slot == null && tab === 'startingDeck' ? ( {hoverSlot != null && breakPointSize !== 'small' && @@ -239,144 +251,153 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { ) : null} ) : null} - - {() => ( - <> - {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 + + {() => ( + <> + {robotType === OT2_ROBOT_TYPE ? ( + + ) : ( + <> + {filteredAddressableAreas.map(addressableArea => { + const cutoutId = getCutoutIdForAddressableArea( + addressableArea.id, + deckDef.cutoutFixtures ) - : 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 ( - - ) - } - })} - - )} - areas.location as CutoutId + ) : 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 ( + + ) + } + })} + )} - {...{ - deckDef, - showGen1MultichannelCollisionWarnings, - }} - /> - 0} - /> - - )} - - {zoomIn.slot == null ? ( + areas.location as CutoutId + )} + {...{ + deckDef, + showGen1MultichannelCollisionWarnings, + }} + /> + 0} + /> + + )} + + + {zoomIn.slot == null && tab === 'startingDeck' ? ( {hoverSlot != null && breakPointSize !== 'small' && @@ -404,6 +425,6 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { setHoveredLabware={setHoveredLabware} /> ) : null} - + ) } diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index d997835e831..06fb418d550 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -301,6 +301,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri || // if nested labware changes but labware doesn't, still delete both (createdLabwareForSlot.labwareDefURI === selectedLabwareDefUri && + selectedNestedLabwareDefUri != null && createdNestedLabwareForSlot?.labwareDefURI !== selectedNestedLabwareDefUri)) ) { @@ -354,6 +355,13 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { dispatch(createDeckFixture(selectedFixture, cutout)) } } + if ( + matchingLabwareFor4thColumn != null && + selectedFixture !== 'stagingArea' && + selectedFixture !== 'wasteChuteAndStagingArea' + ) { + dispatch(deleteContainer({ labwareId: matchingLabwareFor4thColumn.id })) + } if (selectedModuleModel != null) { // create module const moduleType = getModuleType(selectedModuleModel) @@ -411,10 +419,12 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { selectedModuleModel != null && selectedLabwareDefUri != null && (createdLabwareForSlot?.labwareDefURI !== selectedLabwareDefUri || - // if nested labware changes but labware doesn't, still create both both + // if nested labware changes but labware doesn't, still create both (createdLabwareForSlot.labwareDefURI === selectedLabwareDefUri && createdNestedLabwareForSlot?.labwareDefURI !== - selectedNestedLabwareDefUri)) + selectedNestedLabwareDefUri && + (createdNestedLabwareForSlot?.labwareDefURI != null || + selectedNestedLabwareDefUri != null))) ) { // create adapter + labware on module dispatch( diff --git a/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx b/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx index 3696cd7deaa..116d21b1956 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx @@ -4,22 +4,29 @@ import { COLORS, FixedTrash, FlexTrash, + Module, SingleSlotFixture, StagingAreaFixture, WasteChuteFixture, WasteChuteStagingAreaFixture, } from '@opentrons/components' -import { OT2_ROBOT_TYPE, getPositionFromSlotId } from '@opentrons/shared-data' +import { + OT2_ROBOT_TYPE, + getModuleDef2, + getPositionFromSlotId, +} from '@opentrons/shared-data' +import { getLabwareSlot } from '@opentrons/step-generation' import { getInitialDeckSetup } from '../../../step-forms/selectors' import { LabwareOnDeck as LabwareOnDeckComponent } from '../../../organisms' import { lightFill, darkFill } from './DeckSetupContainer' -import { getAdjacentLabware } from './utils' +import { getAdjacentSlots } from './utils' import type { TrashCutoutId, StagingAreaLocation, DeckLabelProps, } from '@opentrons/components' import type { + AddressableAreaName, CutoutId, DeckDefinition, RobotType, @@ -38,17 +45,52 @@ interface FixtureRenderProps { export const FixtureRender = (props: FixtureRenderProps): JSX.Element => { const { fixture, cutout, deckDef, robotType, showHighlight, tagInfo } = props const deckSetup = useSelector(getInitialDeckSetup) - const { labware } = deckSetup - const adjacentLabware = getAdjacentLabware(fixture, cutout, labware) + const { labware, modules } = deckSetup + const adjacentSlots = getAdjacentSlots(fixture, cutout) + + // magnetic block in column 3 if staging area is used + const adjacentModule = Object.values(modules).find(({ slot }) => + adjacentSlots?.includes(slot as AddressableAreaName) + ) + // labware in column 3 or 4, possibly on a magnetic block in column 3 + const adjacentLabwares = Object.values(labware).filter( + ({ slot }) => + adjacentSlots?.includes(slot as AddressableAreaName) || + slot === adjacentModule?.id + ) const renderLabwareOnDeck = (): JSX.Element | null => { - if (!adjacentLabware) return null - const slotPosition = getPositionFromSlotId(adjacentLabware.slot, deckDef) return ( - + {adjacentLabwares.map(adjacentLabware => { + const slot = getLabwareSlot(adjacentLabware.id, labware, modules) + const slotPosition = getPositionFromSlotId(slot, deckDef) + return ( + + ) + })} + + ) + } + const renderModuleOnDeck = (): JSX.Element | null => { + if (adjacentModule == null) { + return null + } + const slotPosition = getPositionFromSlotId(adjacentModule.slot, deckDef) + + return ( + ) } @@ -56,13 +98,18 @@ export const FixtureRender = (props: FixtureRenderProps): JSX.Element => { switch (fixture) { case 'stagingArea': { return ( - + 0 ? adjacentLabwares[0]?.id : 0 + }`} + > + {renderModuleOnDeck()} {renderLabwareOnDeck()} ) @@ -105,7 +152,11 @@ export const FixtureRender = (props: FixtureRenderProps): JSX.Element => { } case 'wasteChuteAndStagingArea': { return ( - + 0 ? adjacentLabwares[0]?.id : 0 + }`} + > { + if (fixture === 'stagingArea' || fixture === 'wasteChuteAndStagingArea') { + const stagingAreaAddressableAreaNames = getStagingAreaAddressableAreas( + [cutout], + false + ) + return stagingAreaAddressableAreaNames + } + return null +} + type BreakPoint = 'small' | 'medium' | 'large' export function useDeckSetupWindowBreakPoint(): BreakPoint { diff --git a/protocol-designer/src/pages/Designer/Offdeck/HighlightOffdeckSlot.tsx b/protocol-designer/src/pages/Designer/OffDeck/HighlightOffdeckSlot.tsx similarity index 100% rename from protocol-designer/src/pages/Designer/Offdeck/HighlightOffdeckSlot.tsx rename to protocol-designer/src/pages/Designer/OffDeck/HighlightOffdeckSlot.tsx diff --git a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx b/protocol-designer/src/pages/Designer/OffDeck/OffDeckDetails.tsx similarity index 84% rename from protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx rename to protocol-designer/src/pages/Designer/OffDeck/OffDeckDetails.tsx index 5f219e8ecd1..b062915271c 100644 --- a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx +++ b/protocol-designer/src/pages/Designer/OffDeck/OffDeckDetails.tsx @@ -1,9 +1,12 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' +import styled from 'styled-components' import { ALIGN_CENTER, + ALIGN_START, BORDERS, + Box, COLORS, DIRECTION_COLUMN, EmptySelectorButton, @@ -14,7 +17,6 @@ import { RobotWorkSpace, SPACING, StyledText, - WRAP, } from '@opentrons/components' import * as wellContentsSelectors from '../../../top-selectors/well-contents' import { selectors } from '../../../labware-ingred/selectors' @@ -32,7 +34,9 @@ import { HighlightOffdeckSlot } from './HighlightOffdeckSlot' import type { CoordinateTuple, DeckSlotId } from '@opentrons/shared-data' import type { DeckSetupTabType } from '../types' -const OFFDECK_MAP_WIDTH = '41.625rem' +const OFF_DECK_MAP_WIDTH = '41.625rem' +const OFF_DECK_MAP_HEIGHT = '45.5rem' +const OFF_DECK_MAP_HEIGHT_FOR_STEP = '31.4rem' const ZERO_SLOT_POSITION: CoordinateTuple = [0, 0, 0] interface OffDeckDetailsProps extends DeckSetupTabType { addLabware: () => void @@ -53,27 +57,29 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { const allWellContentsForActiveItem = useSelector( wellContentsSelectors.getAllWellContentsForActiveItem ) - const containerWidth = tab === 'startingDeck' ? '100vw' : '75vh' + const containerWidth = tab === 'startingDeck' ? '100vw' : '75vw' + const paddingLeftWithHover = hoverSlot == null - ? `calc((${containerWidth} - (${SPACING.spacing24} * 2) - ${OFFDECK_MAP_WIDTH}) / 2)` + ? `calc((${containerWidth} - (${SPACING.spacing24} * 2) - ${OFF_DECK_MAP_WIDTH}) / 2)` : SPACING.spacing24 const paddingLeft = tab === 'startingDeck' ? paddingLeftWithHover : undefined const padding = tab === 'protocolSteps' ? SPACING.spacing24 - : `${SPACING.spacing24} ${paddingLeft}` - const stepDetailsContainerWidth = `calc(((${containerWidth} - ${OFFDECK_MAP_WIDTH}) / 2) - (${SPACING.spacing24} * 3))` + : `${SPACING.spacing40} ${paddingLeft}` + const stepDetailsContainerWidth = `calc(((${containerWidth} - ${OFF_DECK_MAP_WIDTH}) / 2) - (${SPACING.spacing24} * 3))` return ( {hoverSlot != null ? ( @@ -85,27 +91,41 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { ) : null} {i18n.format(t('off_deck_labware'), 'upperCase')} - - + + {tab === 'startingDeck' ? ( + + + + ) : null} {offDeckLabware.map(lw => { const wellContents = allWellContentsForActiveItem ? allWellContentsForActiveItem[lw.id] @@ -123,13 +143,11 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { const highlighted = hoveredDropdownItem.id === lw.id return ( ) })} - - - {tab === 'startingDeck' ? ( - - - - ) : null} - + ) } + +const LabwareWrapper = styled(Box)` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(9.5625rem, 1fr)); + row-gap: ${SPACING.spacing40}; + column-gap: ${SPACING.spacing32}; + justify-content: ${JUSTIFY_CENTER}; /* Center the grid within the container */ + align-items: ${ALIGN_START}; + width: 100%; + // Note(kk: 1/30/2025) this padding is to add space to the right edge and the left edge of the grid + // this is not a perfect solution, but it works for now + padding: 0 ${SPACING.spacing24}; +` diff --git a/protocol-designer/src/pages/Designer/Offdeck/__tests__/Offdeck.test.tsx b/protocol-designer/src/pages/Designer/OffDeck/__tests__/OffDeck.test.tsx similarity index 97% rename from protocol-designer/src/pages/Designer/Offdeck/__tests__/Offdeck.test.tsx rename to protocol-designer/src/pages/Designer/OffDeck/__tests__/OffDeck.test.tsx index 54edbf8e9ac..11bea47d9d0 100644 --- a/protocol-designer/src/pages/Designer/Offdeck/__tests__/Offdeck.test.tsx +++ b/protocol-designer/src/pages/Designer/OffDeck/__tests__/OffDeck.test.tsx @@ -5,7 +5,7 @@ import { selectors } from '../../../../labware-ingred/selectors' import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' import { renderWithProviders } from '../../../../__testing-utils__' import { OffDeckDetails } from '../OffDeckDetails' -import { OffDeck } from '../Offdeck' +import { OffDeck } from '..' import type * as Components from '@opentrons/components' vi.mock('../OffDeckDetails') diff --git a/protocol-designer/src/pages/Designer/Offdeck/__tests__/OffDeckDetails.test.tsx b/protocol-designer/src/pages/Designer/OffDeck/__tests__/OffDeckDetails.test.tsx similarity index 100% rename from protocol-designer/src/pages/Designer/Offdeck/__tests__/OffDeckDetails.test.tsx rename to protocol-designer/src/pages/Designer/OffDeck/__tests__/OffDeckDetails.test.tsx diff --git a/protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx b/protocol-designer/src/pages/Designer/OffDeck/index.tsx similarity index 100% rename from protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx rename to protocol-designer/src/pages/Designer/OffDeck/index.tsx diff --git a/protocol-designer/src/pages/Designer/Offdeck/index.ts b/protocol-designer/src/pages/Designer/Offdeck/index.ts deleted file mode 100644 index 0ca8caf1f01..00000000000 --- a/protocol-designer/src/pages/Designer/Offdeck/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Offdeck' diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/DraggableSidebar.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/DraggableSidebar.tsx index 210abdec6ba..f1f66879c41 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/DraggableSidebar.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/DraggableSidebar.tsx @@ -10,7 +10,7 @@ import { } from '@opentrons/components' import { TimelineToolbox } from './Timeline/TimelineToolbox' -const INITIAL_SIDEBAR_WIDTH = 276 +const INITIAL_SIDEBAR_WIDTH = 235 const MIN_SIDEBAR_WIDTH = 80 const MAX_SIDEBAR_WIDTH = 350 diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx index 45eee9219f8..70c7cca9f94 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx @@ -248,7 +248,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { numErrors, stepTypeDisplayName: i18n.format( t(`stepType.${formData.stepType}`), - 'capitalize' + 'titleCase' ), t, }) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/Initialization.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/Initialization.tsx index 1b388e8f4ea..c394627a677 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/Initialization.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/Initialization.tsx @@ -10,6 +10,7 @@ import { Divider, DropdownMenu, EmptySelectorButton, + FLEX_MAX_CONTENT, Flex, Icon, InputField, @@ -23,6 +24,7 @@ import { Tooltip, useHoverTooltip, } from '@opentrons/components' +import { LINK_BUTTON_STYLE } from '../../../../../../atoms' import { ABSORBANCE_READER_MAX_WAVELENGTH_NM, ABSORBANCE_READER_MIN_WAVELENGTH_NM, @@ -237,12 +239,15 @@ function IntializationEditor(props: InitializationEditorProps): JSX.Element { {wavelengthItems} {mode === 'multi' && wavelengths.length < MAX_WAVELENGTHS ? ( - + + + ) : null} {mode === 'single' ? ( @@ -279,6 +284,7 @@ function WavelengthItem(props: WavelengthItemProps): JSX.Element { handleDeleteWavelength, index, error, + mode, } = props const { t } = useTranslation('form') const showCustom = !DEFINED_OPTIONS.some(({ value }) => value === wavelength) @@ -329,18 +335,17 @@ function WavelengthItem(props: WavelengthItemProps): JSX.Element { error={!isFocused ? error : null} /> ) : null} - {wavelengths.length > 1 ? ( + {wavelengths.length > 1 && mode === 'multi' ? ( { handleDeleteWavelength(index) }} - alignSelf={ALIGN_FLEX_END} padding={SPACING.spacing4} + css={LINK_BUTTON_STYLE} + alignSelf={ALIGN_FLEX_END} + textDecoration={TEXT_DECORATION_UNDERLINE} > - + {t('step_edit_form.absorbanceReader.delete')} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx index 1549f9958a7..8fe0b9e8da8 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx @@ -54,14 +54,18 @@ export function MagnetTools(props: StepFormProps): JSX.Element { const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) return ( - + - + - { - if (canAddStepOrProfile) { - setShowCreateNewCycle(true) - } - }} - textAlignment="left" - iconName="plus" - disabled={!canAddStepOrProfile} - /> - { - if (canAddStepOrProfile) { - setShowCreateNewStep(true) - } - }} - textAlignment="left" - iconName="plus" - disabled={!canAddStepOrProfile} - /> + + { + if (canAddStepOrProfile) { + setShowCreateNewCycle(true) + } + }} + textAlignment="left" + iconName="plus" + disabled={!canAddStepOrProfile} + /> + + + { + if (canAddStepOrProfile) { + setShowCreateNewStep(true) + } + }} + textAlignment="left" + iconName="plus" + disabled={!canAddStepOrProfile} + /> + {steps.length > 0 || showCreateNewStep || showCreateNewCycle ? ( + , - semiBoldText: , + text: ( + + ), + semiBoldText: ( + + ), tag: , }} values={values} @@ -57,7 +64,6 @@ function StyledTrans(props: StyledTransProps): JSX.Element { ) } - const getWellsForStepSummary = ( targetWells: string[], labwareWells: string[] @@ -164,63 +170,55 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { } = currentStep stepSummaryContent = thermocyclerFormType === 'thermocyclerState' ? ( - + {blockIsActive ? ( ) : null} - - {lidIsActive ? ( - - ) : null} + {lidIsActive ? ( - - + ) : null} + + ) : ( - - - - - - - - - - + + + + + + ) break } @@ -269,18 +267,19 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { setTemperature, targetTemperature, } = currentStep - const isDeactivating = setTemperature === 'false' + const isSettingTemperature = + setTemperature != null && JSON.parse(String(setTemperature ?? false)) const tempModuleDisplayName = getModuleDisplayName(modules[tempModuleId]?.model) ?? unknownModule - stepSummaryContent = isDeactivating ? ( + stepSummaryContent = isSettingTemperature ? ( ) : ( ) @@ -383,43 +382,39 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { getModuleDisplayName(modules[heaterShakerModuleId]?.model) ?? unknownModule stepSummaryContent = ( - - + + + {targetSpeed ? ( - {targetSpeed ? ( - - ) : null} - - - {heaterShakerTimer ? ( - - ) : null} + ) : null} + {heaterShakerTimer ? ( - - + ) : null} + + ) break } @@ -433,6 +428,7 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4} width="100%" + height="100%" > {stepSummaryContent != null ? ( @@ -454,3 +450,9 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { ) : null } + +const StepSummaryContainer = styled(Flex)` + flex-wrap: ${WRAP}; + gap: ${SPACING.spacing20}; + row-gap: ${SPACING.spacing4}; +` diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/AddStepOverflowButton.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/AddStepOverflowButton.tsx index 44b131658f7..1f539ba689e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/AddStepOverflowButton.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/AddStepOverflowButton.tsx @@ -45,7 +45,7 @@ export function AddStepOverflowButton( {i18n.format( t(`application:stepType.${stepType}`, stepType), - 'capitalize' + 'titleCase' )} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx index 0051499bf15..e044e1e3338 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx @@ -62,7 +62,7 @@ export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element { setOpenedOverflowMenuId, sidebarWidth, } = props - const { t } = useTranslation('application') + const { i18n, t } = useTranslation('application') const dispatch = useDispatch>() const stepIds = useSelector(getOrderedStepIds) const step = useSelector(stepFormSelectors.getSavedStepForms)[stepId] @@ -227,7 +227,8 @@ export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element { onMouseEnter={highlightStep} iconName={hasError || hasWarnings ? 'alert-circle' : iconName} title={`${stepNumber}. ${ - step.stepName || t(`stepType.${step.stepType}`) + i18n.format(step.stepName, 'titleCase') || + t(`stepType.${step.stepType}`) }`} dragHovered={dragHovered} sidebarWidth={sidebarWidth} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx index d0534b234cf..d01a0b60b48 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx @@ -294,6 +294,7 @@ export function StepContainer(props: StepContainerProps): JSX.Element { confirmDelete={confirmDelete} confirmMultiDelete={confirmMultiDelete} multiSelectItemIds={multiSelectItemIds} + sidebarWidth={sidebarWidth} />, getMainPagePortalEl() ) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx index 465e85b577d..4d62c383cca 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx @@ -41,8 +41,11 @@ interface StepOverflowMenuProps { confirmDelete: () => void confirmMultiDelete: () => void multiSelectItemIds: string[] | null + sidebarWidth: number // adjust the position of the overflow menu } +const POSITION_ADJUSTMENT = 4 + export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { const { stepId, @@ -53,6 +56,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { confirmDelete, confirmMultiDelete, multiSelectItemIds, + sidebarWidth, } = props const { t } = useTranslation('protocol_steps') const singleEditFormHasUnsavedChanges = useSelector( @@ -101,7 +105,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { ref={menuRootRef} zIndex={12} top={top} - left="18.75rem" + left={sidebarWidth - POSITION_ADJUSTMENT} // the space between kebab menu button and overflow menu is 8px position={POSITION_ABSOLUTE} whiteSpace={NO_WRAP} borderRadius={BORDERS.borderRadius8} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/AddStepButton.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/AddStepButton.test.tsx index a2fcea0a7e2..69911628b7e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/AddStepButton.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/AddStepButton.test.tsx @@ -87,7 +87,7 @@ describe('AddStepButton', () => { screen.getByText('Mix') screen.getByText('Pause') screen.getByText('Thermocycler') - screen.getByText('Heater-shaker') + screen.getByText('Heater-Shaker') screen.getByText('Temperature') screen.getByText('Magnet') }) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/StepOverflowMenu.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/StepOverflowMenu.test.tsx index 55197e85ed4..93c115e9255 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/StepOverflowMenu.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/StepOverflowMenu.test.tsx @@ -67,6 +67,7 @@ describe('StepOverflowMenu', () => { handleEdit: vi.fn(), confirmDelete: mockConfirm, confirmMultiDelete: vi.fn(), + sidebarWidth: 235, } vi.mocked(getMultiSelectItemIds).mockReturnValue(null) vi.mocked(getCurrentFormIsPresaved).mockReturnValue(false) 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 31c1c93eafc..32e1f090baa 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx @@ -18,14 +18,14 @@ import { } from '../../../../file-data/selectors' import { getEnableHotKeysDisplay } from '../../../../feature-flags/selectors' import { DeckSetupContainer } from '../../DeckSetup' -import { OffDeck } from '../../Offdeck' +import { OffDeck } from '../../OffDeck' import { SubStepsToolbox } from '../Timeline' import { DraggableSidebar } from '../DraggableSidebar' import { ProtocolSteps } from '..' import type { SavedStepFormState } from '../../../../step-forms' -vi.mock('../../Offdeck') +vi.mock('../../OffDeck') vi.mock('../../../../step-forms/selectors') vi.mock('../../../../ui/steps/selectors') vi.mock('../../../../ui/labware/selectors') @@ -115,6 +115,6 @@ describe('ProtocolSteps', () => { it('renders the current step name', () => { render() - screen.getByText('Custom pause') + screen.getByText('Custom Pause') }) }) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx index e426affd60d..0627ca24382 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx @@ -31,7 +31,7 @@ import { getHoveredTerminalItemId, } from '../../../ui/steps/selectors' import { DeckSetupContainer } from '../DeckSetup' -import { OffDeck } from '../Offdeck' +import { OffDeck } from '../OffDeck' import { SubStepsToolbox } from './Timeline' import { StepForm } from './StepForm' import { StepSummary } from './StepSummary' @@ -43,7 +43,7 @@ import { import { TimelineAlerts } from '../../../organisms' import { DraggableSidebar } from './DraggableSidebar' -const CONTENT_MAX_WIDTH = '44.6704375rem' +const CONTENT_MAX_WIDTH = '46.9375rem' export function ProtocolSteps(): JSX.Element { const { i18n, t } = useTranslation('starting_deck_state') @@ -52,14 +52,15 @@ export function ProtocolSteps(): JSX.Element { const hoveredTerminalItem = useSelector(getHoveredTerminalItemId) const isMultiSelectMode = useSelector(getIsMultiSelectMode) const selectedSubstep = useSelector(getSelectedSubstep) - const enableHoyKeyDisplay = useSelector(getEnableHotKeysDisplay) + const enableHotKeyDisplay = useSelector(getEnableHotKeysDisplay) const tab = useSelector(getDesignerTab) const leftString = t('onDeck') const rightString = t('offDeck') const [deckView, setDeckView] = useState< typeof leftString | typeof rightString >(leftString) - const [targetWidth, setTargetWidth] = useState(350) + // Note (02/03/25:kk) use DrraggableSidebar's initial width + const [targetWidth, setTargetWidth] = useState(235) const currentHoveredStepId = useSelector(getHoveredStepId) const currentSelectedStepId = useSelector(getSelectedStepId) @@ -82,12 +83,11 @@ export function ProtocolSteps(): JSX.Element { - + {showTimelineAlerts ? ( {currentStep != null && hoveredTerminalItem == null ? ( - {i18n.format(currentStep.stepName, 'capitalize')} + {i18n.format(currentStep.stepName, 'titleCase')} ) : null} {(hoveredTerminalItem != null || selectedTerminalItem != null) && @@ -145,15 +148,20 @@ export function ProtocolSteps(): JSX.Element { ) : ( )} - {formData == null ? ( + {/* avoid shifting the deck view container */} + - ) : null} + - {enableHoyKeyDisplay ? ( + {enableHotKeyDisplay ? ( ) : null} - + + + + {isMultiSelectMode ? : null} ) diff --git a/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx b/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx index d7a05a8efe0..9480ebc2d45 100644 --- a/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx +++ b/protocol-designer/src/pages/Designer/__tests__/Designer.test.tsx @@ -16,6 +16,7 @@ import type { NavigateFunction } from 'react-router-dom' const mockNavigate = vi.fn() +vi.mock('../OffDeck') vi.mock('../ProtocolSteps') vi.mock('../../../labware-ingred/actions') vi.mock('../../../labware-ingred/selectors') diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx index 5e560fcf6e4..792d3f25f4f 100644 --- a/protocol-designer/src/pages/Designer/index.tsx +++ b/protocol-designer/src/pages/Designer/index.tsx @@ -3,12 +3,14 @@ import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { - ALIGN_END, + ALIGN_CENTER, + ALIGN_STRETCH, COLORS, DIRECTION_COLUMN, - FLEX_MAX_CONTENT, Flex, INFO_TOAST, + JUSTIFY_CENTER, + JUSTIFY_FLEX_END, SPACING, ToggleGroup, useOnClickOutside, @@ -25,7 +27,7 @@ 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' +import { OffDeck } from './OffDeck' import { LiquidsOverflowMenu } from './LiquidsOverflowMenu' import { ProtocolSteps } from './ProtocolSteps' @@ -126,7 +128,14 @@ export function Designer(): JSX.Element { const deckViewItems = deckView === leftString ? ( - + + + ) : ( ) @@ -159,7 +168,7 @@ export function Designer(): JSX.Element { }} /> ) : null} - + {zoomIn.slot == null ? ( - + = createSelector(getBatchEditFieldChanges, changes => !isEmpty(changes)) -const _formLevelErrors = (hydratedForm: HydratedFormData): StepFormErrors => { - return getFormErrors(hydratedForm.stepType, hydratedForm) +const _formLevelErrors = ( + hydratedForm: HydratedFormData, + moduleEntities: ModuleEntities +): StepFormErrors => { + return getFormErrors(hydratedForm.stepType, hydratedForm, moduleEntities) } const _dynamicFieldFormErrors = ( @@ -651,7 +654,10 @@ export const _hasFormLevelErrors = ( hydratedForm: HydratedFormData, invariantContext: InvariantContext ): boolean => { - if (_formLevelErrors(hydratedForm).length > 0) return true + if ( + _formLevelErrors(hydratedForm, invariantContext.moduleEntities).length > 0 + ) + return true if ( hydratedForm.stepType === 'thermocycler' && @@ -743,13 +749,20 @@ export const getDynamicFieldFormErrorsForUnsavedForm: Selector< export const getFormLevelErrorsForUnsavedForm: Selector< BaseState, StepFormErrors -> = createSelector(getHydratedUnsavedForm, hydratedForm => { - if (!hydratedForm) return [] +> = createSelector( + getHydratedUnsavedForm, + getInvariantContext, + (hydratedForm, invariantContext) => { + if (!hydratedForm) return [] - const errors = _formLevelErrors(hydratedForm) + const errors = _formLevelErrors( + hydratedForm, + invariantContext.moduleEntities + ) - return errors -}) + return errors + } +) export const getCurrentFormCanBeSaved: Selector< BaseState, boolean diff --git a/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts b/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts index 89474e7cb2f..bea9aca8e53 100644 --- a/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts +++ b/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts @@ -305,10 +305,10 @@ const _patchAbsorbanceReaderModuleId = (args: { )?.length ?? 1 const hasAbsorbanceReaderModuleId = stepType === 'absorbanceReader' - const { modules } = initialDeckSetup const robotState: RobotState | null = last(robotStateTimeline.timeline)?.robotState ?? null + const modules = robotState?.modules ?? {} const labware = robotState?.labware ?? {} // pre-select form type if module is set diff --git a/protocol-designer/src/steplist/formLevel/errors.ts b/protocol-designer/src/steplist/formLevel/errors.ts index eb851637d32..efead9117ef 100644 --- a/protocol-designer/src/steplist/formLevel/errors.ts +++ b/protocol-designer/src/steplist/formLevel/errors.ts @@ -21,8 +21,9 @@ import { getTimeFromForm } from '../utils/getTimeFromForm' import type { ReactNode } from 'react' import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data' -import type { LabwareEntities, PipetteEntity } from '@opentrons/step-generation' +import type { PipetteEntity } from '@opentrons/step-generation' import type { StepFieldName } from '../../form-types' +import type { ModuleEntities } from '../../step-forms' /******************* ** Error Messages ** ********************/ @@ -113,10 +114,14 @@ const ENGAGE_HEIGHT_REQUIRED: FormError = { const ENGAGE_HEIGHT_MIN_EXCEEDED: FormError = { title: 'Specified distance is below module minimum', dependentFields: ['magnetAction', 'engageHeight'], + showAtForm: false, + showAtField: true, } const ENGAGE_HEIGHT_MAX_EXCEEDED: FormError = { title: 'Specified distance is above module maximum', dependentFields: ['magnetAction', 'engageHeight'], + showAtForm: false, + showAtField: true, } const MODULE_ID_REQUIRED: FormError = { title: @@ -407,7 +412,7 @@ export interface HydratedFormData { export type FormErrorChecker = ( arg: HydratedFormData, - labwareEntities?: LabwareEntities + moduleEntities?: ModuleEntities ) => FormError | null // TODO: test these @@ -554,7 +559,7 @@ export const targetTemperatureRequired = ( fields: HydratedFormData ): FormError | null => { const { setTemperature, targetTemperature } = fields - return setTemperature && !targetTemperature + return JSON.parse(String(setTemperature ?? false)) && !targetTemperature ? TARGET_TEMPERATURE_REQUIRED : null } @@ -672,27 +677,32 @@ export const newLabwareLocationRequired = ( : null } export const engageHeightRangeExceeded = ( - fields: HydratedFormData + fields: HydratedFormData, + moduleEntities?: ModuleEntities ): FormError | null => { - const { magnetAction, engageHeight } = fields - const moduleEntity = fields.meta?.module - const model = moduleEntity?.model - + const { magnetAction, engageHeight, moduleId } = fields + if (moduleEntities == null) { + return null + } + const moduleModel = moduleEntities[moduleId].model + const engageHeightCast = Number(engageHeight) if (magnetAction === 'engage') { - if (model === MAGNETIC_MODULE_V1) { - if (engageHeight < MIN_ENGAGE_HEIGHT_V1) { + if (moduleModel === MAGNETIC_MODULE_V1) { + if (engageHeightCast < MIN_ENGAGE_HEIGHT_V1) { return ENGAGE_HEIGHT_MIN_EXCEEDED - } else if (engageHeight > MAX_ENGAGE_HEIGHT_V1) { + } else if (engageHeightCast > MAX_ENGAGE_HEIGHT_V1) { return ENGAGE_HEIGHT_MAX_EXCEEDED } - } else if (model === MAGNETIC_MODULE_V2) { - if (engageHeight < MIN_ENGAGE_HEIGHT_V2) { + } else if (moduleModel === MAGNETIC_MODULE_V2) { + if (engageHeightCast < MIN_ENGAGE_HEIGHT_V2) { return ENGAGE_HEIGHT_MIN_EXCEEDED - } else if (engageHeight > MAX_ENGAGE_HEIGHT_V2) { + } else if (engageHeightCast > MAX_ENGAGE_HEIGHT_V2) { return ENGAGE_HEIGHT_MAX_EXCEEDED } } else { - console.warn(`unhandled model for engageHeightRangeExceeded: ${model}`) + console.warn( + `unhandled model for engageHeightRangeExceeded: ${moduleModel}` + ) } } @@ -917,14 +927,13 @@ export const fileNameRequired = ( ********************/ type ComposeErrors = ( ...errorCheckers: FormErrorChecker[] -) => (arg: HydratedFormData) => FormError[] +) => (arg: HydratedFormData, moduleEntities?: ModuleEntities) => FormError[] export const composeErrors: ComposeErrors = ( ...errorCheckers: FormErrorChecker[] -) => value => - errorCheckers.reduce((acc, errorChecker) => { - const possibleError = errorChecker(value) - return possibleError ? [...acc, possibleError] : acc - }, []) +) => (formData: HydratedFormData, moduleEntities?: ModuleEntities) => + errorCheckers + .map(checker => checker(formData, moduleEntities)) + .filter((error): error is FormError => error !== null) export const getIsOutOfRange = ( value: any, diff --git a/protocol-designer/src/steplist/formLevel/index.ts b/protocol-designer/src/steplist/formLevel/index.ts index a0b74ec3c16..7242eccb1dd 100644 --- a/protocol-designer/src/steplist/formLevel/index.ts +++ b/protocol-designer/src/steplist/formLevel/index.ts @@ -64,6 +64,7 @@ import { import type { FormWarning, FormWarningType } from './warnings' import type { HydratedFormData, StepType } from '../../form-types' import type { FormError } from './errors' +import type { ModuleEntities } from '@opentrons/step-generation' export { handleFormChange } from './handleFormChange' export { createBlankForm } from './createBlankForm' export { getDefaultsForStepType } from './getDefaultsForStepType' @@ -78,7 +79,10 @@ export { getNextDefaultEngageHeight } from './getNextDefaultEngageHeight' export { stepFormToArgs } from './stepFormToArgs' export type { FormError, FormWarning, FormWarningType } interface FormHelpers { - getErrors?: (arg: HydratedFormData) => FormError[] + getErrors?: ( + arg: HydratedFormData, + moduleEntities: ModuleEntities + ) => FormError[] getWarnings?: (arg: unknown) => FormWarning[] } const stepFormHelperMap: Partial> = { @@ -180,12 +184,11 @@ const stepFormHelperMap: Partial> = { } export const getFormErrors = ( stepType: StepType, - formData: HydratedFormData + formData: HydratedFormData, + moduleEntities: ModuleEntities ): FormError[] => { - const formErrorGetter = - stepFormHelperMap[stepType] && stepFormHelperMap[stepType]?.getErrors - const errors = formErrorGetter != null ? formErrorGetter(formData) : [] - return errors + const formErrorGetter = stepFormHelperMap[stepType]?.getErrors + return formErrorGetter ? formErrorGetter(formData, moduleEntities) : [] } export const getFormWarnings = ( stepType: StepType, diff --git a/protocol-designer/src/steplist/substepTimeline.ts b/protocol-designer/src/steplist/substepTimeline.ts index 0788acbde6f..638ec97d186 100644 --- a/protocol-designer/src/steplist/substepTimeline.ts +++ b/protocol-designer/src/steplist/substepTimeline.ts @@ -66,9 +66,6 @@ const _createNextTimelineFrame = (args: { volume: args.volume, activeTips: _getNewActiveTips(args.nextFrame.commands.slice(0, args.index)), } - const command = args.command - const isAirGapCommand = - 'meta' in command && command.meta != null && 'isAirGap' in command.meta const newTimelineFrame = args.command.commandType === 'aspirate' || @@ -76,12 +73,10 @@ const _createNextTimelineFrame = (args: { ? { ..._newTimelineFrameKeys, source: args.wellInfo, - isAirGap: isAirGapCommand, } : { ..._newTimelineFrameKeys, dest: args.wellInfo, - isAirGap: isAirGapCommand, } return newTimelineFrame } diff --git a/protocol-designer/src/steplist/test/generateSubsteps.test.ts b/protocol-designer/src/steplist/test/generateSubsteps.test.ts index 8994f921232..04beaea2967 100644 --- a/protocol-designer/src/steplist/test/generateSubsteps.test.ts +++ b/protocol-designer/src/steplist/test/generateSubsteps.test.ts @@ -201,7 +201,6 @@ describe('generateSubstepItem', () => { preIngreds: {}, well: 'C1', }, - isAirGap: false, }, ], }, @@ -240,7 +239,6 @@ describe('generateSubstepItem', () => { preIngreds: {}, well: 'A1', }, - isAirGap: false, source: { postIngreds: {}, preIngreds: {}, @@ -295,7 +293,6 @@ describe('generateSubstepItem', () => { labwareId: tiprackId, wellName: 'A1', }, - isAirGap: false, source: { well: 'A1', preIngreds: {}, postIngreds: {} }, dest: { well: 'A1', @@ -316,7 +313,6 @@ describe('generateSubstepItem', () => { labwareId: tiprackId, wellName: 'A1', }, - isAirGap: false, dest: { postIngreds: { __air__: { @@ -407,7 +403,6 @@ describe('generateSubstepItem', () => { preIngreds: {}, well: 'A1', }, - isAirGap: false, source: { postIngreds: {}, preIngreds: {}, @@ -434,7 +429,6 @@ describe('generateSubstepItem', () => { }, well: 'A1', }, - isAirGap: false, source: { postIngreds: { __air__: { @@ -465,7 +459,6 @@ describe('generateSubstepItem', () => { preIngreds: {}, well: 'A2', }, - isAirGap: false, source: { postIngreds: {}, preIngreds: {}, @@ -492,7 +485,6 @@ describe('generateSubstepItem', () => { }, well: 'A2', }, - isAirGap: false, source: { postIngreds: { __air__: { diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index b1104dba6f5..a3845bb8e8f 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -128,6 +128,13 @@ export const getUnoccupiedLabwareLocationOptions: Selector< if (robotState == null) return null + const trashCutouts = Object.values(additionalEquipmentEntities).reduce< + string[] + >( + (acc, { name, location }) => + name === 'trashBin' && location != null ? [...acc, location] : acc, + [] + ) const { modules, labware } = robotState const slotIdsOccupiedByModules = Object.entries(modules).reduce( (acc, [modId, modOnDeck]) => { @@ -233,6 +240,7 @@ export const getUnoccupiedLabwareLocationOptions: Selector< .map(lw => lw.slot) .includes(slotId) && !isTrashSlot && + !trashCutouts.some(cutout => cutout.includes(slotId)) && !WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slotId) && !notSelectedStagingAreaAddressableAreas.includes(slotId) && !FLEX_MODULE_ADDRESSABLE_AREAS.includes(slotId) && diff --git a/protocol-designer/src/ui/modules/selectors.ts b/protocol-designer/src/ui/modules/selectors.ts index c0b9da78da7..8d5edb5a59b 100644 --- a/protocol-designer/src/ui/modules/selectors.ts +++ b/protocol-designer/src/ui/modules/selectors.ts @@ -8,6 +8,7 @@ import { TEMPERATURE_MODULE_TYPE, } from '@opentrons/shared-data' import { getInitialDeckSetup } from '../../step-forms/selectors' +import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations' import { getLabwareNicknamesById } from '../labware/selectors' import { getModuleLabwareOptions, @@ -86,11 +87,11 @@ export const getHeaterShakerLabwareOptions: Selector< export const getAbsorbanceReaderLabwareOptions: Selector< DropdownOption[] > = createSelector( - getInitialDeckSetup, + getDeckSetupForActiveItem, getLabwareNicknamesById, - (initialDeckSetup, nicknamesById) => { + (deckSetup, nicknamesById) => { const absorbanceReaderModuleOptions = getModuleLabwareOptions( - initialDeckSetup, + deckSetup, nicknamesById, ABSORBANCE_READER_TYPE ) diff --git a/protocol-designer/src/utils/index.ts b/protocol-designer/src/utils/index.ts index 8f4c9397066..60c94d4c346 100644 --- a/protocol-designer/src/utils/index.ts +++ b/protocol-designer/src/utils/index.ts @@ -147,19 +147,24 @@ export const getHas96Channel = (pipettes: PipetteEntities): boolean => { } export const getStagingAreaAddressableAreas = ( - cutoutIds: CutoutId[] + cutoutIds: CutoutId[], + filterStandardSlots: boolean = true ): AddressableAreaName[] => { const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const cutoutFixtures = deckDef.cutoutFixtures - return cutoutIds - .flatMap(cutoutId => { - const addressableAreasOnCutout = cutoutFixtures.find( - cutoutFixture => cutoutFixture.id === STAGING_AREA_RIGHT_SLOT_FIXTURE - )?.providesAddressableAreas[cutoutId] - return addressableAreasOnCutout ?? [] - }) - .filter(aa => !isAddressableAreaStandardSlot(aa, deckDef)) + const addressableAreasRaw = cutoutIds.flatMap(cutoutId => { + const addressableAreasOnCutout = cutoutFixtures.find( + cutoutFixture => cutoutFixture.id === STAGING_AREA_RIGHT_SLOT_FIXTURE + )?.providesAddressableAreas[cutoutId] + return addressableAreasOnCutout ?? [] + }) + if (filterStandardSlots) { + return addressableAreasRaw.filter( + aa => !isAddressableAreaStandardSlot(aa, deckDef) + ) + } + return addressableAreasRaw } export const getCutoutIdByAddressableArea = ( diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 9ed463aecb7..0fd0c012b5f 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -6,6 +6,7 @@ import { WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { + AIR_GAP_META, ASPIRATE_OFFSET_FROM_BOTTOM_MM, DEFAULT_PIPETTE, delayCommand, @@ -1319,7 +1320,7 @@ describe('advanced options', () => { // dispense the aspirate > air gap { commandType: 'dispense', - + meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -1629,6 +1630,7 @@ describe('advanced options', () => { // dispense aspirate > air gap then liquid { commandType: 'dispense', + meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -2036,6 +2038,7 @@ describe('advanced options', () => { // dispense the aspirate > air gap { commandType: 'dispense', + meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -2342,6 +2345,7 @@ describe('advanced options', () => { }, { commandType: 'dispense', + meta: AIR_GAP_META, key: expect.any(String), params: { flowRate: 2.2, @@ -2777,6 +2781,7 @@ describe('advanced options', () => { { commandType: 'dispense', key: expect.any(String), + meta: AIR_GAP_META, params: { pipetteId: 'p300SingleId', volume: 31, @@ -3083,6 +3088,7 @@ describe('advanced options', () => { // dispense "aspirate > air gap" then dispense liquid { commandType: 'dispense', + meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3515,6 +3521,7 @@ describe('advanced options', () => { // dispense { commandType: 'dispense', + meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', @@ -3879,6 +3886,7 @@ describe('advanced options', () => { // dispense "aspirate > air gap" then dispense liquid { commandType: 'dispense', + meta: AIR_GAP_META, key: expect.any(String), params: { pipetteId: 'p300SingleId', diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index 6d64cf00e7d..c2a596d68ac 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -26,7 +26,6 @@ import type { Point } from '../../utils' export interface ExtendedAspirateParams extends AspDispAirgapParams { tipRack: string nozzles: NozzleConfigurationStyle | null - isAirGap?: boolean } /** Aspirate with given args. Requires tip. */ export const aspirate: CommandCreator = ( diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index 2ca1c737d08..3948158fa6b 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -26,6 +26,7 @@ import type { CommandCreator, CommandCreatorError } from '../../types' export interface DispenseAtomicCommandParams extends DispenseParams { nozzles: NozzleConfigurationStyle | null tipRack: string + isAirGap?: boolean } /** Dispense with given args. Requires tip. */ export const dispense: CommandCreator = ( @@ -42,6 +43,7 @@ export const dispense: CommandCreator = ( wellLocation, nozzles, tipRack, + isAirGap, } = args const actionName = 'dispense' const labwareState = prevRobotState.labware @@ -224,6 +226,7 @@ export const dispense: CommandCreator = ( // pushOut will always be undefined in step-generation for now // since there is no easy way to allow users to for it in PD }, + ...(isAirGap && { meta: { isAirGap } }), }, ] return { diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index 8269250b79d..372992f4694 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -257,6 +257,7 @@ export const distribute: CommandCreator = ( }, nozzles, tipRack: args.tipRack, + isAirGap: true, }), ...(dispenseDelay != null ? [ diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index 282b92ad3e1..bb92645a3f4 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -436,6 +436,7 @@ export const transfer: CommandCreator = ( }, tipRack: args.tipRack, nozzles: args.nozzles, + isAirGap: true, }), ...(dispenseDelay != null ? [ diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index 056ad5114cd..6429edf1ab1 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -5,6 +5,7 @@ import { SOURCE_LABWARE, DEFAULT_BLOWOUT_WELL, DEST_LABWARE, + AIR_GAP_META, } from './data' import { ONE_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA } from '@opentrons/shared-data' @@ -253,6 +254,7 @@ export const makeDispenseAirGapHelper: MakeDispenseAirGapHelper