Skip to content

Commit af50cda

Browse files
authored
refactor(protocol-designer): deprecate field-level form errors and refactor form errors (#18437)
closes AUTH-1300 AUTH-1301 AUTH-1320
1 parent e445014 commit af50cda

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+574
-1198
lines changed

protocol-designer/docs/STEP_FORMS_AND_PROTOCOL_TIMELINE.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ The lightweight mapping function `compoundCommandCreatorFromStepArgs` determines
3232

3333
### Timeline warnings & errors
3434

35-
Before we talked about form/field-level errors & warnings. These can be purely generated from the form values, without taking Timeline/RobotState concerns into account.
35+
Before we talked about form-level errors & warnings. These can be purely generated from the form values, without taking Timeline/RobotState concerns into account.
3636

3737
Timeline warnings and errors are for situations when information from the timeline is relevant. For example: the error when running out of tips mid-protocol, or the warning when aspirating from a well that doesn't have enough liquid.
3838

@@ -68,13 +68,11 @@ The rest of this doc will describe the PD-specific side of things: how Step Form
6868

6969
**Masking values**: Masking is a behavior where a field rejects updates when they fail to meet a certain condition -- for example, a field only intended for integers will reject changes to add a decimal point. Maskers are used in the presentational layer (specifically, in the `FieldConnector` component) where they should be applied to all updates.
7070

71-
**Field-level errors**: `steplist/fieldLevel` allows you to specify a `getErrors` function for each field. A field can have multiple "error checker" functions that can be composed together; the final result is an array of strings where each represents an error in the field. ~~(NOTE: if there are multiple field-level errors, `FieldConnector` will just join them with `', '`.)~~
72-
7371
**Form-level errors & warnings**: When an error is related to the value of more than one field, it should be specified in `steplist/formLevel` under `getErrors` for that form's `stepType`. Also, there's no such thing as field-level warnings in PD; if need a warning in a form, go to `formLevel` and specify a `getWarnings` function. Form-level errors have a `dependentFields` array associated with them, which is used to ensure that all fields have been touched (are not pristine) before showing the error.
7472

7573
## Effects of field/form errors
7674

77-
- **Blocking Form Save:** Forms with field-level or form-level errors cannot be saved. The "Save" button will be disabled.
75+
- **Blocking Form Save:** Forms with form-level errors cannot be saved. The "Save" button will be disabled.
7876

7977
- **Timeline truncation:** If a saved step form has field/form errors, it will not be passed to `{formName}ToArgs` and when the timeline is built it will stop before the error-containing step form.
8078

protocol-designer/src/assets/localization/en/protocol_steps.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"open": "open"
7070
}
7171
},
72-
"heater_shaker_settings": "Heater-Shaker Settings",
72+
"heater_shaker_state": "Heater-Shaker state",
7373
"in": "in",
7474
"into": "into",
7575
"labware_to": "{{labware}} to",

