diff --git a/apps/demo-app/src/local-config.json b/apps/demo-app/src/local-config.json index fec061484..87da6eeaa 100644 --- a/apps/demo-app/src/local-config.json +++ b/apps/demo-app/src/local-config.json @@ -2,7 +2,7 @@ "id": "demo-app-dev", "description": "Config for the dev Demo App.", "allowSkipRetake": true, - "enableAddDamage": true, + "addDamage": "part_select", "enableSightGuidelines": true, "allowVehicleTypeSelection": true, "allowManualLogin": true, diff --git a/apps/demo-app/test/pages/PhotoCapturePage.test.tsx b/apps/demo-app/test/pages/PhotoCapturePage.test.tsx index 187a62d64..53e150d99 100644 --- a/apps/demo-app/test/pages/PhotoCapturePage.test.tsx +++ b/apps/demo-app/test/pages/PhotoCapturePage.test.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next'; import { render } from '@testing-library/react'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; -import { PhotoCapture } from '@monkvision/inspection-capture-web'; +import { PhotoCapture, PhotoCaptureProps } from '@monkvision/inspection-capture-web'; import { useMonkAppState } from '@monkvision/common'; import { PhotoCapturePage } from '../../src/pages'; @@ -20,7 +20,7 @@ const appState = { enforceOrientation: 'test-enforceOrientation-test', maxUploadDurationWarning: 'test-maxUploadDurationWarning-test', allowSkipRetake: 'test-allowSkipRetake-test', - enableAddDamage: 'test-enableAddDamage-test', + addDamage: 'test-addDamage-test', enableCompliance: 'test-enableCompliance-test', enableCompliancePerSight: 'test-enableCompliancePerSight-test', complianceIssues: 'test-complianceIssues-test', @@ -53,7 +53,7 @@ describe('PhotoCapture page', () => { enforceOrientation: appState.config.enforceOrientation, maxUploadDurationWarning: appState.config.maxUploadDurationWarning, allowSkipRetake: appState.config.allowSkipRetake, - enableAddDamage: appState.config.enableAddDamage, + addDamage: appState.config.addDamage, enableCompliance: appState.config.enableCompliance, enableCompliancePerSight: appState.config.enableCompliancePerSight, complianceIssues: appState.config.complianceIssues, @@ -62,7 +62,7 @@ describe('PhotoCapture page', () => { customComplianceThresholds: appState.config.customComplianceThresholds, customComplianceThresholdsPerSight: appState.config.customComplianceThresholdsPerSight, additionalTasks: appState.config.additionalTasks, - }); + } as unknown as PhotoCaptureProps); unmount(); }); @@ -74,7 +74,7 @@ describe('PhotoCapture page', () => { expect(appState.getCurrentSights).toHaveBeenCalled(); expectPropsOnChildMock(PhotoCapture, { sights: appState.getCurrentSights(), - }); + } as PhotoCaptureProps); unmount(); }); diff --git a/configs/test-utils/src/__mocks__/@monkvision/common-ui-web.tsx b/configs/test-utils/src/__mocks__/@monkvision/common-ui-web.tsx index 23c141138..9e78b7fc4 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/common-ui-web.tsx +++ b/configs/test-utils/src/__mocks__/@monkvision/common-ui-web.tsx @@ -18,4 +18,5 @@ export = { SVGElement: jest.fn(() => <>), SwitchButton: jest.fn(() => <>), TakePictureButton: jest.fn(() => <>), + VehiclePartSelection: jest.fn(() => <>), }; diff --git a/configs/test-utils/src/__mocks__/@monkvision/common.tsx b/configs/test-utils/src/__mocks__/@monkvision/common.tsx index 520a1d654..65cfadc08 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/common.tsx +++ b/configs/test-utils/src/__mocks__/@monkvision/common.tsx @@ -1,5 +1,7 @@ import { InteractiveStatus } from '@monkvision/types'; +const { vehiclePartLabels } = jest.requireActual('@monkvision/common'); + function createMockLoadingState() { return { isLoading: false, @@ -59,6 +61,7 @@ export = { complianceIssueLabels, imageStatusLabels, getInspectionImages, + vehiclePartLabels, /* Mocks */ useMonkTheme: jest.fn(() => createTheme()), diff --git a/configs/test-utils/src/expects/props.tsx b/configs/test-utils/src/expects/props.tsx index 100579f6c..0de9afe62 100644 --- a/configs/test-utils/src/expects/props.tsx +++ b/configs/test-utils/src/expects/props.tsx @@ -17,9 +17,9 @@ import { * If you want to test the children passed to the child component, you can add the `children` property to the props * object. */ -export function expectPropsOnChildMock( - Component: jest.Mock | FC | ForwardedRef, - props: { [key: string]: unknown }, +export function expectPropsOnChildMock | ForwardedRef>( + Component: T, + props: T extends (prop: infer P) => any ? Partial

