Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol-designer): wire up rename step #16437

Merged
merged 13 commits into from
Oct 17, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"protocol_steps": "Protocol steps",
"protocol_timeline": "Protocol timeline",
"rename": "Rename",
"rename_error": "Oops! Your step name is too long.",
"save_errors": "{{stepType}} has been saved with {{numErrors}} error(s)",
"save_no_errors": "{{stepType}} has been saved",
"save_warnings_and_errors": "{{stepType}} has been saved with {{numErrors}} error(s) and {{numWarnings}} warning(s)",
Expand Down
1 change: 1 addition & 0 deletions protocol-designer/src/assets/localization/en/shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"message_uses_standard_namespace": "This labware definition uses the Opentrons standard labware namespace. Change the namespace if it is custom, or use the standard labware in your protocol.",
"mismatched": "The new labware has a different arrangement of wells than the labware it is replacing. Clicking Overwrite will deselect all wells in any existing steps that use this labware. You will have to edit each of those steps and select new wells.",
"module": "Module",
"name_step": "Name step",
"next": "next",
"ninety_six_channel": "96-Channel",
"no_hints_to_restore": "No hints to restore",
Expand Down
17 changes: 17 additions & 0 deletions protocol-designer/src/labware-ingred/actions/actions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createAction } from 'redux-actions'
import { selectors } from '../selectors'
import type { StepFieldName } from '../../form-types'
import type { DeckSlot, ThunkAction } from '../../types'
import type { Fixture, IngredInputs } from '../types'
import type { CutoutId, ModuleModel } from '@opentrons/shared-data'

// ===== Labware selector actions =====
export interface OpenAddLabwareModalAction {
type: 'OPEN_ADD_LABWARE_MODAL'
Expand Down Expand Up @@ -295,3 +297,18 @@ export const generateNewProtocol: (
type: 'GENERATE_NEW_PROTOCOL',
payload,
})

export interface RenameStepAction {
type: 'CHANGE_STEP_DETAILS'
payload: {
stepId?: string
update: Partial<Record<StepFieldName, unknown | null>>
}
}

export const renameStep: (
payload: RenameStepAction['payload']
) => RenameStepAction = payload => ({
type: 'CHANGE_STEP_DETAILS',
payload,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { fireEvent, screen } from '@testing-library/react'
import { describe, it, beforeEach, vi, expect } from 'vitest'
import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../assets/localization'
import { PAUSE_UNTIL_RESUME } from '../../../constants'
import { renameStep } from '../../../labware-ingred/actions'
import { RenameStepModal } from '..'

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

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

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

beforeEach(() => {
props = {
onClose: vi.fn(),
formData: {
stepType: 'pause',
id: 'test_id',
pauseAction: PAUSE_UNTIL_RESUME,
description: 'some description',
pauseMessage: 'some message',
stepName: 'pause',
stepDetails: '',
},
}
})
it('renders the text and add a step name and a step notes', () => {
render(props)
screen.getByText('Name step')
screen.getByText('Step Name')
screen.getByText('Step Notes')

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

const stepName = screen.getAllByRole('textbox', { name: '' })[0]
fireEvent.change(stepName, { target: { value: 'mockStepName' } })

const stepDetails = screen.getAllByRole('textbox', { name: '' })[1]
fireEvent.change(stepDetails, { target: { value: 'mockStepDetails' } })

fireEvent.click(screen.getByText('Save'))
expect(vi.mocked(renameStep)).toHaveBeenCalled()
expect(props.onClose).toHaveBeenCalled()
})
it('renders the too long step name error', () => {
render(props)
const stepName = screen.getAllByRole('textbox', { name: '' })[0]
fireEvent.change(stepName, {
target: {
value:
'mockStepNameisthelongeststepnameihaveeverseen mockstepNameisthelongeststepnameihaveeverseen mockstepNameisthelongest',
},
})
screen.getByText('Oops! Your step name is too long.')
})
})
129 changes: 129 additions & 0 deletions protocol-designer/src/organisms/RenameStepModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useState } from 'react'
import { useDispatch } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { createPortal } from 'react-dom'
import styled from 'styled-components'
import {
BORDERS,
COLORS,
DIRECTION_COLUMN,
Flex,
JUSTIFY_END,
Modal,
PrimaryButton,
SecondaryButton,
SPACING,
StyledText,
TYPOGRAPHY,
InputField,
} from '@opentrons/components'
import { i18n } from '../../assets/localization'
import { getTopPortalEl } from '../../components/portals/TopPortal'
import { renameStep } from '../../labware-ingred/actions'
import type { FormData } from '../../form-types'

