diff --git a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx
index 99fde7dd81f..85f76f901b2 100644
--- a/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx
+++ b/components/src/atoms/ListButton/ListButtonChildren/ListButtonAccordionContainer.tsx
@@ -16,7 +16,7 @@ export function ListButtonAccordionContainer(
const { id, children } = props
return (
-
+
{children}
)
diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json
index 31be366c9d7..fcf88c2866e 100644
--- a/protocol-designer/src/assets/localization/en/starting_deck_state.json
+++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json
@@ -1,6 +1,6 @@
{
"adapter_compatible_lab": "Adapter compatible labware",
- "adapter": "Adapter",
+ "adapter": "Adapters",
"add_fixture": "Add a fixture",
"add_hardware_labware": "Add hardware/labware",
"add_hw_lw": "Add hardware/labware",
@@ -9,7 +9,7 @@
"add_module": "Add a module",
"add_rest": "Add labware and liquids to complete deck setup",
"alter_pause": "You may also need to alter the time you pause while your magnet is engaged.",
- "aluminumBlock": "Aluminum block",
+ "aluminumBlock": "Aluminum blocks",
"clear_labware": "Clear labware",
"clear_slot": "Clear slot",
"clear": "Clear",
@@ -47,16 +47,16 @@
"protocol_starting_deck": "Protocol starting deck",
"read_more_gen1_gen2": "Read more about the differences between GEN1 and GEN2 Magnetic Modules",
"rename_lab": "Rename labware",
- "reservoir": "Reservoir",
+ "reservoir": "Reservoirs",
"shift_click_to_select_all": "Shift + Click to select all",
"starting_deck_state": "Starting deck state",
"tc_slots_occupied_flex": "The Thermocycler needs slots A1 and B1. Slot A1 is occupied",
"tc_slots_occupied_ot2": "The Thermocycler needs slots 7, 8, 10, and 11. One or more of those slots is occupied",
- "tipRack": "Tip rack",
+ "tipRack": "Tip racks",
"trash_required": "A trash bin or waste chute is required",
- "tubeRack": "Tube rack",
+ "tubeRack": "Tube racks",
"untitled_protocol": "Untitled protocol",
"upload_custom_labware": "Upload custom labware",
"we_added_hardware": "We've added your deck hardware!",
- "wellPlate": "Well plate"
+ "wellPlate": "Well plates"
}
diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx
index 4e2db1df4b8..62a5f92d46e 100644
--- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx
+++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx
@@ -52,11 +52,11 @@ import { getDismissedHints } from '../../../tutorial/selectors'
import { createContainerAboveModule } from '../../../step-forms/actions/thunks'
import { ConfirmDeleteStagingAreaModal } from '../../../organisms'
import { BUTTON_LINK_STYLE } from '../../../atoms'
-import { FIXTURES, MOAM_MODELS } from './constants'
import { getSlotInformation } from '../utils'
-import { getModuleModelsBySlot, getDeckErrors } from './utils'
-import { MagnetModuleChangeContent } from './MagnetModuleChangeContent'
+import { ALL_ORDERED_CATEGORIES, FIXTURES, MOAM_MODELS } from './constants'
import { LabwareTools } from './LabwareTools'
+import { MagnetModuleChangeContent } from './MagnetModuleChangeContent'
+import { getModuleModelsBySlot, getDeckErrors } from './utils'
import type { ModuleModel } from '@opentrons/shared-data'
import type { ThunkDispatch } from '../../../types'
@@ -71,6 +71,8 @@ interface DeckSetupToolsProps {
} | null
}
+export type CategoryExpand = Record
+
export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
const { onCloseClick, setHoveredLabware, onDeckProps } = props
const { t, i18n } = useTranslation(['starting_deck_state', 'shared'])
@@ -117,6 +119,28 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
const [tab, setTab] = useState<'hardware' | 'labware'>(
moduleModels?.length === 0 || slot === 'offDeck' ? 'labware' : 'hardware'
)
+
+ const setAllCategories = (state: boolean): Record =>
+ ALL_ORDERED_CATEGORIES.reduce>(
+ (acc, category) => ({ ...acc, [category]: state }),
+ {}
+ )
+ const allCategoriesExpanded = setAllCategories(true)
+ const allCategoriesCollapsed = setAllCategories(false)
+ const [
+ areCategoriesExpanded,
+ setAreCategoriesExpanded,
+ ] = useState(allCategoriesCollapsed)
+ const [searchTerm, setSearchTerm] = useState('')
+
+ useEffect(() => {
+ if (searchTerm !== '') {
+ setAreCategoriesExpanded(allCategoriesExpanded)
+ } else {
+ setAreCategoriesExpanded(allCategoriesCollapsed)
+ }
+ }, [searchTerm])
+
const hasMagneticModule = Object.values(deckSetup.modules).some(
module => module.type === MAGNETIC_MODULE_TYPE
)
@@ -124,6 +148,12 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
Object.values(deckSetup.modules).find(module => module.slot === slot)
?.model === MAGNETIC_MODULE_V1
+ const handleCollapseAllCategories = (): void => {
+ setAreCategoriesExpanded(allCategoriesCollapsed)
+ }
+ const handleResetSearchTerm = (): void => {
+ setSearchTerm('')
+ }
const changeModuleWarning = useBlockingHint({
hintKey: 'change_magnet_module_model',
handleCancel: () => {
@@ -207,6 +237,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
)
}
+ const handleResetLabwareTools = (): void => {
+ handleCollapseAllCategories()
+ handleResetSearchTerm()
+ }
+
const handleClear = (): void => {
onDeckProps?.setHoveredModule(null)
onDeckProps?.setHoveredFixture(null)
@@ -242,7 +277,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
}
}
handleResetToolbox()
+ handleResetLabwareTools()
setSelectedHardware(null)
+ if (selectedHardware != null) {
+ setTab('hardware')
+ }
}
const handleConfirm = (): void => {
// clear entities first before recreating them
@@ -548,7 +587,15 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
)}
) : (
-
+
)}
diff --git a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx
index dec0d114f83..be4f457429e 100644
--- a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx
+++ b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx
@@ -1,4 +1,4 @@
-import * as React from 'react'
+import { Fragment, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import reduce from 'lodash/reduce'
import styled from 'styled-components'
@@ -49,7 +49,11 @@ import {
selectLabware,
selectNestedLabware,
} from '../../../labware-ingred/actions'
-import { ORDERED_CATEGORIES } from './constants'
+import {
+ ALL_ORDERED_CATEGORIES,
+ CUSTOM_CATEGORY,
+ ORDERED_CATEGORIES,
+} from './constants'
import {
getLabwareIsRecommended,
getLabwareCompatibleWithAdapter,
@@ -59,8 +63,8 @@ import type { DeckSlotId, LabwareDefinition2 } from '@opentrons/shared-data'
import type { ModuleOnDeck } from '../../../step-forms'
import type { ThunkDispatch } from '../../../types'
import type { LabwareDefByDefURI } from '../../../labware-defs'
+import type { CategoryExpand } from './DeckSetupTools'
-const CUSTOM_CATEGORY = 'custom'
const STANDARD_X_DIMENSION = 127.75
const STANDARD_Y_DIMENSION = 85.48
const PLATE_READER_LOADNAME =
@@ -68,10 +72,28 @@ const PLATE_READER_LOADNAME =
interface LabwareToolsProps {
slot: DeckSlotId
setHoveredLabware: (defUri: string | null) => void
+ searchTerm: string
+ setSearchTerm: React.Dispatch>
+ areCategoriesExpanded: CategoryExpand
+ setAreCategoriesExpanded: React.Dispatch>
+ handleReset: () => void
+}
+
+interface LabwareInfo {
+ uri: string
+ def: LabwareDefinition2
}
export function LabwareTools(props: LabwareToolsProps): JSX.Element {
- const { slot, setHoveredLabware } = props
+ const {
+ slot,
+ setHoveredLabware,
+ searchTerm,
+ setSearchTerm,
+ areCategoriesExpanded,
+ setAreCategoriesExpanded,
+ handleReset,
+ } = props
const { t } = useTranslation(['starting_deck_state', 'shared'])
const robotType = useSelector(getRobotType)
const dispatch = useDispatch>()
@@ -87,10 +109,6 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
selectedModuleModel,
selectedNestedLabwareDefUri,
} = zoomedInSlotInfo
- const [selectedCategory, setSelectedCategory] = React.useState(
- null
- )
- const [searchTerm, setSearchTerm] = React.useState('')
const searchFilter = (termToCheck: string): boolean =>
termToCheck.toLowerCase().includes(searchTerm.toLowerCase())
@@ -101,7 +119,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map(
moduleId => modulesById[moduleId]
)
- const [filterRecommended, setFilterRecommended] = React.useState(
+ const [filterRecommended, setFilterRecommended] = useState(
moduleType != null
)
// for OT-2 usage only due to H-S collisions
@@ -110,11 +128,11 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
hardwareModule.type === HEATERSHAKER_MODULE_TYPE &&
getAreSlotsHorizontallyAdjacent(hardwareModule.slot, slot)
)
- const [filterHeight, setFilterHeight] = React.useState(
+ const [filterHeight, setFilterHeight] = useState(
robotType === OT2_ROBOT_TYPE ? isNextToHeaterShaker : false
)
- const getLabwareCompatible = React.useCallback(
+ const getLabwareCompatible = useCallback(
(def: LabwareDefinition2) => {
// assume that custom (non-standard) labware is (potentially) compatible
if (moduleType == null || !getLabwareDefIsStandard(def)) {
@@ -125,7 +143,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
[moduleType]
)
- const getIsLabwareFiltered = React.useCallback(
+ const getIsLabwareFiltered = useCallback(
(labwareDef: LabwareDefinition2) => {
const { dimensions, parameters } = labwareDef
const { xDimension, yDimension } = dimensions
@@ -155,11 +173,8 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
},
[filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot]
)
- const customLabwareURIs: string[] = React.useMemo(
- () => Object.keys(customLabwareDefs),
- [customLabwareDefs]
- )
- const labwareByCategory = React.useMemo(() => {
+
+ const labwareByCategory = useMemo(() => {
return reduce<
LabwareDefByDefURI,
{ [category: string]: LabwareDefinition2[] }
@@ -184,28 +199,51 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
)
}, [permittedTipracks])
- const populatedCategories: { [category: string]: boolean } = React.useMemo(
+ const filteredLabwareByCategory: Record = useMemo(
() =>
- ORDERED_CATEGORIES.reduce((acc, category) => {
+ ALL_ORDERED_CATEGORIES.reduce((acc, category) => {
+ if (category === 'custom') {
+ return {
+ ...acc,
+ [category]: filterRecommended
+ ? []
+ : Object.entries(customLabwareDefs).reduce(
+ (accInner, [uri, def]) => {
+ return searchFilter(def.metadata.displayName)
+ ? [...accInner, { uri, def }]
+ : accInner
+ },
+ []
+ ),
+ }
+ }
const isDeckLocationCategory =
slot === 'offDeck' ? category !== 'adapter' : true
- return category in labwareByCategory &&
- isDeckLocationCategory &&
- labwareByCategory[category].some(lw =>
- searchFilter(lw.metadata.displayName)
- )
- ? {
- ...acc,
- [category]: labwareByCategory[category].some(
- def => !getIsLabwareFiltered(def)
- ),
- }
- : acc
+ if (!(category in labwareByCategory) || !isDeckLocationCategory) {
+ return { ...acc, [category]: [] }
+ }
+ return {
+ ...acc,
+ [category]: labwareByCategory[category].reduce(
+ (accInner, def) => {
+ return searchFilter(def.metadata.displayName) &&
+ !getIsLabwareFiltered(def)
+ ? [...accInner, { def, uri: getLabwareDefURI(def) }]
+ : accInner
+ },
+ []
+ ),
+ }
}, {}),
[labwareByCategory, getIsLabwareFiltered, searchTerm]
)
- const handleCategoryClick = (category: string): void => {
- setSelectedCategory(selectedCategory === category ? null : category)
+
+ const handleCategoryClick = (category: string, expand?: boolean): void => {
+ const updatedExpandState = {
+ ...areCategoriesExpanded,
+ [category]: expand ?? !areCategoriesExpanded[category],
+ }
+ setAreCategoriesExpanded(updatedExpandState)
}
return (
@@ -223,9 +261,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
size="medium"
leftIcon="search"
showDeleteIcon
- onDelete={() => {
- setSearchTerm('')
- }}
+ onDelete={handleReset}
/>
{moduleType != null ||
(isNextToHeaterShaker && robotType === OT2_ROBOT_TYPE) ? (
@@ -253,7 +289,7 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
gridGap={SPACING.spacing4}
paddingTop={SPACING.spacing8}
>
- {customLabwareURIs.length === 0 ? null : (
+ {filteredLabwareByCategory[CUSTOM_CATEGORY].length > 0 ? (
- {customLabwareURIs.map((labwareURI, index) => (
- {
- setHoveredLabware(null)
- }}
- setHovered={() => {
- setHoveredLabware(labwareURI)
- }}
- buttonValue={labwareURI}
- onChange={e => {
- e.stopPropagation()
- dispatch(selectLabware({ labwareDefUri: labwareURI }))
- }}
- isSelected={labwareURI === selectedLabwareDefUri}
- />
- ))}
+ {filteredLabwareByCategory[CUSTOM_CATEGORY].map(
+ ({ uri }, index) => (
+ {
+ setHoveredLabware(null)
+ }}
+ setHovered={() => {
+ setHoveredLabware(uri)
+ }}
+ buttonValue={uri}
+ onChange={e => {
+ e.stopPropagation()
+ dispatch(selectLabware({ labwareDefUri: uri }))
+ }}
+ isSelected={uri === selectedLabwareDefUri}
+ />
+ )
+ )}
- )}
+ ) : null}
{ORDERED_CATEGORIES.map(category => {
- const isPopulated = populatedCategories[category]
- if (isPopulated) {
+ if (filteredLabwareByCategory[category].length > 0) {
return (
- {labwareByCategory[category]?.map((labwareDef, index) => {
- const isFiltered = getIsLabwareFiltered(labwareDef)
- const labwareURI = getLabwareDefURI(labwareDef)
- const loadName = labwareDef.parameters.loadName
- const isMatch = searchFilter(
- labwareDef.metadata.displayName
- )
- if (!isFiltered && isMatch) {
- return (
-
+ {filteredLabwareByCategory[category]?.map(
+ ({ def, uri }, index) => {
+ const loadName = def.parameters.loadName
+
+ return searchFilter(def.metadata.displayName) &&
+ !getIsLabwareFiltered(def) ? (
+
{
setHoveredLabware(null)
}}
setHovered={() => {
- setHoveredLabware(labwareURI)
+ setHoveredLabware(uri)
}}
id={`${index}_${category}_${loadName}`}
- buttonText={labwareDef.metadata.displayName}
- buttonValue={labwareURI}
+ buttonText={def.metadata.displayName}
+ buttonValue={uri}
onChange={e => {
e.stopPropagation()
dispatch(
selectLabware({
labwareDefUri:
- labwareURI === selectedLabwareDefUri
+ uri === selectedLabwareDefUri
? null
- : labwareURI,
+ : uri,
})
)
// reset the nested labware def uri in case it is not compatible
@@ -346,10 +376,10 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
})
)
}}
- isSelected={labwareURI === selectedLabwareDefUri}
+ isSelected={uri === selectedLabwareDefUri}
/>
- {labwareURI === selectedLabwareDefUri &&
+ {uri === selectedLabwareDefUri &&
getLabwareCompatibleWithAdapter(loadName)
?.length > 0 && (
{has96Channel &&
loadName === ADAPTER_96_CHANNEL
@@ -440,10 +468,10 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
)}
-
- )
+
+ ) : null
}
- })}
+ )}
@@ -464,8 +492,8 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element {
data-testid="customLabwareInput"
type="file"
onChange={e => {
- setSelectedCategory(CUSTOM_CATEGORY)
dispatch(createCustomLabwareDef(e))
+ handleCategoryClick(CUSTOM_CATEGORY, true)
}}
/>
diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx
index b4d444a1cf3..479724f3527 100644
--- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx
+++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx
@@ -20,10 +20,6 @@ import { selectors } from '../../../../labware-ingred/selectors'
import { createCustomLabwareDef } from '../../../../labware-defs/actions'
import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors'
import { getRobotType } from '../../../../file-data/selectors'
-import {
- selectLabware,
- selectNestedLabware,
-} from '../../../../labware-ingred/actions'
import { LabwareTools } from '../LabwareTools'
import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data'
@@ -48,6 +44,11 @@ describe('LabwareTools', () => {
props = {
slot: 'D3',
setHoveredLabware: vi.fn(),
+ searchTerm: '',
+ setSearchTerm: vi.fn(),
+ areCategoriesExpanded: {},
+ setAreCategoriesExpanded: vi.fn(),
+ handleReset: vi.fn(),
}
vi.mocked(getCustomLabwareDefsByURI).mockReturnValue({})
vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE)
@@ -80,18 +81,14 @@ describe('LabwareTools', () => {
it('renders an empty slot with all the labware options', () => {
render(props)
screen.getByText('Add labware')
- screen.getByText('Tube rack')
- screen.getByText('Well plate')
- screen.getByText('Reservoir')
- screen.getByText('Aluminum block')
- screen.getByText('Adapter')
+ screen.getByText('Tube racks')
+ screen.getByText('Well plates')
+ screen.getByText('Reservoirs')
+ screen.getByText('Aluminum blocks')
+ screen.getByText('Adapters')
// click and expand well plate accordion
fireEvent.click(screen.getAllByTestId('ListButton_noActive')[1])
- fireEvent.click(
- screen.getByRole('label', { name: 'Corning 384 Well Plate' })
- )
- // set labware
- expect(vi.mocked(selectLabware)).toHaveBeenCalled()
+ expect(props.setAreCategoriesExpanded).toBeCalled()
})
it('renders deck slot and selects an adapter and labware', () => {
vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({
@@ -102,23 +99,10 @@ describe('LabwareTools', () => {
selectedSlot: { slot: 'D3', cutout: 'cutoutD3' },
})
render(props)
- screen.getByText('Adapter')
+ screen.getByText('Adapters')
fireEvent.click(screen.getAllByTestId('ListButton_noActive')[4])
// set adapter
- fireEvent.click(
- screen.getByRole('label', {
- name: 'Fixture Opentrons Universal Flat Heater-Shaker Adapter',
- })
- )
- // set labware
- screen.getByText('Adapter compatible labware')
- screen.getByText('Fixture Corning 96 Well Plate 360 µL Flat')
- fireEvent.click(
- screen.getByRole('label', {
- name: 'Fixture Corning 96 Well Plate 360 µL Flat',
- })
- )
- expect(vi.mocked(selectNestedLabware)).toHaveBeenCalled()
+ expect(props.setAreCategoriesExpanded).toBeCalled()
})
it('renders the custom labware flow', () => {
diff --git a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts
index 53571367f8b..e1acb64424d 100644
--- a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts
+++ b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts
@@ -56,6 +56,8 @@ export const ORDERED_CATEGORIES: string[] = [
'aluminumBlock',
'adapter',
]
+export const CUSTOM_CATEGORY = 'custom'
+export const ALL_ORDERED_CATEGORIES = [CUSTOM_CATEGORY, ...ORDERED_CATEGORIES]
export const RECOMMENDED_LABWARE_BY_MODULE: { [K in ModuleType]: string[] } = {
[TEMPERATURE_MODULE_TYPE]: [
diff --git a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts
index 524a19dfc1c..7a1c7c09be3 100644
--- a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts
+++ b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts
@@ -118,18 +118,16 @@ export const getLabwareIsRecommended = (
): boolean => {
// special-casing the thermocycler module V2 recommended labware since the thermocyclerModuleTypes
// have different recommended labware
- const moduleType = moduleModel != null ? getModuleType(moduleModel) : null
- if (moduleModel === THERMOCYCLER_MODULE_V2) {
- return (
- def.parameters.loadName === 'opentrons_96_wellplate_200ul_pcr_full_skirt'
- )
- } else {
- return moduleType != null
- ? RECOMMENDED_LABWARE_BY_MODULE[moduleType].includes(
- def.parameters.loadName
- )
- : false
+ if (moduleModel == null) {
+ // permissive early exit if no module passed
+ return true
}
+ const moduleType = getModuleType(moduleModel)
+ return moduleModel === THERMOCYCLER_MODULE_V2
+ ? def.parameters.loadName === 'opentrons_96_wellplate_200ul_pcr_full_skirt'
+ : RECOMMENDED_LABWARE_BY_MODULE[moduleType].includes(
+ def.parameters.loadName
+ )
}
export const getLabwareCompatibleWithAdapter = (