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, components): revamp form errors and fix logic for rendering #16576

Merged
merged 4 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions components/src/atoms/InputField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface InputFieldProps {
leftIcon?: IconName
showDeleteIcon?: boolean
onDelete?: () => void
hasBackgroundError?: boolean
}

export const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
Expand All @@ -83,6 +84,7 @@ export const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
tooltipText,
tabIndex = 0,
showDeleteIcon = false,
hasBackgroundError = false,
...inputProps
} = props
const hasError = props.error != null
Expand All @@ -103,11 +105,13 @@ export const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(

const INPUT_FIELD = css`
display: flex;
background-color: ${COLORS.white};
background-color: ${hasBackgroundError ? COLORS.red30 : COLORS.white};
border-radius: ${BORDERS.borderRadius4};
padding: ${SPACING.spacing8};
border: 1px ${BORDERS.styleSolid}
${hasError ? COLORS.red50 : COLORS.grey50};
border: ${hasBackgroundError
? 'none'
: `1px ${BORDERS.styleSolid}
${hasError ? COLORS.red50 : COLORS.grey50}`};
font-size: ${TYPOGRAPHY.fontSizeP};
width: 100%;
height: ${size === 'small' ? '2rem' : '2.75rem'};
Expand Down Expand Up @@ -321,10 +325,7 @@ export const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
</StyledText>
) : null}
{hasError ? (
<StyledText
desktopStyle="bodyDefaultRegular"
css={ERROR_TEXT_STYLE}
>
<StyledText desktopStyle="captionRegular" css={ERROR_TEXT_STYLE}>
{props.error}
</StyledText>
) : null}
Expand All @@ -335,6 +336,7 @@ export const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
)

const StyledInput = styled.input`
background-color: transparent;
&::placeholder {
color: ${COLORS.grey40};
}
Expand Down
8 changes: 8 additions & 0 deletions components/src/molecules/DropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export interface DropdownMenuProps {
tabIndex?: number
/** optional error */
error?: string | null
/** focus handler */
onFocus?: React.FocusEventHandler<HTMLButtonElement>
/** blur handler */
onBlur?: React.FocusEventHandler<HTMLButtonElement>
}

// TODO: (smb: 4/15/22) refactor this to use html select for accessibility
Expand All @@ -79,6 +83,8 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
tooltipText,
tabIndex = 0,
error,
onFocus,
onBlur,
} = props
const [targetProps, tooltipProps] = useHoverTooltip()
const [showDropdownMenu, setShowDropdownMenu] = React.useState<boolean>(false)
Expand Down Expand Up @@ -222,6 +228,8 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
e.preventDefault()
toggleSetShowDropdownMenu()
}}
onFocus={onFocus}
onBlur={onBlur}
css={DROPDOWN_STYLE}
tabIndex={tabIndex}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export function DropdownStepFormField(
tooltipContent,
addPadding = true,
width = '17.5rem',
onFieldFocus,
onFieldBlur,
} = props
const { t } = useTranslation('tooltip')
const availableOptionId = options.find(opt => opt.value === value)
Expand All @@ -35,6 +37,8 @@ export function DropdownStepFormField(
dropdownType="neutral"
filterOptions={options}
title={title}
onBlur={onFieldBlur}
onFocus={onFieldFocus}
currentOption={
availableOptionId ?? { name: 'Choose option', value: '' }
}
Expand Down
1 change: 1 addition & 0 deletions protocol-designer/src/organisms/Alerts/FormAlerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ function FormAlertsComponent(props: FormAlertsProps): JSX.Element | null {
: undefined
}
width="100%"
iconMarginLeft={SPACING.spacing4}
>
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4}>
<StyledText desktopStyle="bodyDefaultSemiBold">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type WellSelectionFieldProps = FieldProps & {
nozzles: string | null
pipetteId?: string | null
labwareId?: string | null
hasFormError?: boolean
}

