Skip to content

Commit

Permalink
feat(protocol-designer): edit labware nickname modal (#16151)
Browse files Browse the repository at this point in the history
closes AUTH-737
  • Loading branch information
jerader authored Aug 29, 2024
1 parent 2499a82 commit b0f944a
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"fixtures_replace": "Fixtures replace standard deck slots and let you add functionality to your Flex.",
"incompatible_tip_body": "Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.",
"incompatible_tips": "Incompatible tips",
"labware_name": "Labware name",
"modules_added": "Modules added",
"name": "Name",
"need_gripper": "Do you want to move labware automatically with the gripper?",
Expand All @@ -23,18 +24,19 @@
"quantity": "Quantity",
"questions": "We’re going to ask a few questions to help you get started building your protocol.",
"remove": "Remove",
"rename_labware": "Rename labware",
"robot_pips": "Robot pipettes",
"robot_type": "What kind of robot do you have?",
"show_all_tips": "Show all tips",
"show_default_tips": "Show default tips",
"show_tips": "Show incompatible tips",
"slots_limit_reached": "Slots limit reached",
"stagingArea": "Staging area",
"swap": "Swap pipettes",
"slots_limit_reached": "Slots limit reached",
"trash_required": "A trash entity is required",
"tell_us": "Tell us about your protocol",
"up_to_3_tipracks": "Up to 3 tip rack types are allowed per pipette",
"trash_required": "A trash entity is required",
"trashBin": "Trash Bin",
"up_to_3_tipracks": "Up to 3 tip rack types are allowed per pipette",
"vol_label": "{{volume}} uL",
"wasteChute": "Waste Chute",
"which_fixtures": "Which fixtures will you be using?",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { fireEvent, screen } from '@testing-library/react'
import { i18n } from '../../../assets/localization'
import { renderWithProviders } from '../../../__testing-utils__'
import { EditNickNameModal } from '..'
import { getLabwareNicknamesById } from '../../../ui/labware/selectors'
import { renameLabware } from '../../../labware-ingred/actions'

vi.mock('../../../ui/labware/selectors')
vi.mock('../../../labware-ingred/actions')

const render = (props: React.ComponentProps<typeof EditNickNameModal>) => {
return renderWithProviders(<EditNickNameModal {...props} />, {
i18nInstance: i18n,
})[0]
}

describe('EditNickNameModal', () => {
let props: React.ComponentProps<typeof EditNickNameModal>

beforeEach(() => {
props = {
onClose: vi.fn(),
labwareId: 'mockId',
}
vi.mocked(getLabwareNicknamesById).mockReturnValue({
mockId: 'mockOriginalName',
})
})
it('renders the text and adds a nickname', () => {
render(props)
screen.getByText('Rename labware')
screen.getByText('Labware name')

fireEvent.click(screen.getByText('Cancel'))
expect(props.onClose).toHaveBeenCalled()

const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'mockNickName' } })
fireEvent.click(screen.getByText('Save'))
expect(vi.mocked(renameLabware)).toHaveBeenCalled()
expect(props.onClose).toHaveBeenCalled()
})
})
82 changes: 82 additions & 0 deletions protocol-designer/src/organisms/EditNickNameModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { createPortal } from 'react-dom'
import {
COLORS,
DIRECTION_COLUMN,
Flex,
InputField,
JUSTIFY_END,
Modal,
PrimaryButton,
SecondaryButton,
SPACING,
StyledText,
} from '@opentrons/components'
import { selectors as uiLabwareSelectors } from '../../ui/labware'
import { getTopPortalEl } from '../../components/portals/TopPortal'
import { renameLabware } from '../../labware-ingred/actions'
import type { ThunkDispatch } from '../../types'