const MAX_STEP_NAME_LENGTH = 60
interface RenameStepModalProps {
formData: FormData
onClose: () => void
}
export function RenameStepModal(props: RenameStepModalProps): JSX.Element {
const { onClose, formData } = props
const dispatch = useDispatch()
const { t } = useTranslation(['form', 'shared', 'protocol_steps'])
Comment on lines +32 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const dispatch = useDispatch()
const { t } = useTranslation(['form', 'shared', 'protocol_steps'])
const { t } = useTranslation(['form', 'shared', 'protocol_steps'])
const dispatch = useDispatch()

const initialName = i18n.format(t(formData.stepName), 'capitalize')
const [stepName, setStepName] = useState<string>(initialName)
const [stepDetails, setStepDetails] = useState<string>(
String(formData.stepDetails)
)

const handleSave = (): void => {
const { stepId } = formData
dispatch(
renameStep({
stepId,
update: {
stepName: stepName,
stepDetails: stepDetails,
},
})
)
onClose()
}

return createPortal(
<Modal
title={t('shared:name_step')}
type="info"
closeOnOutsideClick
onClose={onClose}
childrenPadding={SPACING.spacing24}
footer={
<Flex
justifyContent={JUSTIFY_END}
padding={`0 ${SPACING.spacing24} ${SPACING.spacing24}`}
gridGap={SPACING.spacing8}
>
<SecondaryButton onClick={onClose}>
{t('shared:cancel')}
</SecondaryButton>
<PrimaryButton
data-testid="RenameStepModal_saveButton"
disabled={stepName.length >= MAX_STEP_NAME_LENGTH}
onClick={() => {
handleSave()
}}
>
{t('shared:save')}
</PrimaryButton>
</Flex>
}
>
<form>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing12}>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
<StyledText color={COLORS.grey60} desktopStyle="captionRegular">
{t('form:step_edit_form.field.step_name.label')}
</StyledText>
<InputField
error={
stepName.length >= MAX_STEP_NAME_LENGTH
? t('protocol_steps:rename_error')
: null
}
value={stepName}
autoFocus
onChange={e => {
setStepName(e.target.value)
}}
type="text"
/>
</Flex>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
<StyledText color={COLORS.grey60} desktopStyle="captionRegular">
{t('form:step_edit_form.field.step_notes.label')}
</StyledText>

<DescriptionField
value={stepDetails}
onChange={e => {
setStepDetails(e.target.value)
}}
/>
</Flex>
</Flex>
</form>
</Modal>,
getTopPortalEl()
)
}

const DescriptionField = styled.textarea`
min-height: 5rem;
width: 100%;
border: ${BORDERS.lineBorder};
border-radius: ${BORDERS.borderRadius4};
padding: ${SPACING.spacing8};
font-size: ${TYPOGRAPHY.fontSizeH3};
resize: none;
`
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { stepIconsByType } from '../../../../form-types'
import { FormAlerts } from '../../../../organisms'
import { useKitchen } from '../../../../organisms/Kitchen/hooks'
import { RenameStepModal } from '../../../../organisms/RenameStepModal'
import { getFormWarningsForSelectedStep } from '../../../../dismiss/selectors'
import { getTimelineWarningsForSelectedStep } from '../../../../top-selectors/timelineWarnings'
import { getRobotStateTimeline } from '../../../../file-data/selectors'
Expand Down Expand Up @@ -97,6 +98,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
? 1
: 0
)
const [isRename, setIsRename] = useState<boolean>(false)
const icon = stepIconsByType[formData.stepType]

const ToolsComponent: typeof STEP_FORM_MAP[keyof typeof STEP_FORM_MAP] = get(
Expand Down Expand Up @@ -136,8 +138,17 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
}) as string
)
}

return (
<>
{isRename ? (
<RenameStepModal
formData={formData}
onClose={() => {
setIsRename(false)
}}
/>
) : null}
<Toolbox
subHeader={
isMultiStepToolbox ? (
Expand All @@ -149,7 +160,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
secondaryHeaderButton={
<Btn
onClick={() => {
console.log('TODO: wire this up')
setIsRename(true)
}}
css={BUTTON_LINK_STYLE}
textDecoration={TYPOGRAPHY.textDecorationUnderline}
Expand Down Expand Up @@ -198,7 +209,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
<Flex gridGap={SPACING.spacing8} alignItems={ALIGN_CENTER}>
<Icon size="1rem" name={icon} />
<StyledText desktopStyle="bodyLargeSemiBold">
{i18n.format(t(`stepType.${formData.stepType}`), 'capitalize')}
{i18n.format(t(formData.stepName), 'capitalize')}
</StyledText>
</Flex>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ export function MagnetTools(props: StepFormProps): JSX.Element {
const deckSetup = useSelector(getInitialDeckSetup)
const modulesOnDeck = getModulesOnDeckByType(deckSetup, MAGNETIC_MODULE_TYPE)

console.log(modulesOnDeck)

const moduleModel = moduleEntities[formData.moduleId].model

const slotInfo = moduleLabwareOptions[0].name.split('in')
Expand Down
13 changes: 13 additions & 0 deletions protocol-designer/src/step-forms/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import type {
CreateContainerAction,
DeleteContainerAction,
DuplicateLabwareAction,
RenameStepAction,
SwapSlotContentsAction,
} from '../../labware-ingred/actions'
import type {
Expand Down Expand Up @@ -157,6 +158,7 @@ export type UnsavedFormActions =
| ToggleIsGripperRequiredAction
| CreateDeckFixtureAction
| DeleteDeckFixtureAction
| RenameStepAction
export const unsavedForm = (
rootState: RootState,
action: UnsavedFormActions
Expand Down Expand Up @@ -213,6 +215,17 @@ export const unsavedForm = (
return { ...unsavedFormState, ...fieldUpdate }
}

case 'CHANGE_STEP_DETAILS': {
const fieldUpdate = handleFormChange(
action.payload.update,
unsavedFormState,
_getPipetteEntitiesRootState(rootState),
_getLabwareEntitiesRootState(rootState)
)
// @ts-expect-error (IL, 2020-02-24): address in #3161, underspecified form fields may be overwritten in type-unsafe manner
return { ...unsavedFormState, ...fieldUpdate }
}

case 'POPULATE_FORM':
return action.payload

Expand Down
Loading