export const WellSelectionField = (
Expand All @@ -45,6 +46,7 @@ export const WellSelectionField = (
disabled,
errorToShow,
tooltipContent,
hasFormError,
} = props
const { t, i18n } = useTranslation(['form', 'tooltip'])
const dispatch = useDispatch()
Expand Down Expand Up @@ -90,7 +92,6 @@ export const WellSelectionField = (
? t(`step_edit_form.wellSelectionLabel.columns_${name}`)
: t(`step_edit_form.wellSelectionLabel.wells_${name}`)
const [targetProps, tooltipProps] = useHoverTooltip()

return (
<>
<Flex flexDirection={DIRECTION_COLUMN} padding={SPACING.spacing16}>
Expand All @@ -116,6 +117,7 @@ export const WellSelectionField = (
error={errorToShow}
value={primaryWellCount}
onClick={handleOpen}
hasBackgroundError={hasFormError}
/>
</Flex>
{createPortal(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ import {
TemperatureTools,
ThermocyclerTools,
} from './StepTools'
import { getSaveStepSnackbarText } from './utils'
import {
getSaveStepSnackbarText,
getVisibleFormErrors,
getVisibleFormWarnings,
} from './utils'
import type { StepFieldName } from '../../../../steplist/fieldLevel'
import type { FormData, StepType } from '../../../../form-types'
import type { FieldPropsByName, FocusHandlers, StepFormProps } from './types'
import { getFormLevelErrorsForUnsavedForm } from '../../../../step-forms/selectors'

type StepFormMap = {
[K in StepType]?: React.ComponentType<StepFormProps> | null
Expand Down Expand Up @@ -91,6 +96,9 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
const timelineWarningsForSelectedStep = useSelector(
getTimelineWarningsForSelectedStep
)
const formLevelErrorsForUnsavedForm = useSelector(
getFormLevelErrorsForUnsavedForm
)
const timeline = useSelector(getRobotStateTimeline)
const [toolboxStep, setToolboxStep] = useState<number>(
// progress to step 2 if thermocycler form is populated
Expand All @@ -103,6 +111,16 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
showFormErrorsAndWarnings,
setShowFormErrorsAndWarnings,
] = useState<boolean>(false)
const visibleFormWarnings = getVisibleFormWarnings({
focusedField,
dirtyFields: dirtyFields ?? [],
errors: formWarningsForSelectedStep,
})
const visibleFormErrors = getVisibleFormErrors({
focusedField,
dirtyFields: dirtyFields ?? [],
errors: formLevelErrorsForUnsavedForm,
})
const [isRename, setIsRename] = useState<boolean>(false)
const icon = stepIconsByType[formData.stepType]

Expand All @@ -126,7 +144,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
formData.stepType === 'mix' ||
formData.stepType === 'thermocycler'
const numWarnings =
formWarningsForSelectedStep.length + timelineWarningsForSelectedStep.length
visibleFormWarnings.length + timelineWarningsForSelectedStep.length
const numErrors = timeline.errors?.length ?? 0

const handleSaveClick = (): void => {
Expand Down Expand Up @@ -229,6 +247,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
propsForFields,
focusHandlers,
toolboxStep,
visibleFormErrors,
}}
/>
</Toolbox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
import type { StepFormProps } from '../../types'

export function MixTools(props: StepFormProps): JSX.Element {
const { propsForFields, formData, toolboxStep } = props
const { propsForFields, formData, toolboxStep, visibleFormErrors } = props
const pipettes = useSelector(getPipetteEntities)
const enableReturnTip = useSelector(getEnableReturnTip)
const labwares = useSelector(getLabwareEntities)
Expand Down Expand Up @@ -89,6 +89,11 @@ export function MixTools(props: StepFormProps): JSX.Element {
labwareId={formData.labware}
pipetteId={formData.pipette}
nozzles={String(propsForFields.nozzles.value) ?? null}
hasFormError={
visibleFormErrors?.some(error =>
error.dependentFields.includes('labware')
) ?? false
}
/>
<Divider marginY="0" />
<VolumeField {...propsForFields.volume} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const makeAddFieldNamePrefix = (prefix: string) => (
): StepFieldName => `${prefix}_${fieldName}`

export function MoveLiquidTools(props: StepFormProps): JSX.Element {
const { toolboxStep, propsForFields, formData } = props
const { toolboxStep, propsForFields, formData, visibleFormErrors } = props
const { t, i18n } = useTranslation(['protocol_steps', 'form'])
const { path } = formData
const [tab, setTab] = useState<'aspirate' | 'dispense'>('aspirate')
Expand Down Expand Up @@ -126,6 +126,11 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element {
labwareId={String(propsForFields.aspirate_labware.value)}
pipetteId={formData.pipette}
nozzles={String(propsForFields.nozzles.value) ?? null}
hasFormError={
visibleFormErrors?.some(error =>
error.dependentFields.includes('aspirate_labware')
) ?? false
}
/>
<Divider marginY="0" />
<LabwareField {...propsForFields.dispense_labware} />
Expand All @@ -136,6 +141,11 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element {
labwareId={String(propsForFields.dispense_labware.value)}
pipetteId={formData.pipette}
nozzles={String(propsForFields.nozzles.value) ?? null}
hasFormError={
visibleFormErrors?.some(error =>
error.dependentFields.includes('dispense_wells')
) ?? false
}
/>
)}
<Divider marginY="0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('MagnetTools', () => {
dirtyFields: [],
focusedField: null,
},
visibleFormErrors: [],
toolboxStep: 1,
propsForFields: {
magnetAction: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('TemperatureTools', () => {
dirtyFields: [],
focusedField: null,
},
visibleFormErrors: [],
toolboxStep: 1,
propsForFields: {
moduleId: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,12 @@ function StepFormManager(props: StepFormManagerProps): JSX.Element | null {
if (fieldName === focusedField) {
setFocusedField(null)
}
if (!dirtyFields.includes(fieldName)) {
setDirtyFields([...dirtyFields, fieldName])
}
setDirtyFields(prevDirtyFields => {
if (!prevDirtyFields.includes(fieldName)) {
return [...prevDirtyFields, fieldName]
}
return prevDirtyFields
})
}
const stepId = formData?.id
const handleDelete = (): void => {
Expand Down Expand Up @@ -144,7 +147,6 @@ function StepFormManager(props: StepFormManagerProps): JSX.Element | null {
) {
handleSave = confirmAddPauseUntilHeaterShakerTempStep
}

return (
<>
{/* TODO: update these modals to match new modal design */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FormData, StepFieldName } from '../../../../form-types'
import type { StepFormErrors } from '../../../../steplist'
export interface FocusHandlers {
focusedField: StepFieldName | null
dirtyFields: StepFieldName[]
Expand All @@ -24,4 +25,5 @@ export interface StepFormProps {
focusHandlers: FocusHandlers
propsForFields: FieldPropsByName
toolboxStep: number
visibleFormErrors: StepFormErrors
}
6 changes: 3 additions & 3 deletions protocol-designer/src/steplist/formLevel/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ export interface FormError {
dependentFields: StepFieldName[]
}
const INCOMPATIBLE_ASPIRATE_LABWARE: FormError = {
title: 'Selected aspirate labware is incompatible with selected pipette',
title: 'Selected aspirate labware is incompatible with pipette',
dependentFields: ['aspirate_labware', 'pipette'],
}
const INCOMPATIBLE_DISPENSE_LABWARE: FormError = {
title: 'Selected dispense labware is incompatible with selected pipette',
title: 'Selected dispense labware is incompatible with pipette',
dependentFields: ['dispense_labware', 'pipette'],
}
const INCOMPATIBLE_LABWARE: FormError = {
title: 'Selected labware is incompatible with selected pipette',
title: 'Selected labware is incompatible with pipette',
dependentFields: ['labware', 'pipette'],
}
const PAUSE_TYPE_REQUIRED: FormError = {
Expand Down
Loading