diff --git a/api-client/src/pipettes/index.ts b/api-client/src/pipettes/index.ts index cc19fe7ed15..f2fa52365fc 100644 --- a/api-client/src/pipettes/index.ts +++ b/api-client/src/pipettes/index.ts @@ -1,5 +1,6 @@ export { getPipettes } from './getPipettes' export { getPipetteSettings } from './getPipetteSettings' +export { updatePipetteSettings } from './updatePipetteSettings' export * from './types' export * from './__fixtures__' diff --git a/api-client/src/pipettes/types.ts b/api-client/src/pipettes/types.ts index dff905cca82..c637b64d967 100644 --- a/api-client/src/pipettes/types.ts +++ b/api-client/src/pipettes/types.ts @@ -51,8 +51,8 @@ export interface FetchPipettesResponseBody { right: FetchPipettesResponsePipette } -interface PipetteSettingsField { - value: number | null | undefined +export interface PipetteSettingsField { + value: number | null | boolean | undefined default: number min?: number max?: number @@ -66,7 +66,7 @@ interface PipetteQuirksField { interface QuirksField { quirks?: PipetteQuirksField } -type PipetteSettingsFieldsMap = QuirksField & { +export type PipetteSettingsFieldsMap = QuirksField & { [fieldId: string]: PipetteSettingsField } export interface IndividualPipetteSettings { @@ -77,3 +77,15 @@ export interface IndividualPipetteSettings { type PipetteSettingsById = Partial<{ [id: string]: IndividualPipetteSettings }> export type PipetteSettings = PipetteSettingsById + +export interface PipetteSettingsUpdateFieldsMap { + [fieldId: string]: PipetteSettingsUpdateField +} + +export type PipetteSettingsUpdateField = { + value: PipetteSettingsField['value'] +} | null + +export interface UpdatePipetteSettingsData { + fields: { [fieldId: string]: PipetteSettingsUpdateField } +} diff --git a/api-client/src/pipettes/updatePipetteSettings.ts b/api-client/src/pipettes/updatePipetteSettings.ts new file mode 100644 index 00000000000..7ed76178914 --- /dev/null +++ b/api-client/src/pipettes/updatePipetteSettings.ts @@ -0,0 +1,21 @@ +import { PATCH, request } from '../request' + +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { + IndividualPipetteSettings, + UpdatePipetteSettingsData, +} from './types' + +export function updatePipetteSettings( + config: HostConfig, + pipetteId: string, + data: UpdatePipetteSettingsData +): ResponsePromise { + return request( + PATCH, + `/settings/pipettes/${pipetteId}`, + data, + config + ) +} diff --git a/app/src/organisms/ConfigurePipette/ConfigForm.tsx b/app/src/organisms/ConfigurePipette/ConfigForm.tsx index 5c640e6957b..919d4224f7a 100644 --- a/app/src/organisms/ConfigurePipette/ConfigForm.tsx +++ b/app/src/organisms/ConfigurePipette/ConfigForm.tsx @@ -16,13 +16,12 @@ import { } from './ConfigFormGroup' import type { FormikProps } from 'formik' +import type { FormValues } from './ConfigFormGroup' import type { PipetteSettingsField, PipetteSettingsFieldsMap, - PipetteSettingsFieldsUpdate, -} from '../../redux/pipettes/types' - -import type { FormValues } from './ConfigFormGroup' + UpdatePipetteSettingsData, +} from '@opentrons/api-client' export interface DisplayFieldProps extends PipetteSettingsField { name: string @@ -38,7 +37,7 @@ export interface DisplayQuirkFieldProps { export interface ConfigFormProps { settings: PipetteSettingsFieldsMap updateInProgress: boolean - updateSettings: (fields: PipetteSettingsFieldsUpdate) => unknown + updateSettings: (params: UpdatePipetteSettingsData) => void groupLabels: string[] formId: string } @@ -96,14 +95,16 @@ export class ConfigForm extends React.Component { } handleSubmit: (values: FormValues) => void = values => { - const params = mapValues(values, v => { - if (v === true || v === false) return v + const fields = mapValues< + FormValues, + { value: PipetteSettingsField['value'] } | null + >(values, v => { + if (v === true || v === false) return { value: v } if (v === '' || v == null) return null - return Number(v) + return { value: Number(v) } }) - // @ts-expect-error TODO updateSettings type doesn't include boolean for values of params, but they could be returned. - this.props.updateSettings(params) + this.props.updateSettings({ fields }) } getFieldValue( @@ -161,7 +162,6 @@ export class ConfigForm extends React.Component { PipetteSettingsFieldsMap, string | boolean >(fields, f => { - // @ts-expect-error TODO: PipetteSettingsFieldsMap doesn't include a boolean value, despite checking for it here if (f.value === true || f.value === false) return f.value // @ts-expect-error(sa, 2021-05-27): avoiding src code change, use optional chain to access f.value return f.value !== f.default ? f.value.toString() : '' diff --git a/app/src/organisms/ConfigurePipette/ConfigFormGroup.tsx b/app/src/organisms/ConfigurePipette/ConfigFormGroup.tsx index 227577474dd..919e4660d5d 100644 --- a/app/src/organisms/ConfigurePipette/ConfigFormGroup.tsx +++ b/app/src/organisms/ConfigurePipette/ConfigFormGroup.tsx @@ -96,7 +96,7 @@ export function ConfigInput(props: ConfigInputProps): JSX.Element { const { field } = props const { name, units, displayName } = field const id = makeId(field.name) - const _default = field.default.toString() + const _default = field.default?.toString() return ( diff --git a/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx b/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx index ff7a7abe3e6..a2d9aca36e7 100644 --- a/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx +++ b/app/src/organisms/ConfigurePipette/__tests__/ConfigurePipette.test.tsx @@ -35,9 +35,10 @@ describe('ConfigurePipette', () => { beforeEach(() => { props = { + isUpdateLoading: false, + updateError: null, settings: mockPipetteSettingsFieldsMap, robotName: mockRobotName, - updateRequest: { status: 'pending' }, updateSettings: jest.fn(), closeModal: jest.fn(), formId: 'id', diff --git a/app/src/organisms/ConfigurePipette/index.tsx b/app/src/organisms/ConfigurePipette/index.tsx index 1353854ff54..326f9e5792e 100644 --- a/app/src/organisms/ConfigurePipette/index.tsx +++ b/app/src/organisms/ConfigurePipette/index.tsx @@ -2,27 +2,31 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { Box } from '@opentrons/components' -import { SUCCESS, FAILURE, PENDING } from '../../redux/robot-api' import { ConfigForm } from './ConfigForm' import { ConfigErrorBanner } from './ConfigErrorBanner' - import type { - PipetteSettingsFieldsUpdate, PipetteSettingsFieldsMap, -} from '../../redux/pipettes/types' -import type { RequestState } from '../../redux/robot-api/types' + UpdatePipetteSettingsData, +} from '@opentrons/api-client' interface Props { closeModal: () => void - updateRequest: RequestState | null - updateSettings: (fields: PipetteSettingsFieldsUpdate) => void + updateSettings: (params: UpdatePipetteSettingsData) => void + updateError: Error | null + isUpdateLoading: boolean robotName: string formId: string settings: PipetteSettingsFieldsMap } export function ConfigurePipette(props: Props): JSX.Element { - const { closeModal, updateRequest, updateSettings, formId, settings } = props + const { + updateSettings, + updateError, + isUpdateLoading, + formId, + settings, + } = props const { t } = useTranslation('device_details') const groupLabels = [ @@ -31,25 +35,14 @@ export function ConfigurePipette(props: Props): JSX.Element { t('power_force'), ] - const updateError: string | null = - updateRequest && updateRequest.status === FAILURE - ? // @ts-expect-error(sa, 2021-05-27): avoiding src code change, need to type narrow - updateRequest.error.message || t('an_error_occurred_while_updating') - : null - - // when an in-progress request completes, close modal if response was ok - React.useEffect(() => { - if (updateRequest?.status === SUCCESS) { - closeModal() - } - }, [updateRequest, closeModal]) - return ( - {updateError && } + {updateError != null && ( + + )} { - dispatchRequest(updatePipetteSettings(robotName, pipetteId, fields)) - } - const latestRequestId = last(requestIds) - const updateRequest = useSelector((state: State) => - latestRequestId != null ? getRequestById(state, latestRequestId) : null - ) + const { + updatePipetteSettings, + isLoading, + error, + } = useUpdatePipetteSettingsMutation(pipetteId, { onSuccess: onCloseClick }) + const FORM_ID = `configurePipetteForm_${pipetteId}` return ( @@ -56,18 +44,14 @@ export const PipetteSettingsSlideout = ( title={t('pipette_settings', { pipetteName: pipetteName })} onCloseClick={onCloseClick} isExpanded={isExpanded} - footer={ - - } + footer={} > -const mockUseDispatchApiRequest = RobotApi.useDispatchApiRequest as jest.MockedFunction< - typeof RobotApi.useDispatchApiRequest -> -const mockGetRequestById = RobotApi.getRequestById as jest.MockedFunction< - typeof RobotApi.getRequestById -> -const mockUpdatePipetteSettings = updatePipetteSettings as jest.MockedFunction< - typeof updatePipetteSettings +const mockUseHost = useHost as jest.MockedFunction +const mockUseUpdatePipetteSettingsMutation = useUpdatePipetteSettingsMutation as jest.MockedFunction< + typeof useUpdatePipetteSettingsMutation > const render = ( @@ -43,9 +33,8 @@ const render = ( const mockRobotName = 'mockRobotName' describe('PipetteSettingsSlideout', () => { - let dispatchApiRequest: DispatchApiRequestType - let props: React.ComponentProps + let mockUpdatePipetteSettings: jest.Mock beforeEach(() => { props = { @@ -56,20 +45,19 @@ describe('PipetteSettingsSlideout', () => { isExpanded: true, onCloseClick: jest.fn(), } - mockGetRequestById.mockReturnValue({ - status: RobotApi.SUCCESS, - response: { - method: 'POST', - ok: true, - path: '/', - status: 200, - }, - }) - mockGetConfig.mockReturnValue({} as any) - dispatchApiRequest = jest.fn() - when(mockUseDispatchApiRequest) + when(mockUseHost) .calledWith() - .mockReturnValue([dispatchApiRequest, ['id']]) + .mockReturnValue({} as any) + + mockUpdatePipetteSettings = jest.fn() + + when(mockUseUpdatePipetteSettingsMutation) + .calledWith(props.pipetteId, expect.anything()) + .mockReturnValue({ + updatePipetteSettings: mockUpdatePipetteSettings, + isLoading: false, + error: null, + } as any) }) afterEach(() => { jest.resetAllMocks() @@ -96,30 +84,20 @@ describe('PipetteSettingsSlideout', () => { const { getByRole } = render(props) const button = getByRole('button', { name: 'Confirm' }) - when(mockUpdatePipetteSettings) - .calledWith( - mockRobotName, - props.pipetteId, - expect.objectContaining({ - blowout: 2, - bottom: 3, - dropTip: 1, + fireEvent.click(button) + await waitFor(() => { + expect(mockUpdatePipetteSettings).toHaveBeenCalledWith({ + fields: expect.objectContaining({ + blowout: { value: 2 }, + bottom: { value: 3 }, + dropTip: { value: 1 }, dropTipCurrent: null, dropTipSpeed: null, pickUpCurrent: null, pickUpDistance: null, plungerCurrent: null, - top: 4, - }) - ) - .mockReturnValue({ - type: 'pipettes:UPDATE_PIPETTE_SETTINGS', - } as UpdatePipetteSettingsAction) - - fireEvent.click(button) - await waitFor(() => { - expect(dispatchApiRequest).toHaveBeenCalledWith({ - type: 'pipettes:UPDATE_PIPETTE_SETTINGS', + top: { value: 4 }, + }), }) }) }) diff --git a/react-api-client/src/pipettes/index.ts b/react-api-client/src/pipettes/index.ts index 47023466235..b162ecee8e2 100644 --- a/react-api-client/src/pipettes/index.ts +++ b/react-api-client/src/pipettes/index.ts @@ -1,2 +1,3 @@ export { usePipettesQuery } from './usePipettesQuery' export { usePipetteSettingsQuery } from './usePipetteSettingsQuery' +export { useUpdatePipetteSettingsMutation } from './useUpdatePipetteSettingsMutation' diff --git a/react-api-client/src/pipettes/usePipetteSettingsQuery.ts b/react-api-client/src/pipettes/usePipetteSettingsQuery.ts index 8817db21641..d58818e1d21 100644 --- a/react-api-client/src/pipettes/usePipetteSettingsQuery.ts +++ b/react-api-client/src/pipettes/usePipetteSettingsQuery.ts @@ -9,7 +9,7 @@ export function usePipetteSettingsQuery( ): UseQueryResult { const host = useHost() const query = useQuery( - [host, 'pipettesSettings'], + [host, 'pipettes', 'settings'], () => getPipetteSettings(host as HostConfig).then(response => response.data), { enabled: host !== null, ...options } diff --git a/react-api-client/src/pipettes/useUpdatePipetteSettingsMutation.ts b/react-api-client/src/pipettes/useUpdatePipetteSettingsMutation.ts new file mode 100644 index 00000000000..f306a8b9202 --- /dev/null +++ b/react-api-client/src/pipettes/useUpdatePipetteSettingsMutation.ts @@ -0,0 +1,73 @@ +import { + HostConfig, + IndividualPipetteSettings, + updatePipetteSettings, + UpdatePipetteSettingsData, +} from '@opentrons/api-client' +import { + useMutation, + useQueryClient, + UseMutateAsyncFunction, + UseMutationOptions, + UseMutationResult, +} from 'react-query' +import { useHost } from '../api' +import type { AxiosError } from 'axios' + +export type UpdatePipetteSettingsType = UseMutateAsyncFunction< + IndividualPipetteSettings, + AxiosError, + UpdatePipetteSettingsData +> + +export type UseUpdatePipetteSettingsMutationResult = UseMutationResult< + IndividualPipetteSettings, + AxiosError, + UpdatePipetteSettingsData +> & { + updatePipetteSettings: UpdatePipetteSettingsType +} + +export type UseUpdatePipetteSettingsOptions = UseMutationOptions< + IndividualPipetteSettings, + AxiosError, + UpdatePipetteSettingsData +> + +export function useUpdatePipetteSettingsMutation( + pipetteId: string, + options: UseUpdatePipetteSettingsOptions = {}, + hostOverride?: HostConfig | null +): UseUpdatePipetteSettingsMutationResult { + const contextHost = useHost() + const queryClient = useQueryClient() + const host = + hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost + const mutation = useMutation< + IndividualPipetteSettings, + AxiosError, + UpdatePipetteSettingsData + >( + [host, 'pipettes', 'settings'], + ({ fields }) => + updatePipetteSettings(host as HostConfig, pipetteId, { fields }) + .then(response => { + queryClient + .invalidateQueries([host, 'pipettes', 'settings']) + .catch((e: Error) => + console.error( + `error invalidating pipette settings query: ${e.message}` + ) + ) + return response.data + }) + .catch(e => { + throw e + }), + options + ) + return { + ...mutation, + updatePipetteSettings: mutation.mutateAsync, + } +}