From 5440f3166ab4f1b6732a49880a1db4a0bc74ae08 Mon Sep 17 00:00:00 2001 From: David Ly Date: Wed, 29 Jan 2025 22:44:07 +0100 Subject: [PATCH] Added Sight guidelines EPHEMERAL option --- apps/demo-app/src/local-config.json | 2 +- documentation/docs/configuration.md | 2 +- documentation/src/utils/schemas.ts | 3 +- .../schemas/photoCaptureConfig.schema.ts | 3 +- .../src/PhotoCapture/PhotoCapture.tsx | 10 ++- .../PhotoCaptureHUD/PhotoCaptureHUD.tsx | 15 +++- .../PhotoCaptureHUDElements.tsx | 13 ++- .../PhotoCaptureHUDElementsSight.tsx | 6 +- .../SightGuideline/SightGuideline.styles.ts | 30 +++++-- .../SightGuideline/SightGuideline.tsx | 71 +++++++++++---- .../SightGuideline/hooks.ts | 19 ++++ .../PhotoCaptureHUDElementsSight/hooks.ts | 10 ++- .../PhotoCaptureHUDTutorial.tsx | 2 +- .../src/PhotoCapture/hooks/index.ts | 1 + .../hooks/usePhotoCaptureSightGuidelines.ts | 71 +++++++++++++++ .../hooks/usePhotoCaptureTutorial.ts | 19 +++- .../src/translations/de.json | 4 +- .../src/translations/en.json | 4 +- .../src/translations/fr.json | 4 +- .../src/translations/nl.json | 4 +- .../DamageDislosureHUD.test.tsx | 3 +- .../test/PhotoCapture/PhotoCapture.test.tsx | 15 +++- .../PhotoCaptureHUD/PhotoCaptureHUD.test.tsx | 2 + .../SightGuideline.test.tsx | 89 +++++++++++++------ .../PhotoCaptureHUDTutorial.test.tsx | 10 +-- .../usePhotoCaptureSightGuidelines.test.ts | 86 ++++++++++++++++++ packages/types/src/config.ts | 30 +++++-- 27 files changed, 437 insertions(+), 91 deletions(-) create mode 100644 packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/hooks.ts create mode 100644 packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureSightGuidelines.ts create mode 100644 packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureSightGuidelines.test.ts diff --git a/apps/demo-app/src/local-config.json b/apps/demo-app/src/local-config.json index 4ca0c124f..fb17a198e 100644 --- a/apps/demo-app/src/local-config.json +++ b/apps/demo-app/src/local-config.json @@ -4,7 +4,7 @@ "workflow": "photo", "allowSkipRetake": true, "addDamage": "part_select", - "enableSightGuidelines": true, + "enableSightGuidelines": "ephemeral", "allowVehicleTypeSelection": true, "allowManualLogin": true, "fetchFromSearchParams": true, diff --git a/documentation/docs/configuration.md b/documentation/docs/configuration.md index cb23a32fd..f4d381d1a 100644 --- a/documentation/docs/configuration.md +++ b/documentation/docs/configuration.md @@ -126,7 +126,7 @@ The following table lists the available configuration options in the `PhotoCaptu | allowSkipRetake | `boolean` | If compliance is enabled, this prop indicate if the user is allowed to skip the retaking process if some pictures are not compliant. | | `false` | | addDamage | `AddDamage` | Options for Add Damage. If disabled, the `Add Damage` button will be hidden. | | `AddDamage.PART_SELECT` | | sightGuidelines | `sightGuideline[]` | A collection of sight guidelines in different language with a list of sightIds associate to it. | | | -| enableSightGuideline | `boolean` | Boolean indicating whether the sight guideline feature is enabled. If disabled, the guideline text will be hidden. | | `true` | +| enableSightGuidelines | `PhotoCaptureSightGuidelinesOption` | Option for displaying the Sight guidelines. If disabled, the guideline text will be hidden. | | `PhotoCaptureSightGuidelines.EPHEMERAL` | | defaultVehicleType | `VehicleType` | Default vehicle type to use if no vehicle type has been specified. | ✔️ | | | allowVehicleTypeSelection | `boolean` | Indicates if manual vehicle type selection should be enabled if the vehicle type is not defined. | ✔️ | | | enableTutorial | `PhotoCaptureTutorialOption` | Options for displaying the photo capture tutorial. | | `PhotoCaptureTutorialOption.FIRST_TIME_ONLY` | diff --git a/documentation/src/utils/schemas.ts b/documentation/src/utils/schemas.ts index 356fae305..2e4210c5a 100644 --- a/documentation/src/utils/schemas.ts +++ b/documentation/src/utils/schemas.ts @@ -8,6 +8,7 @@ import { DeviceOrientation, MileageUnit, MonkApiPermission, + PhotoCaptureSightGuidelinesOption, PhotoCaptureTutorialOption, SteeringWheelPosition, TaskName, @@ -309,7 +310,7 @@ export const LiveConfigSchema = z useAdaptiveImageQuality: z.boolean().optional(), allowSkipRetake: z.boolean().optional(), addDamage: z.nativeEnum(AddDamage).optional(), - enableSightGuidelines: z.boolean().optional(), + enableSightGuidelines: z.nativeEnum(PhotoCaptureSightGuidelinesOption).optional(), sightGuidelines: z.array(SightGuidelineSchema).optional(), enableTutorial: z.nativeEnum(PhotoCaptureTutorialOption).optional(), allowSkipTutorial: z.boolean().optional(), diff --git a/documentation/src/utils/schemas/photoCaptureConfig.schema.ts b/documentation/src/utils/schemas/photoCaptureConfig.schema.ts index 4d0e27766..1beeb478c 100644 --- a/documentation/src/utils/schemas/photoCaptureConfig.schema.ts +++ b/documentation/src/utils/schemas/photoCaptureConfig.schema.ts @@ -3,6 +3,7 @@ import { SharedCaptureAppConfigSchema } from '@site/src/utils/schemas/sharedConf import { ComplianceOptionsSchema } from '@site/src/utils/schemas/compliance.schema'; import { CaptureWorkflow, + PhotoCaptureSightGuidelinesOption, PhotoCaptureTutorialOption, TaskName, VehicleType, @@ -27,7 +28,7 @@ export const PhotoCaptureAppConfigSchema = z maxUploadDurationWarning: z.number().optional(), useAdaptiveImageQuality: z.boolean().optional(), sightGuidelines: z.array(SightGuidelineSchema).optional(), - enableSightGuidelines: z.boolean().optional(), + enableSightGuidelines: z.nativeEnum(PhotoCaptureSightGuidelinesOption).optional(), defaultVehicleType: z.nativeEnum(VehicleType), allowVehicleTypeSelection: z.boolean(), enableTutorial: z.nativeEnum(PhotoCaptureTutorialOption).optional(), diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx index 12f99621e..cfa3b4495 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx @@ -15,6 +15,7 @@ import { ComplianceOptions, MonkPicture, PhotoCaptureAppConfig, + PhotoCaptureSightGuidelinesOption, PhotoCaptureTutorialOption, Sight, VehicleType, @@ -37,6 +38,7 @@ import { useComplianceAnalytics, usePhotoCaptureSightState, usePhotoCaptureTutorial, + usePhotoCaptureSightGuidelines, } from './hooks'; /** @@ -137,7 +139,7 @@ export function PhotoCapture({ enableTutorial = PhotoCaptureTutorialOption.FIRST_TIME_ONLY, allowSkipTutorial = true, enableSightTutorial = true, - enableSightGuidelines = true, + enableSightGuidelines = PhotoCaptureSightGuidelinesOption.EPHEMERAL, useAdaptiveImageQuality = true, lang, enforceOrientation, @@ -202,6 +204,9 @@ export function PhotoCapture({ enableSightGuidelines, enableSightTutorial, }); + const { showSightGuidelines, handleDisableSightGuidelines } = usePhotoCaptureSightGuidelines({ + enableSightGuidelines, + }); const { isBadConnectionWarningDialogDisplayed, closeBadConnectionWarningDialog, @@ -277,10 +282,11 @@ export function PhotoCapture({ addDamage, onValidateVehicleParts: addDamageHandle.handleValidateVehicleParts, sightGuidelines, - enableSightGuidelines, + showSightGuidelines, currentTutorialStep, onNextTutorialStep: goToNextTutorialStep, onCloseTutorial: closeTutorial, + onDisableSightGuidelines: handleDisableSightGuidelines, allowSkipTutorial, enforceOrientation, vehicleType, diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx index 3ae301b2f..3801e4110 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx @@ -26,7 +26,6 @@ export interface PhotoCaptureHUDProps extends CameraHUDProps, Pick< PhotoCaptureAppConfig, - | 'enableSightGuidelines' | 'sightGuidelines' | 'addDamage' | 'showCloseButton' @@ -109,6 +108,10 @@ export interface PhotoCaptureHUDProps * Callback called when the user clicks on the "Validate" button of the Add Damage mode. */ onValidateVehicleParts: () => void; + /** + * Callback called when the user clicks on both: 'disable' checkbox and 'okay' button. + */ + onDisableSightGuidelines: () => void; /** * Callback called when the user clicks on the close button. If this callback is not provided, the close button is not * displayed. @@ -122,6 +125,10 @@ export interface PhotoCaptureHUDProps * The vehicle type of the inspection. */ vehicleType: VehicleType; + /** + * Boolean indicating whether the sight guidelines should be displayed. + */ + showSightGuidelines: boolean; } /** @@ -153,13 +160,14 @@ export function PhotoCaptureHUD({ images, addDamage, sightGuidelines, - enableSightGuidelines, + showSightGuidelines, currentTutorialStep, allowSkipTutorial, onNextTutorialStep, onCloseTutorial, enforceOrientation, vehicleType, + onDisableSightGuidelines, }: PhotoCaptureHUDProps) { const { t } = useTranslation(); const [showCloseModal, setShowCloseModal] = useState(false); @@ -203,9 +211,10 @@ export function PhotoCaptureHUD({ images={images} addDamage={addDamage} sightGuidelines={sightGuidelines} - enableSightGuidelines={enableSightGuidelines} + showSightGuidelines={showSightGuidelines} tutorialStep={currentTutorialStep} vehicleType={vehicleType} + onDisableSightGuidelines={onDisableSightGuidelines} /> {mode !== CaptureMode.ADD_DAMAGE_PART_SELECT && ( diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements/PhotoCaptureHUDElements.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements/PhotoCaptureHUDElements.tsx index 11c2608d0..8078e860e 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements/PhotoCaptureHUDElements.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElements/PhotoCaptureHUDElements.tsx @@ -15,7 +15,7 @@ import { CaptureMode } from '../../../types'; * Props of the PhotoCaptureHUDElements component. */ export interface PhotoCaptureHUDElementsProps - extends Pick { + extends Pick { /** * The currently selected sight in the PhotoCapture component : the sight that the user needs to capture. */ @@ -64,6 +64,10 @@ export interface PhotoCaptureHUDElementsProps * Callback called when the user clicks on the "Validate" button of the Add Damage mode. */ onValidateVehicleParts: () => void; + /** + * Callback called when the user clicks on both: 'disable' checkbox and 'okay' button. + */ + onDisableSightGuidelines?: () => void; /** * The effective pixel dimensions of the Camera video stream on the screen. */ @@ -84,6 +88,10 @@ export interface PhotoCaptureHUDElementsProps * The vehicle type of the inspection. */ vehicleType: VehicleType; + /** + * Boolean indicating whether the sight guidelines should be displayed. + */ + showSightGuidelines?: boolean; } /** @@ -106,8 +114,9 @@ export function PhotoCaptureHUDElements(params: PhotoCaptureHUDElementsProps) { images={params.images} addDamage={params.addDamage} sightGuidelines={params.sightGuidelines} - enableSightGuidelines={params.enableSightGuidelines} + showSightGuidelines={params.showSightGuidelines} tutorialStep={params.tutorialStep} + onDisableSightGuidelines={params.onDisableSightGuidelines} /> ); } diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.tsx index fcfc52897..237433e59 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/PhotoCaptureHUDElementsSight.tsx @@ -18,12 +18,13 @@ export function PhotoCaptureHUDElementsSight({ onSelectedSight = () => {}, onRetakeSight = () => {}, onAddDamage = () => {}, + onDisableSightGuidelines = () => {}, sightsTaken, previewDimensions, images, addDamage, sightGuidelines, - enableSightGuidelines, + showSightGuidelines, tutorialStep, }: PhotoCaptureHUDElementsSightProps) { const style = usePhotoCaptureHUDSightPreviewStyle({ previewDimensions }); @@ -39,8 +40,9 @@ export function PhotoCaptureHUDElementsSight({ diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.styles.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.styles.ts index d716c000c..91cb276eb 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.styles.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.styles.ts @@ -14,13 +14,29 @@ export const styles: Styles = { width: `calc(98% - (${PHOTO_CAPTURE_HUD_BUTTONS_BAR_WIDTH * 4}px))`, justifyContent: 'center', }, - button: { - textAlign: 'start', + guideline: { + display: 'flex', + flexDirection: 'column', + backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: '12px', - fontSize: 14, - flexDirection: 'row-reverse', - paddingRight: 0, - alignItems: 'start', - gap: 10, + justifyContent: 'start', + padding: '10px', + gap: '8px', + letterSpacing: '0.15px', + fontSize: '14', + }, + buttonContainer: { + display: 'flex', + justifyContent: 'space-between', + padding: '0px 10px 0px', + }, + checkbox: { + display: 'flex', + cursor: 'pointer', + gap: '5px', + }, + button: { + all: 'unset', + cursor: 'pointer', }, }; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.tsx index 0772c716d..098e5af8b 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/SightGuideline.tsx @@ -1,16 +1,15 @@ import { useEffect, useState } from 'react'; -import { Button } from '@monkvision/common-ui-web'; -import { AddDamage, PhotoCaptureAppConfig } from '@monkvision/types'; +import { PhotoCaptureAppConfig } from '@monkvision/types'; import { useTranslation } from 'react-i18next'; import { getLanguage } from '@monkvision/common'; import { styles } from './SightGuideline.styles'; -import { useColorBackground } from '../../../../hooks'; +import { useSightGuidelineStyle } from './hooks'; /** * Props of the SightGuideline component. */ export interface SightGuidelineProps - extends Pick { + extends Pick { /** * The id of the sight. */ @@ -21,6 +20,16 @@ export interface SightGuidelineProps * @default false */ enableDefaultMessage?: boolean; + /** + * Boolean indicating if the sight guidelines are enabled. + * + * @default true + */ + disabled?: boolean; + /** + * Callback called when the user clicks on both: 'disable' checkbox and 'okay' button. + */ + onDisableSightGuidelines?: () => void; } /** @@ -29,15 +38,16 @@ export interface SightGuidelineProps export function SightGuideline({ sightId, sightGuidelines, - enableSightGuidelines, addDamage, enableDefaultMessage = false, + disabled = false, + onDisableSightGuidelines = () => {}, }: SightGuidelineProps) { const [showGuideline, setShowGuideline] = useState(true); - const primaryColor = useColorBackground(); - const { i18n, t } = useTranslation(); + const [checked, setChecked] = useState(false); - const style = addDamage === AddDamage.DISABLED ? styles['containerWide'] : styles['container']; + const { i18n, t } = useTranslation(); + const style = useSightGuidelineStyle({ addDamage }); const guidelineFound = sightGuidelines?.find((value) => value.sightIds.includes(sightId)); @@ -47,19 +57,46 @@ export function SightGuideline({ const guideline = guidelineFound ? guidelineFound[getLanguage(i18n.language)] : defaultMessage; + const handleShowSightGuidelines = () => { + if (checked) { + onDisableSightGuidelines(); + } + setShowGuideline(false); + }; + useEffect(() => setShowGuideline(true), [sightId]); return ( -
- {enableSightGuidelines && showGuideline && guideline && ( - +
+
setChecked(!checked)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + setChecked(!checked); + } + }} + data-testid='checkbox-container' + > + e.stopPropagation()} /> + {t('photo.hud.guidelines.disable')} +
+ +
+
)} ); diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/hooks.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/hooks.ts new file mode 100644 index 000000000..8ead5567a --- /dev/null +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline/hooks.ts @@ -0,0 +1,19 @@ +import { AddDamage } from '@monkvision/types'; +import { styles } from './SightGuideline.styles'; +import { useColorBackground } from '../../../../hooks'; + +export interface SightGuidelineParams { + addDamage?: AddDamage; +} + +export function useSightGuidelineStyle({ addDamage }: SightGuidelineParams) { + const backgroundColor = useColorBackground(); + + return { + container: addDamage === AddDamage.DISABLED ? styles['containerWide'] : styles['container'], + guideline: { + ...styles['guideline'], + backgroundColor, + }, + }; +} diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/hooks.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/hooks.ts index cf4960f9a..8d40eb7d3 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/hooks.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/hooks.ts @@ -8,7 +8,7 @@ import { TutorialSteps } from '../../hooks'; * Props of the PhotoCaptureHUDElementsSight component. */ export interface PhotoCaptureHUDElementsSightProps - extends Pick { + extends Pick { /** * The list of sights provided to the PhotoCapture component. */ @@ -33,6 +33,10 @@ export interface PhotoCaptureHUDElementsSightProps * Callback called when the user clicks on the AddDamage button. */ onAddDamage?: () => void; + /** + * Callback called when the user clicks on both: 'disable' checkbox and 'okay' button. + */ + onDisableSightGuidelines?: () => void; /** * Array containing the list of sights that the user has already captured. */ @@ -45,6 +49,10 @@ export interface PhotoCaptureHUDElementsSightProps * The current images taken by the user (ignoring retaken pictures etc.). */ images: Image[]; + /** + * Boolean indicating whether the sight guidelines should be displayed. + */ + showSightGuidelines?: boolean; } export function usePhotoCaptureHUDSightPreviewStyle({ diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.tsx index 50263be44..2c7e64605 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDTutorial/PhotoCaptureHUDTutorial.tsx @@ -62,7 +62,7 @@ export function PhotoCaptureHUDTutorial({ diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/index.ts b/packages/inspection-capture-web/src/PhotoCapture/hooks/index.ts index 11517f16a..823329949 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/hooks/index.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/hooks/index.ts @@ -1,3 +1,4 @@ export * from './usePhotoCaptureSightState'; export * from './useComplianceAnalytics'; export * from './usePhotoCaptureTutorial'; +export * from './usePhotoCaptureSightGuidelines'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureSightGuidelines.ts b/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureSightGuidelines.ts new file mode 100644 index 000000000..7b038cea3 --- /dev/null +++ b/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureSightGuidelines.ts @@ -0,0 +1,71 @@ +import { useCallback, useState } from 'react'; +import { PhotoCaptureAppConfig, PhotoCaptureSightGuidelinesOption } from '@monkvision/types'; +import { useObjectMemo } from '@monkvision/common'; + +export const STORAGE_KEY_PHOTO_CAPTURE_GUIDELINES = '@monk_photoCaptureSightGuideline'; +export const TTL_MS = 48 * 60 * 60 * 1000; + +function isTTLExpired(): boolean { + const timestamp = localStorage.getItem(STORAGE_KEY_PHOTO_CAPTURE_GUIDELINES); + if (timestamp) { + console.log(Date.now() - parseInt(timestamp, 10) > TTL_MS); + } + + return !timestamp || Date.now() - parseInt(timestamp, 10) > TTL_MS; +} + +function getShowSightGuidelines( + enableSightGuidelines?: PhotoCaptureSightGuidelinesOption, +): boolean { + switch (enableSightGuidelines) { + case PhotoCaptureSightGuidelinesOption.DISABLED: + return false; + case PhotoCaptureSightGuidelinesOption.EPHEMERAL: + return isTTLExpired(); + default: + return true; + } +} + +/** + * Parameters of the usePhotoCaptureSightGuidelines hook. + */ +export interface PhotoCaptureSightGuidelines + extends Pick {} + +/** + * Handle returned by the usePhotoCaptureSightGuidelines hook to manage the SightGuidelines feature. + */ +export interface PhotoCaptureSightGuidelinesHandle { + /** + * Boolean indicating if the sight guidelines are enabled or not. + */ + showSightGuidelines: boolean; + /** + * Callback called when the user clicks on both: 'disable' checkbox and 'okay' button. + */ + onDisableSightGuidelines?: () => void; +} + +/** + * Custom hook used to manage the state of photo capture sight guidelines. + */ +export function usePhotoCaptureSightGuidelines({ + enableSightGuidelines, +}: PhotoCaptureSightGuidelines) { + const [showSightGuidelines, setShowSightGuidelines] = useState(() => + getShowSightGuidelines(enableSightGuidelines), + ); + + const handleDisableSightGuidelines = useCallback(() => { + if (enableSightGuidelines === PhotoCaptureSightGuidelinesOption.EPHEMERAL) { + localStorage.setItem(STORAGE_KEY_PHOTO_CAPTURE_GUIDELINES, Date.now().toString()); + setShowSightGuidelines(false); + } + if (enableSightGuidelines === PhotoCaptureSightGuidelinesOption.ENABLED) { + setShowSightGuidelines(false); + } + }, []); + + return useObjectMemo({ showSightGuidelines, handleDisableSightGuidelines }); +} diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureTutorial.ts b/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureTutorial.ts index 23a15460a..31f563761 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureTutorial.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureTutorial.ts @@ -40,12 +40,27 @@ function getTutorialState( /** * Parameters of the usePhotoCaptureTutorial hook. */ -export interface PhotoCaptureTutorial +export interface PhotoCaptureTutorialParams extends Pick< PhotoCaptureAppConfig, 'enableTutorial' | 'enableSightTutorial' | 'enableSightGuidelines' > {} +export interface HandlePhotoCaptureTutorial { + /** + * The current tutorial step in PhotoCapture component. + */ + currentTutorialStep: TutorialSteps | null; + /** + * Callback called when the user clicks on the "Next" button in PhotoCapture tutorial. + */ + goToNextTutorialStep: () => void; + /** + * Callback called when the user clicks on the "Close" button in PhotoCapture tutorial. + */ + closeTutorial: () => void; +} + /** * Custom hook used to manage the state of photo capture tutorial. */ @@ -53,7 +68,7 @@ export function usePhotoCaptureTutorial({ enableTutorial, enableSightTutorial, enableSightGuidelines, -}: PhotoCaptureTutorial) { +}: PhotoCaptureTutorialParams): HandlePhotoCaptureTutorial { const [currentTutorialStep, setCurrentTutorialStep] = useState( getTutorialState(enableTutorial), ); diff --git a/packages/inspection-capture-web/src/translations/de.json b/packages/inspection-capture-web/src/translations/de.json index 56ac444e3..bb79facf8 100644 --- a/packages/inspection-capture-web/src/translations/de.json +++ b/packages/inspection-capture-web/src/translations/de.json @@ -31,7 +31,9 @@ "confirm": "Ja" }, "guidelines": { - "defaultGuideline": "Blickwinkelanzeigen für Bilder können hier angezeigt werden." + "defaultGuideline": "Blickwinkelanzeigen für Bilder können hier angezeigt werden.", + "disable": "Nicht mehr anzeigen", + "validate": "Okay" }, "tutorial": { "welcome": "Willkommen zu Ihrer ersten Inspektion!", diff --git a/packages/inspection-capture-web/src/translations/en.json b/packages/inspection-capture-web/src/translations/en.json index 32fa8fa26..0bb1ced68 100644 --- a/packages/inspection-capture-web/src/translations/en.json +++ b/packages/inspection-capture-web/src/translations/en.json @@ -31,7 +31,9 @@ "confirm": "Yes" }, "guidelines": { - "defaultGuideline": "Picture view point indications can be displayed here." + "defaultGuideline": "Picture view point indications can be displayed here.", + "disable": "Don't show this again", + "validate": "Okay" }, "tutorial": { "welcome": "Welcome to your first inspection!", diff --git a/packages/inspection-capture-web/src/translations/fr.json b/packages/inspection-capture-web/src/translations/fr.json index 4ceadefe9..9baf7d584 100644 --- a/packages/inspection-capture-web/src/translations/fr.json +++ b/packages/inspection-capture-web/src/translations/fr.json @@ -31,7 +31,9 @@ "confirm": "Oui" }, "guidelines": { - "defaultGuideline": "Les indications de point de vue de l'image peuvent être affichées ici." + "defaultGuideline": "Les indications de point de vue de l'image peuvent être affichées ici.", + "disable": "Ne plus afficher", + "validate": "D'accord" }, "tutorial": { "welcome": "Bienvenue à votre première inspection!", diff --git a/packages/inspection-capture-web/src/translations/nl.json b/packages/inspection-capture-web/src/translations/nl.json index 4f61debb9..410062fb1 100644 --- a/packages/inspection-capture-web/src/translations/nl.json +++ b/packages/inspection-capture-web/src/translations/nl.json @@ -31,7 +31,9 @@ "confirm": "Ja" }, "guidelines": { - "defaultGuideline": "Afbeeldingsstandpuntaanwijzingen kunnen hier worden weergegeven." + "defaultGuideline": "Afbeeldingsstandpuntaanwijzingen kunnen hier worden weergegeven.", + "disable": "Niet meer tonen", + "validate": "Oké" }, "tutorial": { "welcome": "Welkom bij uw eerste inspectie!", diff --git a/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDislosureHUD.test.tsx b/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDislosureHUD.test.tsx index 9ccfcf079..fd5c86b5e 100644 --- a/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDislosureHUD.test.tsx +++ b/packages/inspection-capture-web/test/DamageDisclosure/DamageDisclosureHUD/DamageDislosureHUD.test.tsx @@ -1,5 +1,3 @@ -import { Image, ImageStatus, VehicleType } from '@monkvision/types'; - jest.mock('../../../src/components/HUDButtons', () => ({ HUDButtons: jest.fn(() => <>), })); @@ -10,6 +8,7 @@ jest.mock('../../../src/DamageDisclosure/DamageDisclosureHUD/DamageDisclosureHUD import { useTranslation } from 'react-i18next'; import { act, render, screen } from '@testing-library/react'; import { LoadingState } from '@monkvision/common'; +import { Image, ImageStatus, VehicleType } from '@monkvision/types'; import { CameraHandle } from '@monkvision/camera-web'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { BackdropDialog } from '@monkvision/common-ui-web'; diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx index c92b23ac1..7b4e61467 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx @@ -6,6 +6,7 @@ import { CompressionFormat, DeviceOrientation, PhotoCaptureTutorialOption, + PhotoCaptureSightGuidelinesOption, TaskName, VehicleType, } from '@monkvision/types'; @@ -16,7 +17,11 @@ import { useMonitoring } from '@monkvision/monitoring'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { act, render, waitFor } from '@testing-library/react'; import { PhotoCapture, PhotoCaptureHUD, PhotoCaptureProps } from '../../src'; -import { usePhotoCaptureSightState, usePhotoCaptureTutorial } from '../../src/PhotoCapture/hooks'; +import { + usePhotoCaptureSightState, + usePhotoCaptureTutorial, + usePhotoCaptureSightGuidelines, +} from '../../src/PhotoCapture/hooks'; import { useStartTasksOnComplete, useAdaptiveCameraConfig, @@ -47,6 +52,9 @@ jest.mock('../../src/PhotoCapture/hooks', () => ({ goToNextTutorialStep: jest.fn(), closeTutorial: jest.fn(), })), + usePhotoCaptureSightGuidelines: jest.fn(() => ({ + showSightGuidelines: true, + })), })); jest.mock('../../src/hooks', () => ({ @@ -118,7 +126,7 @@ function createProps(): PhotoCaptureProps { allowSkipRetake: true, addDamage: AddDamage.PART_SELECT, maxUploadDurationWarning: 456, - enableSightGuidelines: true, + enableSightGuidelines: PhotoCaptureSightGuidelinesOption.ENABLED, sightGuidelines: [ { en: 'en-test', @@ -325,6 +333,7 @@ describe('PhotoCapture component', () => { expect(usePhotoCaptureImages).toHaveBeenCalledWith(props.inspectionId); const images = (usePhotoCaptureImages as jest.Mock).mock.results[0].value; const tutorial = (usePhotoCaptureTutorial as jest.Mock).mock.results[0].value; + const sightGuidelines = (usePhotoCaptureSightGuidelines as jest.Mock).mock.results[0].value; expectPropsOnChildMock(Camera, { hudProps: { sights: props.sights, @@ -343,7 +352,6 @@ describe('PhotoCapture component', () => { onOpenGallery: expect.any(Function), images, addDamage: props.addDamage, - enableSightGuidelines: props.enableSightGuidelines, sightGuidelines: props.sightGuidelines, currentTutorialStep: tutorial.currentTutorialStep, onNextTutorialStep: tutorial.goToNextTutorialStep, @@ -351,6 +359,7 @@ describe('PhotoCapture component', () => { allowSkipTutorial: props.allowSkipRetake, enforceOrientation: props.enforceOrientation, vehicleType: props.vehicleType, + showSightGuidelines: sightGuidelines.showSightGuidelines, }, }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx index 9cf632914..19ffaeb24 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx @@ -63,6 +63,8 @@ function createProps(): PhotoCaptureHUDProps { onValidateVehicleParts: jest.fn(), vehicleParts: [], vehicleType: VehicleType.SEDAN, + showSightGuidelines: true, + onDisableSightGuidelines: jest.fn(), }; } 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..4bfec7923 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 @@ -1,25 +1,41 @@ -import { render } from '@testing-library/react'; -import { useTranslation } from 'react-i18next'; -import { expectPropsOnChildMock } from '@monkvision/test-utils'; -import { Button } from '@monkvision/common-ui-web'; -import { SightGuideline } from '../../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline'; +import '@testing-library/jest-dom'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { getLanguage } from '@monkvision/common'; +import { AddDamage } from '@monkvision/types'; +import { + SightGuideline, + SightGuidelineProps, +} from '../../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDElementsSight/SightGuideline'; -function createProps() { +const GUIDELINE_TEST_ID = 'guideline'; +const CONTAINER_TEST_ID = 'container'; +const CHECKBOX_CONTAINER_TEST_ID = 'checkbox-container'; +const BUTTON_TEST_ID = 'button'; + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { + language: 'en', + }, + }), +})); +function createProps(): SightGuidelineProps { return { sightId: 'sightId-test-1', - guidelines: [ + sightGuidelines: [ { en: 'en-test', fr: 'fr-test', de: 'de-test', nl: 'nl-test', sightIds: ['sightId-test-1', 'sightId-test-2'], - information: 'info-test', }, ], - enableAddDamage: true, - enableSightGuidelines: true, + addDamage: AddDamage.PART_SELECT, + disabled: false, + enableDefaultMessage: false, + onDisableSightGuidelines: jest.fn(), }; } @@ -28,34 +44,49 @@ describe('SightGuideline component', () => { jest.clearAllMocks(); }); - it('should be display when enableSightGuideline is false', () => { - const props = createProps(); + it('should hide the guideline when disabled is true', () => { + const props = { ...createProps(), disabled: true }; (getLanguage as jest.Mock).mockImplementationOnce(() => 'en'); - (useTranslation as jest.Mock).mockImplementationOnce(() => ({ i18n: { language: 'en' } })); - const { unmount } = render( - , - ); - expect(Button).not.toHaveBeenCalled(); + const { unmount } = render(); + expect(screen.getByTestId(CONTAINER_TEST_ID).children.length).toBe(0); unmount(); }); - it('should display a Button with the proper label', () => { + it('should render the guideline when disabled is false', () => { const props = createProps(); (getLanguage as jest.Mock).mockImplementationOnce(() => 'en'); - const { unmount } = render( - , + const { unmount } = render(); + expect(screen.getByTestId(GUIDELINE_TEST_ID)).toBeInTheDocument(); + + unmount(); + }); + + it('should render the default message if no guideline is found', () => { + const props = { ...createProps(), enableDefaultMessage: true, sightGuidelines: [] }; + const { unmount } = render(); + expect(screen.getByTestId(GUIDELINE_TEST_ID)).toHaveTextContent( + 'photo.hud.guidelines.defaultGuideline', ); - expectPropsOnChildMock(Button, { children: props.guidelines[0].en }); + unmount(); + }); + + it('should call onDisableSightGuidelines when checkbox is checked and button is clicked', async () => { + const props = createProps(); + (getLanguage as jest.Mock).mockImplementation(() => 'en'); + const { unmount } = render(); + const checkbox = screen.getByTestId(CHECKBOX_CONTAINER_TEST_ID); + const button = screen.getByTestId(BUTTON_TEST_ID); + + fireEvent.click(checkbox); + await waitFor(() => { + expect(checkbox).toHaveAttribute('aria-checked', 'true'); // Ensure the checkbox is checked + }); + + fireEvent.click(button); + + expect(props.onDisableSightGuidelines).toHaveBeenCalled(); unmount(); }); 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 b5822c0ee..67a0c7411 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 @@ -58,7 +58,7 @@ describe('PhotoCaptureHUDTutorial component', () => { expectPropsOnChildMock(SightGuideline, { sightId: props.sightId, sightGuidelines: props.sightGuidelines, - enableSightGuidelines: props.currentTutorialStep === TutorialSteps.GUIDELINE, + disabled: props.currentTutorialStep !== TutorialSteps.GUIDELINE, addDamage: AddDamage.PART_SELECT, }); @@ -74,20 +74,20 @@ describe('PhotoCaptureHUDTutorial component', () => { const props = createProps(); const { unmount, rerender } = render(); - expectPropsOnChildMock(SightGuideline, { enableSightGuidelines: false }); + expectPropsOnChildMock(SightGuideline, { disabled: true }); rerender( , ); - expectPropsOnChildMock(SightGuideline, { enableSightGuidelines: true }); + expectPropsOnChildMock(SightGuideline, { disabled: false }); rerender( , ); - expectPropsOnChildMock(SightGuideline, { enableSightGuidelines: false }); + expectPropsOnChildMock(SightGuideline, { disabled: true }); rerender(); - expectPropsOnChildMock(SightGuideline, { enableSightGuidelines: false }); + expectPropsOnChildMock(SightGuideline, { disabled: true }); unmount(); }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureSightGuidelines.test.ts b/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureSightGuidelines.test.ts new file mode 100644 index 000000000..bed1db68c --- /dev/null +++ b/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureSightGuidelines.test.ts @@ -0,0 +1,86 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { + STORAGE_KEY_PHOTO_CAPTURE_GUIDELINES, + TTL_MS, + usePhotoCaptureSightGuidelines, +} from '../../../src/PhotoCapture/hooks'; +import { PhotoCaptureSightGuidelinesOption } from '@monkvision/types'; + +describe('usePhotoCaptureSightGuidelines', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('should return showSightGuidelines as true when enableSightGuidelines is ENABLED', () => { + const { result } = renderHook(() => + usePhotoCaptureSightGuidelines({ + enableSightGuidelines: PhotoCaptureSightGuidelinesOption.ENABLED, + }), + ); + + expect(result.current.showSightGuidelines).toBe(true); + }); + + it('should return showSightGuidelines as false when enableSightGuidelines is DISABLED', () => { + const { result } = renderHook(() => + usePhotoCaptureSightGuidelines({ + enableSightGuidelines: PhotoCaptureSightGuidelinesOption.DISABLED, + }), + ); + + expect(result.current.showSightGuidelines).toBe(false); + }); + + it('should return showSightGuidelines as false when enableSightGuidelines is EPHEMERAL and TTL is not expired', () => { + localStorage.setItem(STORAGE_KEY_PHOTO_CAPTURE_GUIDELINES, (Date.now() - 5000).toString()); + + const { result } = renderHook(() => + usePhotoCaptureSightGuidelines({ + enableSightGuidelines: PhotoCaptureSightGuidelinesOption.EPHEMERAL, + }), + ); + + expect(result.current.showSightGuidelines).toBe(false); + }); + + it('should return showSightGuidelines as true when enableSightGuidelines is EPHEMERAL and TTL is expired', () => { + localStorage.setItem(STORAGE_KEY_PHOTO_CAPTURE_GUIDELINES, (Date.now() - TTL_MS).toString()); + + const { result } = renderHook(() => + usePhotoCaptureSightGuidelines({ + enableSightGuidelines: PhotoCaptureSightGuidelinesOption.EPHEMERAL, + }), + ); + + expect(result.current.showSightGuidelines).toBe(true); + }); + + it('should disable sight guidelines when onDisableSightGuidelines is called and enableSightGuidelines is EPHEMERAL', () => { + const { result } = renderHook(() => + usePhotoCaptureSightGuidelines({ + enableSightGuidelines: PhotoCaptureSightGuidelinesOption.EPHEMERAL, + }), + ); + + act(() => { + result.current.handleDisableSightGuidelines(); + }); + + expect(result.current.showSightGuidelines).toBe(false); + expect(localStorage.getItem(STORAGE_KEY_PHOTO_CAPTURE_GUIDELINES)).toBeTruthy(); + }); + + it('should disable sight guidelines when onDisableSightGuidelines is called and enableSightGuidelines is ENABLED', () => { + const { result } = renderHook(() => + usePhotoCaptureSightGuidelines({ + enableSightGuidelines: PhotoCaptureSightGuidelinesOption.ENABLED, + }), + ); + + act(() => { + result.current.handleDisableSightGuidelines(); + }); + + expect(result.current.showSightGuidelines).toBe(false); + }); +}); diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 6d91f593a..15af77c59 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -19,20 +19,37 @@ export enum CaptureWorkflow { VIDEO = 'video', } +/** + * Enumeration of the Sight guidelines options. + */ +export enum PhotoCaptureSightGuidelinesOption { + /** + * Sight guidelines disabled. + */ + DISABLED = 'disabled', + /** + * Sight guidelines enabled. + */ + ENABLED = 'enabled', + /** + * Sight guidelines is enabled only during ephemeral time. + */ + EPHEMERAL = 'ephemeral', +} /** * Enumeration of the tutorial options. */ export enum PhotoCaptureTutorialOption { /** - * Photo capture is disabled. + * Photo capture tutorial is disabled. */ DISABLED = 'disabled', /** - * Photo capture is enabled. + * Photo capture tutorial is enabled. */ ENABLED = 'enabled', /** - * Photo capture is enabled only time. + * Photo capture tutorial is enabled only time. */ FIRST_TIME_ONLY = 'first_time_only', } @@ -204,12 +221,11 @@ export type PhotoCaptureAppConfig = SharedCaptureAppConfig & */ sightGuidelines?: SightGuideline[]; /** - * Boolean indicating whether the sight guideline feature is enabled. If disabled, the guideline text will be - * hidden. + * Option for displaying the Sight guidelines. If disabled, the guideline text will be hidden. * - * @default true + * @default SightGuidelinesOption.EPHMERAL. */ - enableSightGuidelines?: boolean; + enableSightGuidelines?: PhotoCaptureSightGuidelinesOption; /** * The default vehicle type used if no vehicle type is defined. */