protocol-designer/src/components/molecules/ToggleExpandStepFormField/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export function ToggleExpandStepFormField(
4848
const resetFieldValue = (): void => {
4949
restProps.updateValue(null)
5050
}
51-
5251
// TODO: refactor this, it is messy
5352
const onToggleUpdateValue = (): void => {
5453
if (toggleValue === 'engage' || toggleValue === 'disengage') {

protocol-designer/src/components/organisms/Alerts/FormAlerts.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ function FormAlertsComponent(props: FormAlertsProps): JSX.Element | null {
138138

139139
const filteredFormErrorsForBanner = visibleFormErrors.reduce<WarningType[]>(
140140
(acc, error) => {
141-
return error.showAtForm ?? true
141+
return error.location === 'form'
142142
? [
143143
...acc,
144144
{

protocol-designer/src/components/organisms/Alerts/__tests__/FormAlerts.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ describe('FormAlerts', () => {
7373
type: 'TIP_POSITIONED_LOW_IN_TUBE',
7474
title: 'mockTitle',
7575
dependentFields: [],
76+
location: 'form',
7677
},
7778
])
7879
render(props)

protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/DisposalField.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ import {
1111
import { selectors as stepFormSelectors } from '../../../../../step-forms'
1212
import { getMaxDisposalVolumeForMultidispense } from '../../../../../steplist/formLevel/handleFormChange/utils'
1313
import { selectors as uiLabwareSelectors } from '../../../../../ui/labware'
14-
import { getBlowoutLocationOptionsForForm, getFormLevelError } from '../utils'
14+
import { getBlowoutLocationOptionsForForm } from '../utils'
1515
import { BlowoutOffsetField } from './BlowoutOffsetField'
1616
import { FlowRateField } from './FlowRateField'
1717

1818
import type { FormData, PathOption, StepType } from '../../../../../form-types'
19-
import type { FormError } from '../../../../../steplist/formLevel/errors'
2019
import type { FieldPropsByName } from '../types'
2120

2221
interface DisposalFieldProps {
@@ -26,7 +25,6 @@ interface DisposalFieldProps {
2625
propsForFields: FieldPropsByName
2726
stepType: StepType
2827
volume: string | null
29-
mappedErrorsToField: Record<string, FormError>
3028
aspirate_airGap_checkbox?: boolean | null
3129
aspirate_airGap_volume?: string | null
3230
tipRack?: string | null
@@ -42,7 +40,6 @@ export function DisposalField(props: DisposalFieldProps): JSX.Element {
4240
aspirate_airGap_checkbox,
4341
aspirate_airGap_volume,
4442
tipRack,
45-
mappedErrorsToField,
4643
formData,
4744
} = props
4845
const { t } = useTranslation(['application', 'form'])
@@ -95,10 +92,6 @@ export function DisposalField(props: DisposalFieldProps): JSX.Element {
9592
/>
9693
<DropdownStepFormField
9794
{...propsForFields.blowout_location}
98-
errorToShow={getFormLevelError(
99-
'blowout_location',
100-
mappedErrorsToField
101-
)}
10295
options={disposalDestinationOptions}
10396
title={t('protocol_steps:blowout_location')}
10497
padding="0"

protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ import {
4545
getCurrentFormIsPresaved,
4646
getDynamicFieldFormErrorsForUnsavedForm,
4747
getFormLevelErrorsForUnsavedForm,
48-
getLabwareEntities,
49-
getPipetteEntities,
48+
getInvariantContext,
5049
getSavedStepForms,
5150
} from '../../../../step-forms/selectors'
51+
import { actions } from '../../../../steplist'
52+
import { maskField } from '../../../../steplist/fieldLevel'
5253
import { updateFieldsForLiquidClass } from '../../../../steplist/formLevel/handleFormChange/utils'
5354
import { getTimelineWarningsForSelectedStep } from '../../../../top-selectors/timelineWarnings'
5455
import {
@@ -74,20 +75,20 @@ import {
7475
getSaveStepSnackbarText,
7576
getVisibleFormErrors,
7677
getVisibleFormWarnings,
78+
makeSingleEditFieldProps,
7779
} from './utils'
7880

7981
import type { ComponentType } from 'react'
8082
import type { RobotType } from '@opentrons/shared-data'
8183
import type { AnalyticsEvent } from '../../../../analytics/mixpanel'
82-
import type { FormData, StepType } from '../../../../form-types'
84+
import type {
85+
FormData,
86+
HydratedFormData,
87+
StepType,
88+
} from '../../../../form-types'
8389
import type { FormWarningType } from '../../../../steplist'
8490
import type { StepFieldName } from '../../../../steplist/fieldLevel'
85-
import type {
86-
FieldPropsByName,
87-
FocusHandlers,
88-
LiquidHandlingTab,
89-
StepFormProps,
90-
} from './types'
91+
import type { FocusHandlers, LiquidHandlingTab, StepFormProps } from './types'
9192

9293
type StepFormMap = {
9394
[K in StepType]?: ComponentType<StepFormProps> | null
@@ -121,7 +122,7 @@ interface StepFormToolboxProps {
121122
focusHandlers: FocusHandlers
122123
focusedField: StepFieldName | null
123124
formData: FormData
124-
propsForFields: FieldPropsByName
125+
hydratedForm: HydratedFormData
125126
handleClose: () => void
126127
handleSave: () => void
127128
}
@@ -133,9 +134,9 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
133134
canSave,
134135
handleClose,
135136
handleSave,
136-
propsForFields,
137137
dirtyFields,
138138
focusedField,
139+
hydratedForm,
139140
} = props
140141
const { t, i18n } = useTranslation([
141142
'application',
@@ -149,8 +150,13 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
149150
const [showConfirmationModal, setShowConfirmationModal] = useState<boolean>(
150151
false
151152
)
152-
const pipetteEntities = useSelector(getPipetteEntities)
153-
const labwareEntities = useSelector(getLabwareEntities)
153+
154+
const handleChangeFormInput = (name: string, value: unknown): void => {
155+
const maskedValue = maskField(name, value)
156+
dispatch(actions.changeFormInput({ update: { [name]: maskedValue } }))
157+
}
158+
159+
const { pipetteEntities, labwareEntities } = useSelector(getInvariantContext)
154160
const additionalEquipmentEntities = useSelector(
155161
getAdditionalEquipmentEntities
156162
)
@@ -169,6 +175,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
169175
title: error.title,
170176
body: error.body,
171177
dependentFields: error.dependentProfileFields,
178+
location: error.location,
172179
}))
173180
const timeline = useSelector(getRobotStateTimeline)
174181
const enableLiquidClasses = useSelector(getEnableLiquidClasses)
@@ -226,8 +233,17 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
226233
...dynamicFormLevelErrorsForUnsavedForm,
227234
],
228235
page: toolboxStep,
229-
showErrors: showFormErrors,
236+
showErrors: !currentFormIsPresaved || showFormErrors,
230237
})
238+
const propsForFields = makeSingleEditFieldProps(
239+
focusHandlers,
240+
formData,
241+
handleChangeFormInput,
242+
hydratedForm,
243+
t,
244+
visibleFormErrors
245+
)
246+
231247
const [isRename, setIsRename] = useState<boolean>(false)
232248
const icon = stepIconsByType[formData.stepType]
233249

@@ -491,7 +507,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
491507
<FormAlerts
492508
focusedField={focusedField}
493509
dirtyFields={dirtyFields}
494-
showFormErrors={showFormErrors}
510+
showFormErrors={!currentFormIsPresaved || showFormErrors}
495511
page={toolboxStep}
496512
/>
497513
<ToolsComponent
@@ -500,7 +516,6 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element {
500516
propsForFields,
501517
focusHandlers,
502518
toolboxStep,
503-
visibleFormErrors,
504519
showFormErrors,
505520
focusedField,
506521
setShowFormErrors,

protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/Initialization.tsx

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,13 @@ import {
3232
ABSORBANCE_READER_MIN_WAVELENGTH_NM,
3333
} from '../../../../../../constants'
3434
import { maskToInteger } from '../../../../../../steplist/fieldLevel/processing'
35-
import { getFormErrorsMappedToField } from '../../utils'
3635

3736
import type { TFunction } from 'i18next'
3837
import type { Dispatch, SetStateAction } from 'react'
3938
import type { DropdownOption } from '@opentrons/components'
4039
import type { FormData } from '../../../../../../form-types'
4140
import type { InitializationMode } from '../../../../../../step-forms/types'
42-
import type { StepFormErrors } from '../../../../../../steplist'
4341
import type { FieldProps, FieldPropsByName } from '../../types'
44-
import type { ErrorMappedToField } from '../../utils'
4542

4643
const MAX_WAVELENGTHS = 6
4744
const CUSTOM_OPTION: DropdownOption = { name: 'Other', value: '' }
@@ -62,7 +59,6 @@ const WAVELENGTH_OPTIONS = [...DEFINED_OPTIONS, CUSTOM_OPTION]
6259
interface InitializationProps {
6360
formData: FormData
6461
propsForFields: FieldPropsByName
65-
visibleFormErrors: StepFormErrors
6662
showFormErrors: boolean
6763
}
6864

@@ -90,12 +86,10 @@ const getBadWavelengthError = (
9086
}
9187

9288
export function Initialization(props: InitializationProps): JSX.Element {
93-
const { formData, propsForFields, visibleFormErrors, showFormErrors } = props
89+
const { formData, propsForFields, showFormErrors } = props
9490
const [numWavelengths, setNumWavelengths] = useState<number>(
9591
(formData.wavelengths?.length as number) ?? 1
9692
)
97-
const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors)
98-
9993
return (
10094
<Flex
10195
gridGap={SPACING.spacing4}
@@ -112,7 +106,6 @@ export function Initialization(props: InitializationProps): JSX.Element {
112106
mode={formData.mode}
113107
numWavelengths={numWavelengths}
114108
setNumWavelengths={setNumWavelengths}
115-
mappedErrorsToField={mappedErrorsToField}
116109
showFormErrors={showFormErrors}
117110
/>
118111
</Flex>
@@ -160,7 +153,6 @@ interface InitializationEditorProps {
160153
wavelengthsProps: FieldProps
161154
numWavelengths: number
162155
setNumWavelengths: Dispatch<SetStateAction<number>>
163-
mappedErrorsToField: ErrorMappedToField
164156
showFormErrors: boolean
165157
}
166158

@@ -173,7 +165,6 @@ function IntializationEditor(props: InitializationEditorProps): JSX.Element {
173165
wavelengthsProps,
174166
numWavelengths,
175167
setNumWavelengths,
176-
mappedErrorsToField,
177168
showFormErrors,
178169
} = props
179170
const { t } = useTranslation('form')
@@ -259,7 +250,6 @@ function IntializationEditor(props: InitializationEditorProps): JSX.Element {
259250
<ReferenceWavelength
260251
formData={formData}
261252
propsForFields={propsForFields}
262-
error={mappedErrorsToField.referenceWavelength?.title ?? null}
263253
/>
264254
</Flex>
265255
</>
@@ -359,19 +349,17 @@ function WavelengthItem(props: WavelengthItemProps): JSX.Element {
359349
interface ReferenceWavelengthProps {
360350
formData: FormData
361351
propsForFields: FieldPropsByName
362-
error: string | null
363352
}
364353

365354
function ReferenceWavelength(props: ReferenceWavelengthProps): JSX.Element {
366-
const { formData, propsForFields, error } = props
355+
const { formData, propsForFields } = props
367356
const { t } = useTranslation('form')
368357
const isExpanded = formData.referenceWavelengthActive === true
369358
const referenceWavelength = formData.referenceWavelength
370359
const showCustom = !DEFINED_OPTIONS.some(
371360
({ value }) => value === referenceWavelength
372361
)
373362

374-
const [isFocused, setIsFocused] = useState<boolean>(false)
375363
const [targetProps, tooltipProps] = useHoverTooltip()
376364

377365
return (
@@ -458,13 +446,7 @@ function ReferenceWavelength(props: ReferenceWavelengthProps): JSX.Element {
458446
onClick={e => {
459447
e.stopPropagation()
460448
}}
461-
onBlur={() => {
462-
setIsFocused(false)
463-
}}
464-
onFocus={() => {
465-
setIsFocused(true)
466-
}}
467-
error={!isFocused ? error : null}
449+
error={propsForFields.referenceWavelength.errorToShow}
468450
/>
469451
) : null}
470452
</>

protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/ReadSettings.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,15 @@ import {
88
} from '@opentrons/components'
99

1010
import { InputStepFormField } from '../../../../../../components/molecules'
11-
import { getFormErrorsMappedToField } from '../../utils'
1211

13-
import type { StepFormErrors } from '../../../../../../steplist'
1412
import type { FieldPropsByName } from '../../types'
1513

1614
interface ReadSettingsProps {
1715
propsForFields: FieldPropsByName
18-
visibleFormErrors: StepFormErrors
1916
}
2017

2118
export function ReadSettings(props: ReadSettingsProps): JSX.Element {
22-
const { propsForFields, visibleFormErrors } = props
23-
24-
const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors)
19+
const { propsForFields } = props
2520

2621
const { t } = useTranslation('form')
2722
return (
@@ -43,7 +38,6 @@ export function ReadSettings(props: ReadSettingsProps): JSX.Element {
4338
padding="0"
4439
{...propsForFields.fileName}
4540
title={t('step_edit_form.field.absorbanceReader.fileName')}
46-
errorToShow={mappedErrorsToField.fileName?.title}
4741
showTooltip={false}
4842
/>
4943
</Flex>

0 commit comments

Comments
 (0)