: never, ): void { expect(Component).toHaveBeenCalledWith(expect.objectContaining(props), expect.anything()); } diff --git a/documentation/docs/photo-capture-workflow.md b/documentation/docs/photo-capture-workflow.md index 5490e6453..682be0f81 100644 --- a/documentation/docs/photo-capture-workflow.md +++ b/documentation/docs/photo-capture-workflow.md @@ -58,7 +58,7 @@ increase the detection rate. This feature is called `Add Damage`, and there two take a close-up picture of the damage. For now, only the 2-shot workflow is implemented in the PhotoCapture workflow. This feature is enabled by default in the -`PhotoCapture` component. To disable it, pass the `enableAddDamage` prop to `false`. +`PhotoCapture` component. To disable it, pass the `addDamage` prop to `AddDamage.DISABLED`. ## Using Compliance The compliance is a feature that allows our AI models to analyze the quality of the pictures taken by the user, and if diff --git a/documentation/src/utils/schemas.ts b/documentation/src/utils/schemas.ts index b64eda6f6..37c696c17 100644 --- a/documentation/src/utils/schemas.ts +++ b/documentation/src/utils/schemas.ts @@ -9,6 +9,7 @@ import { SteeringWheelPosition, TaskName, VehicleType, + AddDamage, } from '@monkvision/types'; import { sights } from '@monkvision/sights'; import { flatten } from '@monkvision/common'; @@ -270,7 +271,7 @@ export const LiveConfigSchema = z maxUploadDurationWarning: z.number().positive().or(z.literal(-1)).optional(), useAdaptiveImageQuality: z.boolean().optional(), allowSkipRetake: z.boolean().optional(), - enableAddDamage: z.boolean().optional(), + addDamage: z.nativeEnum(AddDamage).optional(), enableSightGuidelines: z.boolean().optional(), sightGuidelines: z.array(SightGuidelineSchema).optional(), enableTutorial: z.nativeEnum(PhotoCaptureTutorialOption).optional(), diff --git a/packages/common-ui-web/src/components/InspectionGallery/hooks/useInspectionGalleryItems.ts b/packages/common-ui-web/src/components/InspectionGallery/hooks/useInspectionGalleryItems.ts index 576b3c14c..d29107ee2 100644 --- a/packages/common-ui-web/src/components/InspectionGallery/hooks/useInspectionGalleryItems.ts +++ b/packages/common-ui-web/src/components/InspectionGallery/hooks/useInspectionGalleryItems.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { ImageStatus, Sight } from '@monkvision/types'; +import { AddDamage, ImageStatus, Sight } from '@monkvision/types'; import { getInspectionImages, MonkState, useMonkState } from '@monkvision/common'; import { useInspectionPoll } from '@monkvision/network'; import { InspectionGalleryItem, InspectionGalleryProps } from '../types'; @@ -57,7 +57,7 @@ function getItems( captureMode: boolean, entities: MonkState, inspectionSights?: Sight[], - enableAddDamage?: boolean, + addDamage?: AddDamage, ): InspectionGalleryItem[] { const images = getInspectionImages(inspectionId, entities.images, captureMode); const items: InspectionGalleryItem[] = images.map((image) => ({ @@ -73,7 +73,7 @@ function getItems( items.push({ isTaken: false, isAddDamage: false, sightId: sight.id }); } }); - if (captureMode && enableAddDamage !== false) { + if (captureMode && (!addDamage || addDamage !== AddDamage.DISABLED)) { items.push({ isAddDamage: true }); } return items.sort((a, b) => compareGalleryItems(a, b, captureMode, inspectionSights)); @@ -93,15 +93,8 @@ export function useInspectionGalleryItems(props: InspectionGalleryProps): Inspec const { state } = useMonkState(); const items = useMemo( - () => - getItems( - props.inspectionId, - props.captureMode, - state, - inspectionSights, - props.enableAddDamage, - ), - [props.inspectionId, props.captureMode, state, inspectionSights, props.enableAddDamage], + () => getItems(props.inspectionId, props.captureMode, state, inspectionSights, props.addDamage), + [props.inspectionId, props.captureMode, state, inspectionSights, props.addDamage], ); const shouldFetch = useMemo(() => shouldContinueToFetch(items), items); diff --git a/packages/common-ui-web/src/components/InspectionGallery/types.ts b/packages/common-ui-web/src/components/InspectionGallery/types.ts index 01fbe9410..122ca2de6 100644 --- a/packages/common-ui-web/src/components/InspectionGallery/types.ts +++ b/packages/common-ui-web/src/components/InspectionGallery/types.ts @@ -1,4 +1,4 @@ -import { ComplianceOptions, Image, Sight } from '@monkvision/types'; +import { AddDamage, ComplianceOptions, Image, Sight } from '@monkvision/types'; import { MonkApiConfig } from '@monkvision/network'; /** @@ -105,7 +105,7 @@ export type InspectionGalleryProps = { * * @default true */ - enableAddDamage?: boolean; + addDamage: AddDamage; /** * Custom label for validate button. */ diff --git a/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.tsx b/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.tsx index 74a92ab6f..3d60d7d0a 100644 --- a/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.tsx +++ b/packages/common-ui-web/src/components/VehicleDynamicWireframe/VehicleDynamicWireframe.tsx @@ -1,4 +1,3 @@ -import { useMonkTheme } from '@monkvision/common'; import { PartSelectionOrientation, VehicleModel, @@ -68,6 +67,10 @@ function createGetAttributesCallback( } function getVehicleModel(vehicleType: VehicleType): VehicleModel { + if (vehicleType === VehicleType.SUV) { + // eslint-disable-next-line no-param-reassign + vehicleType = VehicleType.CUV; + } const detail = Object.entries(vehicles) .filter(([type]) => type !== VehicleModel.AUDIA7) .find(([, details]) => details.type === vehicleType)?.[1]; @@ -91,14 +94,13 @@ export function VehicleDynamicWireframe({ throw new Error(`No wireframe found for vehicle type ${vehicleType}`); } const overlay = wireframes[orientation]; - const { utils } = useMonkTheme(); return ( diff --git a/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.style.ts b/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.style.ts index e00d5c53f..b7898f657 100644 --- a/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.style.ts +++ b/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.style.ts @@ -2,11 +2,10 @@ import { Styles } from '@monkvision/types'; export const styles: Styles = { wrapper: { - position: 'absolute', display: 'flex', - justifyContent: 'center', alignItems: 'center', - inset: '0 0 0 0', gap: '30px', + flexGrow: 1, + maxHeight: '100%', }, }; diff --git a/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.tsx b/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.tsx index 50dadec22..535376d06 100644 --- a/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.tsx +++ b/packages/common-ui-web/src/components/VehiclePartSelection/VehiclePartSelection.tsx @@ -65,9 +65,8 @@ export function VehiclePartSelection({ part: VehiclePart, ) => ({ style: { - // TODO: need to finalize the color for the selected parts. - fill: selectedParts.includes(part) ? palette.primary.xlight : undefined, - stroke: palette.primary.light, + fill: selectedParts.includes(part) ? palette.primary.base : undefined, + stroke: palette.text.primary, display: 'block', }, }); diff --git a/packages/common-ui-web/src/icons/assets.ts b/packages/common-ui-web/src/icons/assets.ts index 0dac2a587..4f3e6e829 100644 --- a/packages/common-ui-web/src/icons/assets.ts +++ b/packages/common-ui-web/src/icons/assets.ts @@ -330,7 +330,7 @@ export const MonkIconAssetsMap: IconAssetsMap = { 'zoom-out': '', 'rotate-left': - '', + '', 'rotate-right': - '', + '', }; diff --git a/packages/common-ui-web/test/components/ImageDetailedView/ImageDetailedViewOverlay/ImageDetailedViewOverlay.test.tsx b/packages/common-ui-web/test/components/ImageDetailedView/ImageDetailedViewOverlay/ImageDetailedViewOverlay.test.tsx index c90dfb659..41716f413 100644 --- a/packages/common-ui-web/test/components/ImageDetailedView/ImageDetailedViewOverlay/ImageDetailedViewOverlay.test.tsx +++ b/packages/common-ui-web/test/components/ImageDetailedView/ImageDetailedViewOverlay/ImageDetailedViewOverlay.test.tsx @@ -30,7 +30,7 @@ import { useImageLabelIcon, } from '../../../../src/components/ImageDetailedView/ImageDetailedViewOverlay/hooks'; import { Image, ImageStatus } from '@monkvision/types'; -import { Button, Icon } from '../../../../src'; +import { Button, Icon, IconName } from '../../../../src'; import { useObjectTranslation } from '@monkvision/common'; function createProps(): ImageDetailedViewOverlayProps { @@ -87,7 +87,7 @@ describe('ImageDetailedViewOverlay component', () => { it('should display the image label with the proper icon', () => { const props = createProps(); props.image.label = { en: 'test', fr: 'fr', de: 'test-de', nl: 'test-nl' }; - const icon = 'hello-test-icon'; + const icon = 'hello-test-icon' as IconName; const primaryColor = 'test-primary-test'; (useImageLabelIcon as jest.Mock).mockImplementationOnce(() => ({ icon, primaryColor })); const label = 'valid-test-label'; diff --git a/packages/common-ui-web/test/components/InspectionGallery/InspectionGalleryItemCard/InspectionGalleryItemCard.test.tsx b/packages/common-ui-web/test/components/InspectionGallery/InspectionGalleryItemCard/InspectionGalleryItemCard.test.tsx index 90960bf18..141e23860 100644 --- a/packages/common-ui-web/test/components/InspectionGallery/InspectionGalleryItemCard/InspectionGalleryItemCard.test.tsx +++ b/packages/common-ui-web/test/components/InspectionGallery/InspectionGalleryItemCard/InspectionGalleryItemCard.test.tsx @@ -164,7 +164,7 @@ describe('InspectionGalleryItemCard component', () => { const { unmount } = render(); expect(useInspectionGalleryItemStatusIconName).toHaveBeenCalled(); - expectPropsOnChildMock(Icon, { icon: useInspectionGalleryItemStatusIconName({} as any) }); + expectPropsOnChildMock(Icon, { icon: useInspectionGalleryItemStatusIconName({} as any)! }); unmount(); }); diff --git a/packages/common-ui-web/test/components/InspectionGallery/hooks/useInspectionGalleryItems.test.ts b/packages/common-ui-web/test/components/InspectionGallery/hooks/useInspectionGalleryItems.test.ts index 9dd7f0869..0022ef346 100644 --- a/packages/common-ui-web/test/components/InspectionGallery/hooks/useInspectionGalleryItems.test.ts +++ b/packages/common-ui-web/test/components/InspectionGallery/hooks/useInspectionGalleryItems.test.ts @@ -1,6 +1,12 @@ import { renderHook } from '@testing-library/react-hooks'; import { sights } from '@monkvision/sights'; -import { ComplianceIssue, ComplianceOptions, Image, ImageStatus } from '@monkvision/types'; +import { + AddDamage, + ComplianceIssue, + ComplianceOptions, + Image, + ImageStatus, +} from '@monkvision/types'; import { createEmptyMonkState, useMonkState } from '@monkvision/common'; import { useInspectionPoll } from '@monkvision/network'; import { useInspectionGalleryItems } from '../../../../src/components/InspectionGallery/hooks'; @@ -19,6 +25,7 @@ function createProps(): InspectionGalleryProps { }, refreshIntervalMs: 1234, captureMode: true, + addDamage: AddDamage.TWO_SHOT, }; } diff --git a/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx b/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx index 9d0daccd1..989c63220 100644 --- a/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx +++ b/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx @@ -19,7 +19,7 @@ describe('LiveConfigAppProvider component', () => { }); it('should fetch the live config and pass it to the MonkAppStateProvider component', async () => { - const config = { hello: 'world' }; + const config = { hello: 'world' } as unknown as CaptureAppConfig; (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve(config)); const id = 'test-id-test'; const { unmount } = render(); diff --git a/packages/common/test/i18n/utils.test.tsx b/packages/common/test/i18n/utils.test.tsx index f23c536dc..5cef6ee70 100644 --- a/packages/common/test/i18n/utils.test.tsx +++ b/packages/common/test/i18n/utils.test.tsx @@ -72,8 +72,8 @@ describe('Monkvision i18n utils', () => { }>; it('should wrap the component in a I18nextProvider with the given instance', () => { - const instance = { test: 'test' }; - const Wrapped = i18nWrap(TestComponent, instance as unknown as i18n); + const instance = { test: 'test' } as unknown as i18n; + const Wrapped = i18nWrap(TestComponent, instance); const { unmount } = render(); diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx index 33cf907bd..58308c1a7 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx @@ -24,6 +24,7 @@ import { DeviceOrientation, PhotoCaptureTutorialOption, Sight, + AddDamage, } from '@monkvision/types'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -59,7 +60,7 @@ export interface PhotoCaptureProps | 'showCloseButton' | 'enforceOrientation' | 'allowSkipRetake' - | 'enableAddDamage' + | 'addDamage' | 'sightGuidelines' | 'enableSightGuidelines' | 'enableTutorial' @@ -129,7 +130,7 @@ export function PhotoCapture({ customComplianceThresholdsPerSight, useLiveCompliance = false, allowSkipRetake = false, - enableAddDamage = true, + addDamage = AddDamage.PART_SELECT, sightGuidelines, enableTutorial = PhotoCaptureTutorialOption.FIRST_TIME_ONLY, allowSkipTutorial = true, @@ -261,6 +262,7 @@ export function PhotoCapture({ onSelectSight: sightState.selectSight, onRetakeSight: sightState.retakeSight, onAddDamage: addDamageHandle.handleAddDamage, + onAddDamageParts: addDamageHandle.handleAddParts, onCancelAddDamage: addDamageHandle.handleCancelAddDamage, onRetry: sightState.retryLoadingInspection, loading, @@ -268,7 +270,7 @@ export function PhotoCapture({ inspectionId, showCloseButton, images, - enableAddDamage, + addDamage, sightGuidelines, enableSightGuidelines, currentTutorialStep, @@ -310,7 +312,7 @@ export function PhotoCapture({ onBack={handleGalleryBack} onNavigateToCapture={handleNavigateToCapture} onValidate={handleGalleryValidate} - enableAddDamage={enableAddDamage} + addDamage={addDamage} validateButtonLabel={validateButtonLabel} isInspectionCompleted={sightState.isInspectionCompleted} {...complianceOptions} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx index 67394cce7..84c45157f 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx @@ -7,7 +7,7 @@ import { LoadingState } from '@monkvision/common'; import { useAnalytics } from '@monkvision/analytics'; import { PhotoCaptureHUDButtons } from './PhotoCaptureHUDButtons'; import { usePhotoCaptureHUDStyle } from './hooks'; -import { PhotoCaptureMode, TutorialSteps } from '../hooks'; +import { AddDamageHandle, PhotoCaptureMode, TutorialSteps } from '../hooks'; import { PhotoCaptureHUDOverlay } from './PhotoCaptureHUDOverlay'; import { PhotoCaptureHUDElements } from './PhotoCaptureHUDElements'; import { PhotoCaptureHUDTutorial } from './PhotoCaptureHUDTutorial'; @@ -21,7 +21,7 @@ export interface PhotoCaptureHUDProps CaptureAppConfig, | 'enableSightGuidelines' | 'sightGuidelines' - | 'enableAddDamage' + | 'addDamage' | 'showCloseButton' | 'allowSkipTutorial' > { @@ -76,11 +76,15 @@ export interface PhotoCaptureHUDProps /** * Callback to be called when the user clicks on the "Add Damage" button. */ - onAddDamage: () => void; + onAddDamage: AddDamageHandle['handleAddDamage']; + /** + * Callback to be called when the user selects the parts to take a picture of. + */ + onAddDamageParts: AddDamageHandle['handleAddParts']; /** * Callback to be called when the user clicks on the "Cancel" button of the Add Damage mode. */ - onCancelAddDamage: () => void; + onCancelAddDamage: AddDamageHandle['handleCancelAddDamage']; /** * Callback that can be used to retry fetching this state object from the API in case the previous fetch failed. */ @@ -115,6 +119,7 @@ export function PhotoCaptureHUD({ onSelectSight, onRetakeSight, onAddDamage, + onAddDamageParts, onCancelAddDamage, onOpenGallery, onRetry, @@ -124,7 +129,7 @@ export function PhotoCaptureHUD({ handle, cameraPreview, images, - enableAddDamage, + addDamage, sightGuidelines, enableSightGuidelines, currentTutorialStep, @@ -145,13 +150,11 @@ export function PhotoCaptureHUD({ ).length, [images], ); - const handleCloseConfirm = () => { setShowCloseModal(false); trackEvent('Capture Closed'); onClose?.(); }; - return (