interface EditNickNameModalProps {
labwareId: string
onClose: () => void
}
export function EditNickNameModal(props: EditNickNameModalProps): JSX.Element {
const { onClose, labwareId } = props
const { t } = useTranslation(['create_new_protocol', 'shared'])
const dispatch = useDispatch<ThunkDispatch<any>>()
const nickNames = useSelector(uiLabwareSelectors.getLabwareNicknamesById)
const savedNickname = nickNames[labwareId]
const [nickName, setNickName] = React.useState<string>(savedNickname)
const saveNickname = (): void => {
dispatch(renameLabware({ labwareId, name: nickName }))
onClose()
}

return createPortal(
<Modal
title={t('rename_labware')}
type="info"
onClose={onClose}
footer={
<Flex
justifyContent={JUSTIFY_END}
gridGap={SPACING.spacing8}
padding={SPACING.spacing24}
>
<SecondaryButton
onClick={() => {
onClose()
}}
>
{t('shared:cancel')}
</SecondaryButton>
<PrimaryButton onClick={saveNickname}>
{t('shared:save')}
</PrimaryButton>
</Flex>
}
>
<Flex flexDirection={DIRECTION_COLUMN} girGap={SPACING.spacing4}>
<Flex color={COLORS.grey60}>
<StyledText desktopStyle="bodyDefaultRegular">
{t('labware_name')}
</StyledText>
</Flex>
<InputField
data-testid="renameLabware_inputField"
name="renameLabware"
onChange={e => {
setNickName(e.target.value)
}}
value={nickName}
type="text"
autoFocus
/>
</Flex>
</Modal>,
getTopPortalEl()
)
}
14 changes: 6 additions & 8 deletions protocol-designer/src/organisms/SlotDetailsContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FLEX_ROBOT_TYPE, getModuleDisplayName } from '@opentrons/shared-data'
import { RobotCoordsForeignObject } from '@opentrons/components'
import * as wellContentsSelectors from '../../top-selectors/well-contents'
import { selectors } from '../../labware-ingred/selectors'
import { selectors as uiLabwareSelectors } from '../../ui/labware'
import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations'
import { SlotInformation } from '../../organisms/SlotInformation'
import { getYPosition } from './utils'
Expand All @@ -29,6 +30,7 @@ export function SlotDetailsContainer(
const allWellContentsForActiveItem = useSelector(
wellContentsSelectors.getAllWellContentsForActiveItem
)
const nickNames = useSelector(uiLabwareSelectors.getLabwareNicknamesById)
const allIngredNamesIds = useSelector(selectors.allIngredientNamesIds)

if (slot == null || (slot === 'offDeck' && offDeckLabwareId == null)) {
Expand All @@ -55,10 +57,6 @@ export function SlotDetailsContainer(
const nestedLabwareOnSlot = Object.values(deckSetupLabwares).find(lw =>
Object.keys(deckSetupLabwares).includes(lw.slot)
)
const labwareOnSlotDisplayName = labwareOnSlot?.def.metadata.displayName
const nestedLabwareOnSlotDisplayName =
nestedLabwareOnSlot?.def.metadata.displayName

const fixturesOnSlot = Object.values(additionalEquipmentOnDeck).filter(
ae => ae.location?.split('cutout')[1] === slot
)
Expand Down Expand Up @@ -98,11 +96,11 @@ export function SlotDetailsContainer(
if (offDeckLabwareDisplayName != null) {
labwares.push(offDeckLabwareDisplayName)
} else {
if (labwareOnSlotDisplayName != null) {
labwares.push(labwareOnSlotDisplayName)
if (labwareOnSlot != null) {
labwares.push(nickNames[labwareOnSlot.id])
}
if (nestedLabwareOnSlotDisplayName != null) {
labwares.push(nestedLabwareOnSlotDisplayName)
if (nestedLabwareOnSlot != null) {
labwares.push(nickNames[nestedLabwareOnSlot.id])
}
}

Expand Down
1 change: 1 addition & 0 deletions protocol-designer/src/organisms/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './DefineLiquidsModal'
export * from './EditNickNameModal'
export * from './FeatureFlagsModal'
export * from './FileUploadMessagesModal/'
export * from './IncompatibleTipsModal'
Expand Down
163 changes: 92 additions & 71 deletions protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '@opentrons/components'
import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations'
import { deleteModule } from '../../../step-forms/actions'
import { EditNickNameModal } from '../../../organisms'
import { deleteDeckFixture } from '../../../step-forms/actions/additionalItems'
import {
deleteContainer,
Expand All @@ -33,9 +34,14 @@ export function SlotOverflowMenu(
const { slot, setShowMenuList, addEquipment } = props
const { t } = useTranslation('starting_deck_state')
const dispatch = useDispatch<ThunkDispatch<any>>()
const [showNickNameModal, setShowNickNameModal] = React.useState<boolean>(
false
)
const overflowWrapperRef = useOnClickOutside<HTMLDivElement>({
onClickOutside: () => {
setShowMenuList(false)
if (!showNickNameModal) {
setShowMenuList(false)
}
},
})
const deckSetup = useSelector(getDeckSetupForActiveItem)
Expand Down Expand Up @@ -86,79 +92,94 @@ export function SlotOverflowMenu(
}

return (
<Flex
whiteSpace={NO_WRAP}
ref={overflowWrapperRef}
borderRadius={BORDERS.borderRadius8}
boxShadow="0px 1px 3px rgba(0, 0, 0, 0.2)"
backgroundColor={COLORS.white}
flexDirection={DIRECTION_COLUMN}
onClick={(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
}}
>
<MenuButton
onClick={() => {
addEquipment(slot)
setShowMenuList(false)
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('add_hw_lw')}
</StyledText>
</MenuButton>
<MenuButton
disabled={labwareOnSlot == null || isLabwareAnAdapter}
onClick={() => {
// todo(ja, 8/22/24): wire this up
console.log('open nick name modal')
setShowMenuList(false)
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('rename_lab')}
</StyledText>
</MenuButton>
<MenuButton
disabled={labwareOnSlot == null || isLabwareTiprack}
onClick={() => {
// todo(ja, 8/22/24): wire this up
console.log('open liquids')
setShowMenuList(false)
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('add_liquid')}
</StyledText>
</MenuButton>
<MenuButton
disabled={labwareOnSlot == null && !isLabwareAnAdapter}
onClick={() => {
if (labwareOnSlot != null && !isLabwareAnAdapter) {
dispatch(duplicateLabware(labwareOnSlot.id))
} else if (nestedLabwareOnSlot != null) {
dispatch(duplicateLabware(nestedLabwareOnSlot.id))
<>
{showNickNameModal && labwareOnSlot != null ? (
<EditNickNameModal
labwareId={
nestedLabwareOnSlot != null
? nestedLabwareOnSlot.id
: labwareOnSlot.id
}
setShowMenuList(false)
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('duplicate')}
</StyledText>
</MenuButton>
<MenuButton
disabled={hasNoItems || hasTrashOnSlot}
onClick={() => {
handleClear()
setShowMenuList(false)
onClose={() => {
setShowNickNameModal(false)
setShowMenuList(false)
}}
/>
) : null}
<Flex
whiteSpace={NO_WRAP}
ref={overflowWrapperRef}
borderRadius={BORDERS.borderRadius8}
boxShadow="0px 1px 3px rgba(0, 0, 0, 0.2)"
backgroundColor={COLORS.white}
flexDirection={DIRECTION_COLUMN}
onClick={(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('clear_slot')}
</StyledText>
</MenuButton>
</Flex>
<MenuButton
onClick={() => {
addEquipment(slot)
setShowMenuList(false)
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('add_hw_lw')}
</StyledText>
</MenuButton>
<MenuButton
disabled={labwareOnSlot == null || isLabwareAnAdapter}
onClick={(e: React.MouseEvent) => {
setShowNickNameModal(true)
e.preventDefault()
e.stopPropagation()
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('rename_lab')}
</StyledText>
</MenuButton>
<MenuButton
disabled={labwareOnSlot == null || isLabwareTiprack}
onClick={() => {
// todo(ja, 8/22/24): wire this up
console.log('open liquids')
setShowMenuList(false)
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('add_liquid')}
</StyledText>
</MenuButton>
<MenuButton
disabled={labwareOnSlot == null && !isLabwareAnAdapter}
onClick={() => {
if (labwareOnSlot != null && !isLabwareAnAdapter) {
dispatch(duplicateLabware(labwareOnSlot.id))
} else if (nestedLabwareOnSlot != null) {
dispatch(duplicateLabware(nestedLabwareOnSlot.id))
}
setShowMenuList(false)
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('duplicate')}
</StyledText>
</MenuButton>
<MenuButton
disabled={hasNoItems || hasTrashOnSlot}
onClick={() => {
handleClear()
setShowMenuList(false)
}}
>
<StyledText desktopStyle="bodyDefaultRegular">
{t('clear_slot')}
</StyledText>
</MenuButton>
</Flex>
</>
)
}

Expand Down
Loading

0 comments on commit b0f944a

Please sign in to comment.