diff --git a/apps/demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx b/apps/demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx index 1fe91210e..cb2b1ccc1 100644 --- a/apps/demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx +++ b/apps/demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx @@ -5,12 +5,20 @@ import { useMonkAppState } from '@monkvision/common'; import { Page } from '../pages'; export function VehicleTypeSelectionPage() { - const { config, vehicleType, setVehicleType } = useMonkAppState(); + const { config, vehicleType, authToken, inspectionId, setVehicleType } = useMonkAppState(); const { i18n } = useTranslation(); if (vehicleType || !config.allowVehicleTypeSelection) { return ; } - return ; + return ( + + ); } diff --git a/apps/drive-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx b/apps/drive-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx index 1fe91210e..cb2b1ccc1 100644 --- a/apps/drive-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx +++ b/apps/drive-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx @@ -5,12 +5,20 @@ import { useMonkAppState } from '@monkvision/common'; import { Page } from '../pages'; export function VehicleTypeSelectionPage() { - const { config, vehicleType, setVehicleType } = useMonkAppState(); + const { config, vehicleType, authToken, inspectionId, setVehicleType } = useMonkAppState(); const { i18n } = useTranslation(); if (vehicleType || !config.allowVehicleTypeSelection) { return ; } - return ; + return ( + + ); } diff --git a/apps/lux-demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx b/apps/lux-demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx index 1fe91210e..cb2b1ccc1 100644 --- a/apps/lux-demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx +++ b/apps/lux-demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx @@ -5,12 +5,20 @@ import { useMonkAppState } from '@monkvision/common'; import { Page } from '../pages'; export function VehicleTypeSelectionPage() { - const { config, vehicleType, setVehicleType } = useMonkAppState(); + const { config, vehicleType, authToken, inspectionId, setVehicleType } = useMonkAppState(); const { i18n } = useTranslation(); if (vehicleType || !config.allowVehicleTypeSelection) { return ; } - return ; + return ( + + ); } diff --git a/apps/renault-demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx b/apps/renault-demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx index 1fe91210e..cb2b1ccc1 100644 --- a/apps/renault-demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx +++ b/apps/renault-demo-app/src/pages/VehicleTypeSelectionPage/VehicleTypeSelectionPage.tsx @@ -5,12 +5,20 @@ import { useMonkAppState } from '@monkvision/common'; import { Page } from '../pages'; export function VehicleTypeSelectionPage() { - const { config, vehicleType, setVehicleType } = useMonkAppState(); + const { config, vehicleType, authToken, inspectionId, setVehicleType } = useMonkAppState(); const { i18n } = useTranslation(); if (vehicleType || !config.allowVehicleTypeSelection) { return ; } - return ; + return ( + + ); } diff --git a/packages/common-ui-web/README.md b/packages/common-ui-web/README.md index ea1fabb3f..7930c1c67 100644 --- a/packages/common-ui-web/README.md +++ b/packages/common-ui-web/README.md @@ -618,4 +618,6 @@ function VehicleSelectionPage() { | availableVehicleTypes | VehicleType[] | A list of available vehicle type to choose from. The order of the list will be modified to always follow the same order. | | `[SUV, CUV, SEDAN, HATCHBACK, VAN, MINIVAN, PICKUP]` | | onSelectVehicleType | (type: VehicleType) => void | Callback called when the user has selected a vehicle type. | | The center-most vehicle type in the list. | | lang | string | The language to use by the component. | | `en` | -| patchInspection | boolean | Boolean indicating if the patch vehicle inspection feature is enabled. | | `true` | +| inspectionId | string | The ID of the inspection. | | | +| apiDomain | string | The domain of the Monk API. | | | +| authToken | string | The authentication token used to communicate with the API. | | | diff --git a/packages/common-ui-web/src/components/VehicleTypeSelection/VehicleTypeSelection.tsx b/packages/common-ui-web/src/components/VehicleTypeSelection/VehicleTypeSelection.tsx index 50624d76d..4ae957e36 100644 --- a/packages/common-ui-web/src/components/VehicleTypeSelection/VehicleTypeSelection.tsx +++ b/packages/common-ui-web/src/components/VehicleTypeSelection/VehicleTypeSelection.tsx @@ -3,26 +3,39 @@ import { i18nWrap, useI18nSync, useLoadingState, - useMonkAppState, useMonkTheme, useResponsiveStyle, } from '@monkvision/common'; -import { VehicleType } from '@monkvision/types'; +import { AllOrNone, VehicleType } from '@monkvision/types'; import { RefObject, useEffect, useMemo, useRef, useState } from 'react'; -import { decodeMonkJwt, useMonkApi } from '@monkvision/network'; +import { decodeMonkJwt, MonkApiConfig, useMonkApi } from '@monkvision/network'; import { useMonitoring } from '@monkvision/monitoring'; import { useAnalytics } from '@monkvision/analytics'; import { styles } from './VehicleTypeSelection.styles'; import { i18nVehicleTypeSelection } from './i18n'; import { Button } from '../Button'; -import { getInitialSelectedVehicleType, getVehicleTypes } from './utils'; +import { + getInitialSelectedVehicleType, + getVehicleTypeFromInspection, + getVehicleTypes, +} from './utils'; import { VehicleTypeSelectionCard } from './VehicleTypeSelectionCard'; import { Spinner } from '../Spinner'; +/** + * Props used to check inspection before displaying vehicle type selection. + */ +export interface MonkApiProps extends MonkApiConfig { + /** + * The ID of the inspection. + */ + inspectionId: string; +} + /** * Props accepted by the VehicleTypeSelection component. */ -export interface VehicleTypeSelectionProps { +export type VehicleTypeSelectionProps = { /** * The initially selected vehicle type. * @@ -46,16 +59,7 @@ export interface VehicleTypeSelectionProps { * @default en */ lang?: string; - /** - * Boolean indicating if the patch vehicle inspection feature is enabled. If true, it will trigger 2 actions: - * - At the start, it will check if a vehicle type is defined. If so, it will immediately - * call the 'onSelectVehicleType' callback. - * - When the 'confirm button' is pressed by the user, it will make a request to the API to PATCH the vehicle type of the inspection. - * - * @default true - */ - patchInspection?: boolean; -} +} & AllOrNone; function scrollToSelectedVehicleType( ref: RefObject, @@ -73,126 +77,105 @@ function scrollToSelectedVehicleType( /** * A single page component that allows the user to select a vehicle type. */ -export const VehicleTypeSelection = i18nWrap( - ({ - availableVehicleTypes, - selectedVehicleType, - onSelectVehicleType, - lang, - patchInspection = true, - }: VehicleTypeSelectionProps) => { - useI18nSync(lang); - const { config, authToken, inspectionId } = useMonkAppState(); - const { updateInspectionVehicle, getInspection } = useMonkApi({ - authToken: authToken ?? '', - apiDomain: config.apiDomain, - }); - const loading = useLoadingState(true); - const { handleError, setTags, setUserId } = useMonitoring(); - const analytics = useAnalytics(); - const [initialScroll, setInitialScroll] = useState(true); - const vehicleTypes = useMemo( - () => getVehicleTypes(availableVehicleTypes), - [availableVehicleTypes], - ); - const [selected, setSelected] = useState( - getInitialSelectedVehicleType(vehicleTypes, selectedVehicleType), - ); - const { t } = useTranslation(); - const { rootStyles } = useMonkTheme(); - const sliderRef = useRef(null); - const { responsive } = useResponsiveStyle(); +export const VehicleTypeSelection = i18nWrap((props: VehicleTypeSelectionProps) => { + useI18nSync(props.lang); + const { getInspection } = useMonkApi({ + authToken: props.authToken ?? '', + apiDomain: props.apiDomain ?? '', + }); + const loading = useLoadingState(true); + const { handleError, setTags, setUserId } = useMonitoring(); + const analytics = useAnalytics(); + const [initialScroll, setInitialScroll] = useState(true); + const vehicleTypes = useMemo( + () => getVehicleTypes(props.availableVehicleTypes), + [props.availableVehicleTypes], + ); + const [selected, setSelected] = useState( + getInitialSelectedVehicleType(vehicleTypes, props.selectedVehicleType), + ); + const { t } = useTranslation(); + const { rootStyles } = useMonkTheme(); + const sliderRef = useRef(null); + const { responsive } = useResponsiveStyle(); - const onValidate = (type: VehicleType) => { - if (patchInspection && inspectionId) { - updateInspectionVehicle({ inspectionId, vehicle: { type } }); - } - onSelectVehicleType?.(type); - }; + useEffect(() => { + if (props.inspectionId) { + setTags({ inspectionId: props.inspectionId }); + analytics.setUserId(props.inspectionId); + } + const userId = props.authToken ? decodeMonkJwt(props.authToken) : undefined; + if (userId?.sub) { + setUserId(userId.sub); + analytics.setUserProperties({ authToken: userId.sub }); + } + }, [props.inspectionId, props.authToken, analytics, setTags, setUserId]); - useEffect(() => { - if (inspectionId) { - setTags({ inspectionId }); - analytics.setUserId(inspectionId); + useEffect(() => { + const fetchInspection = async () => { + if (!props.inspectionId) { + loading.onSuccess(); + return; } - const userId = authToken ? decodeMonkJwt(authToken) : undefined; - if (userId?.sub) { - setUserId(userId.sub); - analytics.setUserProperties({ authToken: userId.sub }); + loading.start(); + const fetchedInspection = await getInspection({ + id: props.inspectionId, + }); + const vehicleType = getVehicleTypeFromInspection(fetchedInspection); + if (vehicleType && props.availableVehicleTypes?.includes(vehicleType)) { + props.onSelectVehicleType?.(vehicleType); } - }, [inspectionId, authToken, analytics, setTags, setUserId]); - - useEffect(() => { - const fetchInspection = async () => { - if (patchInspection && inspectionId) { - const fetchedInspection = await getInspection({ - id: inspectionId, - }); + loading.onSuccess(); + }; - const vehicle = fetchedInspection.entities.vehicles.find( - (v) => v.inspectionId === inspectionId, - ); - const vehicleTypeFoundInInspection = Object.values(VehicleType).find( - (vehicleType) => vehicleType === vehicle?.type, - ); - if (vehicleTypeFoundInInspection) { - onSelectVehicleType?.(vehicleTypeFoundInInspection); - } - } - loading.onSuccess(); - }; + fetchInspection().catch(handleError); + }, [props.inspectionId]); - loading.start(); - fetchInspection().catch(handleError); - }, [patchInspection, inspectionId]); - - useEffect(() => { - const index = vehicleTypes.indexOf(selected); - if (index >= 0 && !loading.isLoading) { - scrollToSelectedVehicleType(sliderRef, index, !initialScroll); - setInitialScroll(false); - } - }, [vehicleTypes, selected, loading]); + useEffect(() => { + const index = vehicleTypes.indexOf(selected); + if (index >= 0 && !loading.isLoading) { + scrollToSelectedVehicleType(sliderRef, index, !initialScroll); + setInitialScroll(false); + } + }, [vehicleTypes, selected, loading]); - const loadingContainer = loading.isLoading ? styles['loadingContainer'] : {}; + const loadingContainer = loading.isLoading ? styles['loadingContainer'] : {}; - return ( -
- {loading.isLoading && } - {!loading.isLoading && !loading.error && ( - <> -
{t('header.title')}
- -
-
- {vehicleTypes.map((v) => ( - setSelected(v)} - /> - ))} -
+ return ( +
+ {loading.isLoading && } + {!loading.isLoading && !loading.error && ( + <> +
{t('header.title')}
+ +
+
+ {vehicleTypes.map((v) => ( + setSelected(v)} + /> + ))}
- - )} -
- ); - }, - i18nVehicleTypeSelection, -); +
+ + )} +
+ ); +}, i18nVehicleTypeSelection); diff --git a/packages/common-ui-web/src/components/VehicleTypeSelection/utils.ts b/packages/common-ui-web/src/components/VehicleTypeSelection/utils.ts index e54a66a9d..1d9058694 100644 --- a/packages/common-ui-web/src/components/VehicleTypeSelection/utils.ts +++ b/packages/common-ui-web/src/components/VehicleTypeSelection/utils.ts @@ -1,3 +1,5 @@ +import { GetInspectionResponse } from '@monkvision/network'; +import { sights, vehicles } from '@monkvision/sights'; import { VehicleType } from '@monkvision/types'; export const VEHICLE_TYPE_ORDER = [ @@ -40,3 +42,21 @@ export function getInitialSelectedVehicleType( } return vehicleTypes[Math.floor(vehicleTypes.length / 2)]; } + +export function getVehicleTypeFromInspection( + response: GetInspectionResponse, +): VehicleType | undefined { + const imageWithSightId = response.entities.images.find((image) => image.sightId); + if (!imageWithSightId) { + return undefined; + } + + const sight = Object.values(sights).find((s) => s.id === imageWithSightId.sightId); + if (!sight) { + return undefined; + } + + const vehicle = Object.values(vehicles).find((v) => v.id === sight.vehicle); + + return vehicle?.type; +} diff --git a/packages/common-ui-web/test/components/VehicleTypeSelection/VehicleTypeSelection.test.tsx b/packages/common-ui-web/test/components/VehicleTypeSelection/VehicleTypeSelection.test.tsx index a09c839d1..3abf334fb 100644 --- a/packages/common-ui-web/test/components/VehicleTypeSelection/VehicleTypeSelection.test.tsx +++ b/packages/common-ui-web/test/components/VehicleTypeSelection/VehicleTypeSelection.test.tsx @@ -1,24 +1,31 @@ +const { getVehicleTypes, getInitialSelectedVehicleType } = jest.requireActual( + '../../../src/components/VehicleTypeSelection/utils', +); jest.mock('../../../src/components/Button', () => ({ Button: jest.fn(() => <>), })); jest.mock('../../../src/components/VehicleTypeSelection/VehicleTypeSelectionCard', () => ({ VehicleTypeSelectionCard: jest.fn(() => <>), })); +jest.mock('@monkvision/sights', () => [{ id: 'test-sight-id' }]); +jest.mock('../../../src/components/VehicleTypeSelection/utils', () => ({ + getVehicleTypeFromInspection: jest.fn(() => 'test-vehicle-type'), + getVehicleTypes, + getInitialSelectedVehicleType, +})); import { act, render } from '@testing-library/react'; -import { VehicleType } from '@monkvision/types'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { Button, VehicleTypeSelection } from '../../../src'; import { VehicleTypeSelectionCard } from '../../../src/components/VehicleTypeSelection/VehicleTypeSelectionCard'; +import { VehicleType } from '@monkvision/types'; import { useMonkAppState } from '@monkvision/common'; import { useMonkApi } from '@monkvision/network'; const appState = { authToken: 'test-auth-token', inspectionId: 'test-inspection-id', - config: { - apiDomain: 'test-api-domain', - }, + apiDomain: 'test-api-domain', }; describe('VehicleTypeSelection component', () => { @@ -102,46 +109,26 @@ describe('VehicleTypeSelection component', () => { unmount(); }); - it('should update the vehicle in the inspection when user confirms his vehicle choice', () => { - const onSelectVehicleType = jest.fn(); - const updateInspectionVehicle = jest.fn(() => Promise.resolve()); - (useMonkAppState as jest.Mock).mockImplementation(() => appState); - (useMonkApi as jest.Mock).mockImplementation(() => ({ updateInspectionVehicle })); - const { unmount } = render(); - - const { vehicleType } = (VehicleTypeSelectionCard as jest.Mock).mock.calls.find( - (args) => args[0].isSelected, - )[0]; - expectPropsOnChildMock(Button, { - children: 'header.confirm', - }); - const { onClick } = (Button as unknown as jest.Mock).mock.calls.find( - (args) => args[0].children === 'header.confirm', - )[0]; - - onClick(); - expect(updateInspectionVehicle).toHaveBeenCalled(); - expect(updateInspectionVehicle).toHaveBeenCalledWith({ - inspectionId: appState.inspectionId, - vehicle: { type: vehicleType }, - }); - - unmount(); - }); - it('should trigger the onSelectVehicle if VehicleType is found in the fetched inspection', async () => { const onSelectVehicleType = jest.fn(); (useMonkAppState as jest.Mock).mockImplementation(() => appState); - const vehicleMock = { - entities: { vehicles: [{ inspectionId: 'test-inspection-id', type: 'van' }] }, + const imageMock = { + entities: { images: [{ sightId: 'test-sight-id' }] }, }; - const getInspection = jest.fn(() => Promise.resolve(vehicleMock)); + const availableVehicleTypesMock = ['test-vehicle-type'] as unknown as VehicleType[]; + const getInspection = jest.fn(() => Promise.resolve(imageMock)); (useMonkApi as jest.Mock).mockImplementation(() => ({ getInspection })); await act(async () => { - render(); + render( + , + ); }); expect(onSelectVehicleType).toHaveBeenCalled(); - expect(onSelectVehicleType).toHaveBeenCalledWith(vehicleMock.entities.vehicles[0].type); + expect(onSelectVehicleType).toHaveBeenCalledWith('test-vehicle-type'); }); });