@@ -162,6 +165,7 @@ export function PhotoCaptureHUD({ sightsTaken={sightsTaken} mode={mode} onAddDamage={onAddDamage} + onAddDamageParts={onAddDamageParts} onCancelAddDamage={onCancelAddDamage} onSelectSight={onSelectSight} onRetakeSight={onRetakeSight} @@ -169,7 +173,7 @@ export function PhotoCaptureHUD({ error={loading.error ?? handle.error} previewDimensions={handle.previewDimensions} images={images} - enableAddDamage={enableAddDamage} + addDamage={addDamage} sightGuidelines={sightGuidelines} enableSightGuidelines={enableSightGuidelines} tutorialStep={currentTutorialStep} @@ -210,6 +214,7 @@ export function PhotoCaptureHUD({ onCloseTutorial={onCloseTutorial} allowSkipTutorial={allowSkipTutorial} sightId={selectedSight.id} + addDamage={addDamage} sightGuidelines={sightGuidelines} />
diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.model.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.model.ts new file mode 100644 index 000000000..219313a6e --- /dev/null +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.model.ts @@ -0,0 +1,73 @@ +import { IconName } from '@monkvision/common-ui-web'; + +/** + * Photo capture action button props. + */ +export interface ActionButtonProps { + /** + * Type of the action button to display. If the value is 'close', the button will display a close icon. + * + * @default 'close' + */ + action?: 'close' & IconName; + /** + * Boolean indicating if the close button is disabled. + * + * @default false + */ + closeDisabled?: boolean; + /** + * Boolean indicating if the close button should be displayed in the HUD on top of the Camera preview. + * + * @default false + */ + showCloseButton?: boolean; + /** + * Callback called when the user clicks on the close button. If this callback is not provided, the close button will + * not be displayed on the screen. + */ + onClose?: () => void; +} + +/** + * Props of the PhotoCaptureHUDButtons component. + */ +export type PhotoCaptureHUDButtonsProps = ActionButtonProps & { + /** + * URI of the picture displayed in the gallery button icon. Usually, this is the last picture taken by the user. If no + * picture is provided, a gallery icon will be displayed instead. + */ + galleryPreview?: string; + /** + * Callback called when the user clicks on the take picture button. + */ + onTakePicture?: () => void; + /** + * Callback called when the user clicks on the gallery button. + */ + onOpenGallery?: () => void; + /** + * Boolean indicating if the gallery button is disabled. + * + * @default false + */ + galleryDisabled?: boolean; + /** + * Boolean indicating if the take picture button is disabled. + * + * @default false + */ + takePictureDisabled?: boolean; + /** + * Boolean indicating if the little notification badge on top of the gallery button should be displayed. + * + * @default false + */ + showGalleryBadge?: boolean; + /** + * Total number of sights to retake + * + * @default 0 + */ + retakeCount?: number; +}; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles.ts index 16da8d336..505728017 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.styles.ts @@ -68,6 +68,7 @@ export const styles: Styles = { }, buttonDisabled: { cursor: 'default', + opacity: 0.3, }, backgroundCover: { width: '100%', diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.tsx index 23788e848..ccccee400 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDButtons/PhotoCaptureHUDButtons.tsx @@ -1,66 +1,7 @@ import { Icon, TakePictureButton } from '@monkvision/common-ui-web'; import { useInteractiveStatus } from '@monkvision/common'; import { useCaptureHUDButtonsStyles } from './hooks'; - -/** - * Props of the PhotoCaptureHUDButtons component. - */ -export interface PhotoCaptureHUDButtonsProps { - /** - * URI of the picture displayed in the gallery button icon. Usually, this is the last picture taken by the user. If no - * picture is provided, a gallery icon will be displayed instead. - */ - galleryPreview?: string; - /** - * Callback called when the user clicks on the take picture button. - */ - onTakePicture?: () => void; - /** - * Callback called when the user clicks on the gallery button. - */ - onOpenGallery?: () => void; - /** - * Callback called when the user clicks on the close button. If this callback is not provided, the close button will - * not be displayed on the screen. - */ - onClose?: () => void; - /** - * Boolean indicating if the gallery button is disabled. - * - * @default false - */ - galleryDisabled?: boolean; - /** - * Boolean indicating if the take picture button is disabled. - * - * @default false - */ - takePictureDisabled?: boolean; - /** - * Boolean indicating if the close button is disabled. - * - * @default false - */ - closeDisabled?: boolean; - /** - * Boolean indicating if the close button should be displayed in the HUD on top of the Camera preview. - * - * @default false - */ - showCloseButton?: boolean; - /** - * Boolean indicating if the little notification badge on top of the gallery button should be displayed. - * - * @default false - */ - showGalleryBadge?: boolean; - /** - * Total number of sights to retake - * - * @default 0 - */ - retakeCount?: number; -} +import { PhotoCaptureHUDButtonsProps } from './PhotoCaptureHUDButtons.model'; /** * Components implementing the main buttons of the PhotoCapture Camera HUD. This component implements 3 buttons : @@ -79,6 +20,7 @@ export function PhotoCaptureHUDButtons({ showCloseButton = false, showGalleryBadge = false, retakeCount = 0, + action = 'close', }: PhotoCaptureHUDButtonsProps) { const { status: galleryStatus, eventHandlers: galleryEventHandlers } = useInteractiveStatus({ disabled: galleryDisabled, @@ -105,7 +47,7 @@ export function PhotoCaptureHUDButtons({ {...closeEventHandlers} data-testid='monk-close-btn' > - + + + +
+ } + /> + ); +} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddPartSelectShot/index.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddPartSelectShot/index.ts new file mode 100644 index 000000000..3d85f16e8 --- /dev/null +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddPartSelectShot/index.ts @@ -0,0 +1 @@ +export { PhotoCaptureHUDElementsAddPartSelectShot } from './PhotoCaptureHUDElementsAddPartSelectShot'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddPartSelectShot/usePhotoCaptureHUDElementsAddPartSelectShotStyle.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddPartSelectShot/usePhotoCaptureHUDElementsAddPartSelectShotStyle.ts new file mode 100644 index 000000000..ed18c0d91 --- /dev/null +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsAddPartSelectShot/usePhotoCaptureHUDElementsAddPartSelectShotStyle.ts @@ -0,0 +1,29 @@ +import { useMonkTheme } from '@monkvision/common'; +import { CSSProperties } from 'react'; + +export function usePhotoCaptureHUDElementsAddPartSelectShotStyle(): Record< + 'popup' | 'dialogButtonGroup' | 'vehicleSelect' | 'button', + CSSProperties +> { + const { palette } = useMonkTheme(); + const minValueWithAspectRatio = (width: number) => `min(${width}dvw, calc(${width}dvh * 1.5))`; + return { + popup: { + width: `clamp(${minValueWithAspectRatio(80)}, 80%, ${minValueWithAspectRatio(90)})`, + backgroundColor: palette.background.base, + display: 'flex', + flexDirection: 'column', + justifyItems: 'center', + padding: '3svw', + gap: 10, + borderRadius: '3svmin', + alignItems: 'center', + boxSizing: 'border-box', + overflow: 'scroll', + maxHeight: '100%', + }, + dialogButtonGroup: { display: 'flex', gap: 20 }, + vehicleSelect: { alignSelf: 'stretch', justifySelf: 'stretch' }, + button: { padding: '.5svw 2svw', fontSize: 'min(14px, 3svmin)', borderRadius: '4svmin' }, + }; +} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton/AddDamageButton.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton/AddDamageButton.tsx index 44e5dbf65..98565f97c 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton/AddDamageButton.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton/AddDamageButton.tsx @@ -1,5 +1,7 @@ import { Button } from '@monkvision/common-ui-web'; +import { AddDamage } from '@monkvision/types'; import { useTranslation } from 'react-i18next'; +import { AddDamageHandle } from '../../../hooks'; import { usePhotoCaptureHUDButtonBackground } from '../../hooks'; /** @@ -9,25 +11,25 @@ export interface AddDamageButtonProps { /** * Callback called when the user presses the button. */ - onAddDamage?: () => void; + onAddDamage?: AddDamageHandle['handleAddDamage']; /** * Boolean indicating whether the Add Damage feature is enabled. If disabled, the `Add Damage` button will be hidden. * - * @default true + * @default AddDamage.DISABLED */ - enableAddDamage?: boolean; + addDamage?: AddDamage; } /** * Custom button displayed in the PhotoCapture Camera HUD that allows user to enter add damage mode. */ -export function AddDamageButton({ onAddDamage, enableAddDamage }: AddDamageButtonProps) { +export function AddDamageButton({ onAddDamage, addDamage }: AddDamageButtonProps) { const { t } = useTranslation(); const primaryColor = usePhotoCaptureHUDButtonBackground(); return ( <> - {enableAddDamage && ( + {addDamage && addDamage !== AddDamage.DISABLED && ( ; + }); + }); + afterEach(() => jest.clearAllMocks()); + + it('should have message of select parts', () => { + const { getByText } = render( + {}} onAddDamageParts={() => {}} />, + ); + expect(getByText('photo.hud.addDamage.selectParts')).toBeInTheDocument(); + }); + + it('should call VehiclePartSelection', () => { + render( + {}} onAddDamageParts={() => {}} />, + ); + expectPropsOnChildMock(VehiclePartSelection, { + vehicleType: VehicleType.CUV, + }); + }); + + it('should have accept and cancel buttons', () => { + const { getByText } = render( + {}} onAddDamageParts={() => {}} />, + ); + expect(getByText('photo.hud.addDamage.accept')).toBeInTheDocument(); + expect(getByText('photo.hud.addDamage.cancel')).toBeInTheDocument(); + }); + + it('should call onCancel when cancel button is clicked', () => { + const onCancel = jest.fn(); + const { getByText } = render( + {}} />, + ); + fireEvent.click(getByText('photo.hud.addDamage.cancel')); + expect(onCancel).toHaveBeenCalled(); + }); + + it('should call onAddDamageParts and hide the popup when accept button is clicked', () => { + const onAddDamageParts = jest.fn(); + const { getByText } = render( + {}} + onAddDamageParts={onAddDamageParts} + />, + ); + fireEvent.click(getByText('photo.hud.addDamage.accept')); + expect(onAddDamageParts).toHaveBeenCalled(); + }); + + it('should show the selected parts on UI', () => { + (getLanguage as jest.MockedFunction).mockReturnValue('en'); + const { getByText, container } = render( + {}} onAddDamageParts={() => {}} />, + ); + const { onPartsSelected } = ( + VehiclePartSelection as jest.MockedFunction + ).mock.calls[0][0]; + expect(onPartsSelected).toBeDefined(); + expect(getByText('photo.hud.addDamage.selectParts')).toBeInTheDocument(); + if (onPartsSelected) { + act(() => onPartsSelected([VehiclePart.BUMPER_BACK])); + } + console.log(prettyDOM(container)); + expect(getByText('photo.hud.addDamage.selectPartsRear Bumper')).toBeInTheDocument(); + }); +}); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton.test.tsx index 07c9ed665..37eb1a574 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/AddDamageButton.test.tsx @@ -3,12 +3,13 @@ import { useTranslation } from 'react-i18next'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { Button } from '@monkvision/common-ui-web'; import { AddDamageButton } from '../../../../src'; +import { AddDamage } from '@monkvision/types'; describe('AddDamageButton component', () => { - it('should not render when enableAddDamage is false', () => { + it('should not render when addDamage is disable', () => { const onAddDamage = jest.fn(); const { unmount } = render( - , + , ); expect(Button).not.toHaveBeenCalled(); @@ -18,7 +19,7 @@ describe('AddDamageButton component', () => { it('should pass the onAddDamage callback to the onClick event of the Button', () => { const onAddDamage = jest.fn(); const { unmount } = render( - , + , ); expectPropsOnChildMock(Button, { onClick: onAddDamage }); @@ -30,7 +31,7 @@ describe('AddDamageButton component', () => { const label = 'test-label-ok'; const tMock = jest.fn(() => label); (useTranslation as jest.Mock).mockImplementationOnce(() => ({ t: tMock })); - const { unmount } = render(); + const { unmount } = render(); expect(tMock).toHaveBeenCalledWith('photo.hud.sight.addDamageBtn'); expectPropsOnChildMock(Button, { children: label }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.test.tsx index bac9cc830..8c07c3057 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.test.tsx @@ -1,4 +1,4 @@ -import { Image, ImageStatus } from '@monkvision/types'; +import { AddDamage, Image, ImageStatus } from '@monkvision/types'; jest.mock('../../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter', () => ({ PhotoCaptureHUDCounter: jest.fn(() => <>), @@ -48,6 +48,7 @@ function createProps(): PhotoCaptureHUDElementsSightProps { { sightId: 'test-sight-2', status: ImageStatus.SUCCESS }, ] as Image[], tutorialStep: null, + addDamage: AddDamage.TWO_SHOT, }; } diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline.test.tsx index aadbc94bd..5d2d2073e 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline.test.tsx @@ -4,6 +4,7 @@ import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { Button } from '@monkvision/common-ui-web'; import { SightGuideline } from '../../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline'; import { getLanguage } from '@monkvision/common'; +import { AddDamage } from '@monkvision/types'; function createProps() { return { @@ -18,7 +19,7 @@ function createProps() { information: 'info-test', }, ], - enableAddDamage: true, + addDamage: AddDamage.TWO_SHOT, enableSightGuidelines: true, }; } diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightSlider/SightSliderButton.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightSlider/SightSliderButton.test.tsx index 8fa0749d4..4474c5350 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightSlider/SightSliderButton.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightSlider/SightSliderButton.test.tsx @@ -1,8 +1,8 @@ import { render } from '@testing-library/react'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; -import { Button } from '@monkvision/common-ui-web'; +import { Button, ButtonProps } from '@monkvision/common-ui-web'; import { ImageStatus } from '@monkvision/types'; -import { SightSliderButton } from '../../../../../src'; +import { SightSliderButton, SightSliderButtonProps } from '../../../../../src'; describe('SightSliderButton component', () => { it('should display a Button with the proper label', () => { @@ -30,40 +30,47 @@ describe('SightSliderButton component', () => { unmount(); }); - [ - { - status: ImageStatus.UPLOADING, - icon: 'processing', - primaryColor: 'background-base', - disabled: true, - }, - { - status: ImageStatus.COMPLIANCE_RUNNING, - icon: 'processing', - primaryColor: 'background-base', - disabled: true, - }, - { status: ImageStatus.SUCCESS, icon: 'check-circle', primaryColor: 'primary', disabled: true }, - { - status: ImageStatus.UPLOAD_FAILED, - icon: 'wifi-off', - primaryColor: 'alert-dark', - disabled: false, - }, - { - status: ImageStatus.UPLOAD_ERROR, - icon: 'sync-problem', - primaryColor: 'alert-dark', - disabled: false, - }, - { - status: ImageStatus.NOT_COMPLIANT, - icon: 'error', - primaryColor: 'alert-dark', - disabled: false, - }, - { status: null, icon: undefined, primaryColor: 'background-base', disabled: false }, - ].forEach(({ status, icon, primaryColor, disabled }) => { + ( + [ + { + status: ImageStatus.UPLOADING, + icon: 'processing', + primaryColor: 'background-base', + disabled: true, + }, + { + status: ImageStatus.COMPLIANCE_RUNNING, + icon: 'processing', + primaryColor: 'background-base', + disabled: true, + }, + { + status: ImageStatus.SUCCESS, + icon: 'check-circle', + primaryColor: 'primary', + disabled: true, + }, + { + status: ImageStatus.UPLOAD_FAILED, + icon: 'wifi-off', + primaryColor: 'alert-dark', + disabled: false, + }, + { + status: ImageStatus.UPLOAD_ERROR, + icon: 'sync-problem', + primaryColor: 'alert-dark', + disabled: false, + }, + { + status: ImageStatus.NOT_COMPLIANT, + icon: 'error', + primaryColor: 'alert-dark', + disabled: false, + }, + { status: null, icon: undefined, primaryColor: 'background-base', disabled: false }, + ] as Array + ).forEach(({ status, icon, primaryColor, disabled }) => { it(`should properly handle the ${status} status`, () => { const { unmount } = render(); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureTutorial/PhotoCaptureHUDTutorial.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureTutorial/PhotoCaptureHUDTutorial.test.tsx index 43604bef9..54c74a400 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureTutorial/PhotoCaptureHUDTutorial.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureTutorial/PhotoCaptureHUDTutorial.test.tsx @@ -14,6 +14,7 @@ import { } from '../../../../src'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { TutorialSteps } from '../../../../src/PhotoCapture/hooks'; +import { AddDamage } from '@monkvision/types'; const BACKDROP_TEST_ID = 'backdrop'; const TITLE_TEST_ID = 'title'; @@ -27,6 +28,7 @@ function createProps(): PhotoCaptureHUDTutorialProps { allowSkipTutorial: false, sightGuidelines: [], sightId: 'test-sight-id', + addDamage: AddDamage.TWO_SHOT, }; } @@ -57,7 +59,7 @@ describe('PhotoCaptureHUDTutorial component', () => { sightId: props.sightId, sightGuidelines: props.sightGuidelines, enableSightGuidelines: props.currentTutorialStep === TutorialSteps.GUIDELINE, - enableAddDamage: true, + addDamage: AddDamage.TWO_SHOT, }); expect(DynamicSVG).not.toHaveBeenCalled(); diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/useAddDamageMode.test.ts b/packages/inspection-capture-web/test/PhotoCapture/hooks/useAddDamageMode.test.ts index d819ec617..230f56d6a 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/useAddDamageMode.test.ts +++ b/packages/inspection-capture-web/test/PhotoCapture/hooks/useAddDamageMode.test.ts @@ -1,8 +1,15 @@ import { renderHook } from '@testing-library/react-hooks'; import { PhotoCaptureMode, useAddDamageMode } from '../../../src/PhotoCapture/hooks'; import { act } from '@testing-library/react'; +import { MonkAppState, useMonkAppState } from '@monkvision/common'; +import { AddDamage, VehiclePart } from '@monkvision/types'; describe('useAddDamageMode hook', () => { + beforeEach(() => { + (useMonkAppState as jest.MockedFunction).mockReturnValue({ + config: { addDamage: AddDamage.TWO_SHOT }, + } as ReturnType infer R ? (args: any) => R : never>); + }); it('should be in the SIGHT mode by default', () => { const { result, unmount } = renderHook(useAddDamageMode); expect(result.current.mode).toEqual(PhotoCaptureMode.SIGHT); @@ -44,4 +51,42 @@ describe('useAddDamageMode hook', () => { expect(result.current.mode).toEqual(PhotoCaptureMode.SIGHT); unmount(); }); + + it('should switch to ADD_DAMAGE_PART_SELECT', () => { + (useMonkAppState as jest.MockedFunction).mockReturnValue({ + config: { addDamage: AddDamage.PART_SELECT }, + } as ReturnType infer R ? (args: any) => R : never>); + const { result } = renderHook(useAddDamageMode); + act(() => result.current.handleAddDamage()); + expect(result.current.mode).toEqual(PhotoCaptureMode.ADD_DAMAGE_PART_SELECT); + }); + + it('should throw error if add damage is disabled', () => { + (useMonkAppState as jest.MockedFunction).mockReturnValue({ + config: { addDamage: AddDamage.DISABLED }, + } as ReturnType infer R ? (args: any) => R : never>); + const { result } = renderHook(useAddDamageMode); + expect(() => act(() => result.current.handleAddDamage())).toThrowError( + 'Add Damage feature is disabled', + ); + }); + + it('should throw error if add damage type is unknown', () => { + (useMonkAppState as jest.MockedFunction).mockReturnValue({ + config: { addDamage: 'unknown' as AddDamage }, + } as ReturnType infer R ? (args: any) => R : never>); + const { result } = renderHook(useAddDamageMode); + expect(() => act(() => result.current.handleAddDamage())).toThrowError( + 'Unknown Add Damage type', + ); + }); + + it('should set the vehicle parts', () => { + const { result } = renderHook(useAddDamageMode); + act(() => result.current.handleAddParts([VehiclePart.BUMPER_BACK, VehiclePart.BUMPER_FRONT])); + expect(result.current.vehicleParts).toEqual([ + VehiclePart.BUMPER_BACK, + VehiclePart.BUMPER_FRONT, + ]); + }); }); diff --git a/packages/network/src/api/image/requests.ts b/packages/network/src/api/image/requests.ts index 682329e72..fd52a9171 100644 --- a/packages/network/src/api/image/requests.ts +++ b/packages/network/src/api/image/requests.ts @@ -1,6 +1,11 @@ import ky from 'ky'; import { Dispatch } from 'react'; -import { getFileExtensions, MonkActionType, MonkCreatedOneImageAction } from '@monkvision/common'; +import { + getFileExtensions, + MonkActionType, + MonkCreatedOneImageAction, + vehiclePartLabels, +} from '@monkvision/common'; import { ComplianceOptions, Image, @@ -12,6 +17,7 @@ import { MonkPicture, TaskName, TranslationObject, + VehiclePart, } from '@monkvision/types'; import { v4 } from 'uuid'; import { labels, sights } from '@monkvision/sights'; @@ -33,6 +39,11 @@ export enum ImageUploadType { * add damage workflow. */ CLOSE_UP_2_SHOT = 'close_up_2_shot', + /** + * Upload type corresponding to a part selection shot in the PhotoCapture process. when using the part select add + * damage workflow. + */ + PART_SELECT_SHOT = 'part_select_shot', /** * Upload type corresponding to a video frame in the VideoCapture process. */ @@ -110,6 +121,27 @@ export interface Add2ShotCloseUpImageOptions { compliance?: ComplianceOptions; } +/** + * Options specified when adding a close up (an "add damage" image) to an inspection using the part select process. + */ +export type AddPartSelectCloseUpImageOptions = Pick< + Add2ShotCloseUpImageOptions, + 'picture' | 'inspectionId' | 'compliance' | 'useThumbnailCaching' +> & { + /** + * The type of the image upload : `ImageUploadType.PART_SELECT_SHOT`; + */ + uploadType: ImageUploadType.PART_SELECT_SHOT; + /** + * To mark image type as close up. + */ + image_type: ImageType.CLOSE_UP; + /** + * List of damage parts chosen by User with part selected wireframe + */ + vehicleParts: VehiclePart[]; +}; + /** * Options specififed when adding a video frame to a VideoCapture inspection. */ @@ -143,7 +175,8 @@ export interface AddVideoFrameOptions { export type AddImageOptions = | AddBeautyShotImageOptions | Add2ShotCloseUpImageOptions - | AddVideoFrameOptions; + | AddVideoFrameOptions + | AddPartSelectCloseUpImageOptions; interface AddImageData { filename: string; @@ -151,7 +184,10 @@ interface AddImageData { } function getImageType(options: AddImageOptions): ImageType { - if (options.uploadType === ImageUploadType.CLOSE_UP_2_SHOT) { + if ( + options.uploadType === ImageUploadType.CLOSE_UP_2_SHOT || + options.uploadType === ImageUploadType.PART_SELECT_SHOT + ) { return ImageType.CLOSE_UP; } return ImageType.BEAUTY_SHOT; @@ -169,12 +205,24 @@ function getImageLabel(options: AddImageOptions): TranslationObject | undefined nl: `Videoframe ${options.frameIndex}`, }; } - return { - en: options.firstShot ? 'Close Up (part)' : 'Close Up (damage)', - fr: options.firstShot ? 'Photo Zoomée (partie)' : 'Photo Zoomée (dégât)', - de: options.firstShot ? 'Gezoomtes Foto (Teil)' : 'Close Up (Schaden)', - nl: options.firstShot ? 'Nabij (onderdeel)' : 'Nabij (schade)', - }; + if (options.uploadType === ImageUploadType.PART_SELECT_SHOT) { + const partsTranslation = options.vehicleParts.map((part) => vehiclePartLabels[part]); + return { + en: `Damage on ${partsTranslation.map((part) => part.en).join(', ')}`, + fr: `Dégât sur ${partsTranslation.map((part) => part.en).join(', ')}`, + de: `Schaden an ${partsTranslation.map((part) => part.en).join(', ')}`, + nl: `Schade aan ${partsTranslation.map((part) => part.en).join(', ')}`, + }; + } + if (options.uploadType === ImageUploadType.CLOSE_UP_2_SHOT) { + return { + en: options.firstShot ? 'Close Up (part)' : 'Close Up (damage)', + fr: options.firstShot ? 'Photo Zoomée (partie)' : 'Photo Zoomée (dégât)', + de: options.firstShot ? 'Gezoomtes Foto (Teil)' : 'Close Up (Schaden)', + nl: options.firstShot ? 'Nabij (onderdeel)' : 'Nabij (schade)', + }; + } + return undefined as never; } function getAdditionalData(options: AddImageOptions): ImageAdditionalData { @@ -229,7 +277,33 @@ function createBeautyShotImageData( return { filename, body }; } -function createCloseUpImageData( +function createPartSelectImageData(options: AddPartSelectCloseUpImageOptions): AddImageData { + const filename = `part-select-${options.inspectionId}-${Date.now()}.jpg`; + + const body: ApiImagePost = { + acquisition: { + strategy: 'upload_multipart_form_keys', + file_key: MULTIPART_KEY_IMAGE, + }, + image_type: ImageType.CLOSE_UP, + tasks: [ + TaskName.DAMAGE_DETECTION, + { + name: TaskName.COMPLIANCES, + wait_for_result: + options.compliance?.enableCompliance && options.compliance?.useLiveCompliance, + }, + ], + detailed_viewpoint: { + centers_on: options.vehicleParts, + }, + additional_data: getAdditionalData(options), + }; + + return { filename, body }; +} + +function createCloseUp2ShotImageData( options: Add2ShotCloseUpImageOptions, filetype: string, ): AddImageData { @@ -278,11 +352,13 @@ function getAddImageData(options: AddImageOptions, filetype: string): AddImageDa case ImageUploadType.BEAUTY_SHOT: return createBeautyShotImageData(options, filetype); case ImageUploadType.CLOSE_UP_2_SHOT: - return createCloseUpImageData(options, filetype); + return createCloseUp2ShotImageData(options, filetype); case ImageUploadType.VIDEO_FRAME: return createVideoFrameData(options, filetype); + case ImageUploadType.PART_SELECT_SHOT: + return createPartSelectImageData(options); default: - throw new Error('Unknown image upload type.'); + return 'Unknown image upload type.' as never; } } diff --git a/packages/network/src/api/models/image.ts b/packages/network/src/api/models/image.ts index 06da6b64f..262c057c5 100644 --- a/packages/network/src/api/models/image.ts +++ b/packages/network/src/api/models/image.ts @@ -1,4 +1,4 @@ -import { TranslationObject } from '@monkvision/types'; +import { TranslationObject, VehiclePart } from '@monkvision/types'; import type { ApiAdditionalData, ApiCenterOnElement, ApiLabelPrediction } from './common'; import type { ApiRenderedOutputs } from './renderedOutput'; import type { ApiImageComplianceResults } from './compliance'; @@ -19,7 +19,7 @@ export interface ApiRelatedImage { export type ApiRelatedImages = ApiRelatedImage[]; export interface ApiViewpointComponent { - centers_on: ApiCenterOnElement[]; + centers_on: Array; distance?: string; is_exterior?: boolean; } @@ -90,4 +90,5 @@ export interface ApiImagePost { image_sibling_key?: string; compliances?: ApiCompliance; additional_data?: ApiImageAdditionalData; + detailed_viewpoint?: ApiViewpointComponent; } diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 6e06506fb..38d07edf6 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -23,6 +23,23 @@ export enum PhotoCaptureTutorialOption { FIRST_TIME_ONLY = 'first_time_only', } +/** + * Enumeration of the possible values for the Add Damage type. + */ +export enum AddDamage { + /** + * The Add Damage feature is disabled. + */ + DISABLED = 'disabled', + /** + * The Add Damage feature is enabled with two shot. first for the zoom out image and second for the damaged image. + */ + TWO_SHOT = 'two_shot', + /** + * The Add Damage feature is enabled with a single shot after part selected. + */ + PART_SELECT = 'part_select', +} /** * Configuration used to configure the Camera and picture output of the SDK. */ @@ -106,12 +123,11 @@ export type CaptureAppConfig = CameraConfig & */ allowSkipRetake?: boolean; /** - * Boolean indicating if `Add Damage` feature should be enabled or not. If disabled, the `Add Damage` button will - * be hidden. + * Indicating if Add Damage type. If disabled, the `Add Damage` button will be hidden. * - * @default true + * @default AddDamage.PART_SELECT */ - enableAddDamage?: boolean; + addDamage: AddDamage; /** * A collection of sight guidelines in different language with a list of sightIds associate to it. */