From 41177b97a5b0d88e643f9ef61dfe6d5fd3e75bf5 Mon Sep 17 00:00:00 2001 From: Samy Ouyahia <103439265+souyahia-monk@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:36:53 +0200 Subject: [PATCH] Fix SightSlider icons & some Drive fixes (#728) * Disable login and inspection creation in Drive app * Added HinL task in create inspection in Drive App * Fixed SightSlider status icons --- .../CreateInspectionPage.module.css | 1 + apps/drive-app/.env-cmdrc.json | 1 + .../CreateInspectionPage.module.css | 6 +- .../CreateInspectionPage.tsx | 51 ++++-- .../src/pages/LogInPage/LogInPage.tsx | 12 +- apps/drive-app/src/translations/de.json | 6 +- apps/drive-app/src/translations/en.json | 6 +- apps/drive-app/src/translations/fr.json | 6 +- .../test/pages/CreateInspectionPage.test.tsx | 57 +------ apps/drive-app/test/pages/LogInPage.test.tsx | 148 +----------------- .../src/PhotoCapture/PhotoCapture.tsx | 3 + .../PhotoCaptureHUD/PhotoCaptureHUD.tsx | 21 ++- .../PhotoCaptureHUDPreview.tsx | 12 +- .../PhotoCaptureHUDPreviewSight.tsx | 14 +- .../SightSlider/SightSlider.tsx | 26 ++- .../PhotoCaptureHUD/hooks/index.ts | 1 - .../hooks/useComplianceNotification.ts | 19 --- .../src/PhotoCapture/hooks/index.ts | 1 + .../hooks/usePhotoCaptureImages.ts | 15 ++ .../test/PhotoCapture/PhotoCapture.test.tsx | 36 +++-- .../PhotoCaptureHUD/PhotoCaptureHUD.test.tsx | 34 ++-- .../PhotoCaptureHUDPreview.test.tsx | 8 +- .../PhotoCaptureHUDPreviewSight.test.tsx | 9 +- .../SightSlider/SightSlider.test.tsx | 37 +---- .../hooks/useComplianceNotification.test.ts | 98 ------------ .../hooks/usePhotoCaptureImages.test.ts | 24 +++ .../network/src/api/inspection/mappers.ts | 60 ++++++- packages/network/src/api/models/task.ts | 8 +- 28 files changed, 291 insertions(+), 429 deletions(-) delete mode 100644 packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/hooks/useComplianceNotification.ts create mode 100644 packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureImages.ts delete mode 100644 packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/hooks/useComplianceNotification.test.ts create mode 100644 packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureImages.test.ts diff --git a/apps/demo-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css b/apps/demo-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css index 8b5fde5ec..d6d8803dc 100644 --- a/apps/demo-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css +++ b/apps/demo-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css @@ -4,6 +4,7 @@ display: flex; align-items: center; justify-content: center; + flex-direction: column; } .error-message { diff --git a/apps/drive-app/.env-cmdrc.json b/apps/drive-app/.env-cmdrc.json index e884c01aa..5e7484998 100644 --- a/apps/drive-app/.env-cmdrc.json +++ b/apps/drive-app/.env-cmdrc.json @@ -3,6 +3,7 @@ "PORT": "17200", "HTTPS": "true", "ESLINT_NO_DEV_ERRORS": "true", + "REACT_APP_ALLOW_CREATE_INSPECTION": "true", "REACT_APP_ENVIRONMENT": "local", "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", diff --git a/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css b/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css index 8b5fde5ec..902f8b37f 100644 --- a/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css +++ b/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.module.css @@ -4,9 +4,13 @@ display: flex; align-items: center; justify-content: center; + flex-direction: column; } .error-message { text-align: center; - padding-bottom: 20px; +} + +.retry-btn-container { + padding-top: 20px; } diff --git a/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.tsx b/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.tsx index befea8dd7..4bbf944b3 100644 --- a/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.tsx +++ b/apps/drive-app/src/pages/CreateInspectionPage/CreateInspectionPage.tsx @@ -2,13 +2,35 @@ import { useTranslation } from 'react-i18next'; import { useEffect } from 'react'; import { Navigate } from 'react-router-dom'; import { Button, Spinner } from '@monkvision/common-ui-web'; -import { useMonkApi } from '@monkvision/network'; +import { CreateInspectionOptions, useMonkApi } from '@monkvision/network'; import { getEnvOrThrow, useLoadingState, useMonkAppParams } from '@monkvision/common'; import { useMonitoring } from '@monkvision/monitoring'; import { TaskName } from '@monkvision/types'; import { Page } from '../pages'; import styles from './CreateInspectionPage.module.css'; +const options: CreateInspectionOptions = { + tasks: [ + TaskName.DAMAGE_DETECTION, + TaskName.WHEEL_ANALYSIS, + { + name: TaskName.HUMAN_IN_THE_LOOP, + callbacks: [ + { + url: 'https://webhook.site/15f8682f-91a8-4df0-8e73-74adc6c74ca4', + headers: {}, + params: {}, + }, + ], + }, + ], +}; + +enum CreateInspectionPageError { + CREATE_INSPECTION = 'create-inspection.errors.create-inspection', + MISSING_INSPECTION_ID = 'create-inspection.errors.missing-inspection-id', +} + export function CreateInspectionPage() { const loading = useLoadingState(); const { t } = useTranslation(); @@ -21,21 +43,26 @@ export function CreateInspectionPage() { const handleCreateInspection = () => { loading.start(); - createInspection({ tasks: [TaskName.DAMAGE_DETECTION, TaskName.WHEEL_ANALYSIS] }) + createInspection(options) .then((res) => { loading.onSuccess(); setInspectionId(res.id); }) .catch((err) => { - loading.onError(err); + loading.onError(CreateInspectionPageError.CREATE_INSPECTION); handleError(err); }); }; useEffect(() => { if (!inspectionId) { - loading.start(); - handleCreateInspection(); + if (process.env['REACT_APP_ALLOW_CREATE_INSPECTION'] === 'true') { + // On local environment, we allow creating the inspection if the ID is not provided. + loading.start(); + handleCreateInspection(); + } else { + loading.onError(CreateInspectionPageError.MISSING_INSPECTION_ID); + } } }, [inspectionId]); @@ -48,12 +75,14 @@ export function CreateInspectionPage() { {loading.isLoading && } {!loading.isLoading && loading.error && ( <> -
- {t('create-inspection.errors.create-inspection')} -
- +
{t(loading.error)}
+ {loading.error === CreateInspectionPageError.CREATE_INSPECTION && ( +
+ +
+ )} )} diff --git a/apps/drive-app/src/pages/LogInPage/LogInPage.tsx b/apps/drive-app/src/pages/LogInPage/LogInPage.tsx index 5b5fd5cb1..f8d8f63a6 100644 --- a/apps/drive-app/src/pages/LogInPage/LogInPage.tsx +++ b/apps/drive-app/src/pages/LogInPage/LogInPage.tsx @@ -18,6 +18,8 @@ function getLoginErrorMessage(err: unknown): string { return 'login.errors.unknown'; } +const allowCreateInspection = process.env['REACT_APP_ALLOW_CREATE_INSPECTION'] === 'true'; + export function LogInPage() { const [isExpired, setIsExpired] = useState(false); const loading = useLoadingState(); @@ -28,6 +30,11 @@ export function LogInPage() { const navigate = useNavigate(); useEffect(() => { + if (!authToken && !allowCreateInspection) { + setAuthToken(null); + setIsExpired(false); + loading.onError('login.errors.missing-token'); + } if (authToken && !isUserAuthorized(authToken, REQUIRED_AUTHORIZATIONS)) { loading.onError('login.errors.insufficient-authorization'); } @@ -62,11 +69,12 @@ export function LogInPage() {
{t('login.errors.token-expired')}
)} {loading.error &&
{t(loading.error)}
} - {authToken ? ( + {authToken && allowCreateInspection && ( - ) : ( + )} + {!authToken && allowCreateInspection && ( diff --git a/apps/drive-app/src/translations/de.json b/apps/drive-app/src/translations/de.json index 63276e924..5d0593a5b 100644 --- a/apps/drive-app/src/translations/de.json +++ b/apps/drive-app/src/translations/de.json @@ -8,13 +8,15 @@ "popup-closed": "Huch! Wir konnten Sie nicht anmelden, weil das Popup geschlossen wurde. Versuchen wir es noch einmal!", "token-expired": "Ihr Authentifizierungstoken ist abgelaufen. Bitte melden Sie sich erneut an.", "insufficient-authorization": "Sie haben nicht die erforderlichen Berechtigungen, um diese Anwendung zu nutzen. Bitte melden Sie sich ab und verwenden Sie ein anderes Konto.", - "unknown": "Huch! Beim Einloggen ist ein unerwarteter Fehler aufgetreten. Versuchen wir es noch einmal!" + "unknown": "Huch! Beim Einloggen ist ein unerwarteter Fehler aufgetreten. Versuchen wir es noch einmal!", + "missing-token": "Die URL, auf die Sie zuzugreifen versuchen, ist ungültig." } }, "create-inspection": { "errors": { "retry": "Wiederholung", - "create-inspection": "Bei der Erstellung der Inspektion ist ein unerwarteter Fehler aufgetreten." + "create-inspection": "Bei der Erstellung der Inspektion ist ein unerwarteter Fehler aufgetreten.", + "missing-inspection-id": "Die URL, auf die Sie zuzugreifen versuchen, ist ungültig." } }, "inspection-complete": { diff --git a/apps/drive-app/src/translations/en.json b/apps/drive-app/src/translations/en.json index 2bb81663f..f023597ee 100644 --- a/apps/drive-app/src/translations/en.json +++ b/apps/drive-app/src/translations/en.json @@ -8,13 +8,15 @@ "popup-closed": "Oops! We couldn't log you in because the popup was closed. Let's try again!", "token-expired": "Your authentication token is expired. Please log-in again.", "insufficient-authorization": "You do not have the required authorizations to use this application. Please log out and use a different account.", - "unknown": "Oops! An unexpected error occurred during the log in. Let's try again!" + "unknown": "Oops! An unexpected error occurred during the log in. Let's try again!", + "missing-token": "The URL you are trying to access is invalid." } }, "create-inspection": { "errors": { "retry": "Retry", - "create-inspection": "An unexpected error occurred while creating the inspection." + "create-inspection": "An unexpected error occurred while creating the inspection.", + "missing-inspection-id": "The URL you are trying to access is invalid." } }, "inspection-complete": { diff --git a/apps/drive-app/src/translations/fr.json b/apps/drive-app/src/translations/fr.json index 36b3ec0c7..72669631f 100644 --- a/apps/drive-app/src/translations/fr.json +++ b/apps/drive-app/src/translations/fr.json @@ -8,13 +8,15 @@ "popup-closed": "Oups ! Nous n'avons pas pu vous connecter car la pop-up s'est fermée. Essayons à nouveau !", "token-expired": "Votre token d'authentification est expiré. Veuillez vous reconnecter.", "insufficient-authorization": "Vous n'avez pas les autorisations nécessaires pour utiliser cette application. Veuillez vous déconnecter et utiliser un autre compte.", - "unknown": "Oups ! Une erreur inattendue est survenue lors de la connection. Essayons à nouveau !" + "unknown": "Oups ! Une erreur inattendue est survenue lors de la connection. Essayons à nouveau !", + "missing-token": "L'URL à laquelle vous essayez d'accéder est invalide." } }, "create-inspection": { "errors": { "retry": "Réessayer", - "create-inspection": "Une erreur inattendue est survenue lors de la création de l'inspection." + "create-inspection": "Une erreur inattendue est survenue lors de la création de l'inspection.", + "missing-inspection-id": "L'URL à laquelle vous essayez d'accéder est invalide." } }, "inspection-complete": { diff --git a/apps/drive-app/test/pages/CreateInspectionPage.test.tsx b/apps/drive-app/test/pages/CreateInspectionPage.test.tsx index 051f5a844..4fd316a3d 100644 --- a/apps/drive-app/test/pages/CreateInspectionPage.test.tsx +++ b/apps/drive-app/test/pages/CreateInspectionPage.test.tsx @@ -1,11 +1,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import { Navigate } from 'react-router-dom'; -import { act } from 'react-dom/test-utils'; import { useLoadingState, useMonkAppParams } from '@monkvision/common'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; -import { useMonkApi } from '@monkvision/network'; -import { TaskName } from '@monkvision/types'; -import { Button } from '@monkvision/common-ui-web'; import { CreateInspectionPage, Page } from '../../src/pages'; const appParams = { @@ -31,63 +27,18 @@ describe('CreateInspection page', () => { unmount(); }); - it('should create the inspection and then redirect to the PhotoCapture page if the inspectionId is not defined', async () => { - const id = 'test-id-test'; - const createInspection = jest.fn(() => Promise.resolve({ id })); - (useMonkApi as jest.Mock).mockImplementation(() => ({ createInspection })); - (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); - const { unmount } = render(); - - expect(createInspection).toHaveBeenCalledWith({ - tasks: [TaskName.DAMAGE_DETECTION, TaskName.WHEEL_ANALYSIS], - }); - await waitFor(() => { - expect(appParams.setInspectionId).toHaveBeenCalledWith(id); - }); - - unmount(); - }); - - it('should display an error message if the API call fails', async () => { - const createInspection = jest.fn(() => Promise.reject()); - (useMonkApi as jest.Mock).mockImplementation(() => ({ createInspection })); - (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); + it('should display an error message if the inspection ID is not defined', async () => { + (useMonkAppParams as jest.Mock).mockImplementation(() => ({ inspectionId: null })); const onError = jest.fn(); const error = 'test-error'; (useLoadingState as jest.Mock).mockImplementation(() => ({ onError, error, start: jest.fn() })); const { unmount } = render(); await waitFor(() => { - expect(appParams.setInspectionId).not.toHaveBeenCalled(); - expect(onError).toHaveBeenCalled(); - expect(screen.queryByText('create-inspection.errors.create-inspection')).not.toBeNull(); + expect(onError).toHaveBeenCalledWith('create-inspection.errors.missing-inspection-id'); + expect(screen.getByText(error)).not.toBeNull(); }); unmount(); }); - - it('should display a retry button if the API call fails', async () => { - const createInspection = jest.fn(() => Promise.reject()); - (useMonkApi as jest.Mock).mockImplementation(() => ({ createInspection })); - (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); - (useLoadingState as jest.Mock).mockImplementation(() => ({ - onError: jest.fn(), - error: 'test-error', - start: jest.fn(), - })); - const { unmount } = render(); - - expectPropsOnChildMock(Button, { - variant: 'outline', - icon: 'refresh', - onClick: expect.any(Function), - }); - const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; - - createInspection.mockClear(); - act(() => onClick()); - expect(createInspection).toHaveBeenCalled(); - - unmount(); - }); }); diff --git a/apps/drive-app/test/pages/LogInPage.test.tsx b/apps/drive-app/test/pages/LogInPage.test.tsx index 1ab2088e2..6b274f003 100644 --- a/apps/drive-app/test/pages/LogInPage.test.tsx +++ b/apps/drive-app/test/pages/LogInPage.test.tsx @@ -1,11 +1,7 @@ -import { act } from 'react-dom/test-utils'; import { useNavigate } from 'react-router-dom'; import { render, screen, waitFor } from '@testing-library/react'; import { useLoadingState, useMonkAppParams } from '@monkvision/common'; -import { isTokenExpired, isUserAuthorized, useAuth } from '@monkvision/network'; -import { expectPropsOnChildMock } from '@monkvision/test-utils'; -import { Button } from '@monkvision/common-ui-web'; -import { LogInPage, Page } from '../../src/pages'; +import { LogInPage } from '../../src/pages'; const appParams = { authToken: 'test-auth-token', @@ -18,160 +14,22 @@ describe('Log In page', () => { jest.clearAllMocks(); }); - it('should display a login button on the screen', async () => { + it('should display an error message if the token is not defined', async () => { (useMonkAppParams as jest.Mock).mockImplementation(() => ({ ...appParams, authToken: null })); - const { unmount } = render(); - - expectPropsOnChildMock(Button, { - onClick: expect.any(Function), - children: 'login.actions.log-in', - }); - const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; - expect(useAuth).toHaveBeenCalled(); - const { login } = (useAuth as jest.Mock).mock.results[0].value; - - act(() => onClick()); - expect(login).toHaveBeenCalled(); - - unmount(); - }); - - it('should redirect to the PhotoCapture page after the login if the inspectionId is defined', async () => { - (useMonkAppParams as jest.Mock).mockImplementation(() => ({ ...appParams, authToken: null })); - const { unmount } = render(); - - expect(Button).toHaveBeenCalled(); - const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; - expect(useNavigate).toHaveBeenCalled(); - const navigate = (useNavigate as jest.Mock).mock.results[0].value; - - act(() => onClick()); - await waitFor(() => { - expect(navigate).toHaveBeenCalledWith(Page.PHOTO_CAPTURE); - }); - - unmount(); - }); - - it('should redirect to the CreateInspection page after the login if the inspectionId is not defined', async () => { - (useMonkAppParams as jest.Mock).mockImplementation(() => ({ - ...appParams, - authToken: null, - inspectionId: null, - })); - const { unmount } = render(); - - expect(Button).toHaveBeenCalled(); - const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; - expect(useNavigate).toHaveBeenCalled(); - const navigate = (useNavigate as jest.Mock).mock.results[0].value; - - act(() => onClick()); - await waitFor(() => { - expect(navigate).toHaveBeenCalledWith(Page.CREATE_INSPECTION); - }); - - unmount(); - }); - - it('should not redirect after log in if the user does not have sufficient authorization', async () => { - (useMonkAppParams as jest.Mock).mockImplementation(() => ({ ...appParams, authToken: null })); - (isUserAuthorized as jest.Mock).mockImplementation(() => false); const onError = jest.fn(); const error = 'test-error'; (useLoadingState as jest.Mock).mockImplementation(() => ({ onError, error, start: jest.fn() })); const { unmount } = render(); - expect(Button).toHaveBeenCalled(); - const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; expect(useNavigate).toHaveBeenCalled(); const navigate = (useNavigate as jest.Mock).mock.results[0].value; - act(() => onClick()); await waitFor(() => { - expect(onError).toHaveBeenCalledWith('login.errors.insufficient-authorization'); + expect(onError).toHaveBeenCalledWith('login.errors.missing-token'); expect(navigate).not.toHaveBeenCalled(); expect(screen.queryByText(error)).not.toBeNull(); }); unmount(); }); - - it('should display an error message and a log out button if the user does not have sufficient authorizations', async () => { - (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); - (isUserAuthorized as jest.Mock).mockImplementation(() => false); - const onError = jest.fn(); - const error = 'test-error'; - (useLoadingState as jest.Mock).mockImplementation(() => ({ onError, error })); - const { unmount } = render(); - - expect(onError).toHaveBeenCalledWith('login.errors.insufficient-authorization'); - expect(screen.queryByText(error)).not.toBeNull(); - - expectPropsOnChildMock(Button, { - primaryColor: 'alert', - onClick: expect.any(Function), - children: 'login.actions.log-out', - }); - const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; - expect(useAuth).toHaveBeenCalled(); - const { logout } = (useAuth as jest.Mock).mock.results[0].value; - expect(logout).not.toHaveBeenCalled(); - - await act(() => onClick()); - expect(logout).toHaveBeenCalled(); - - unmount(); - }); - - it('should display an error message on the screen if the token was expired', async () => { - (useMonkAppParams as jest.Mock).mockImplementation(() => appParams); - (isTokenExpired as jest.Mock).mockImplementation(() => true); - const { unmount } = render(); - - expect(isTokenExpired).toHaveBeenCalledWith(appParams.authToken); - expect(appParams.setAuthToken).toHaveBeenCalledWith(null); - expect(screen.queryByText('login.errors.token-expired')).not.toBeNull(); - - unmount(); - }); - - [ - { - testCase: 'when the user closes the log in pop up', - err: new Error('Popup closed'), - label: 'login.errors.popup-closed', - }, - { - testCase: 'when an unexpected error occurrs during the log in', - err: new Error(), - label: 'login.errors.unknown', - }, - ].forEach(({ testCase, err, label }) => { - it(`should not redirect and display the proper error message ${testCase}`, async () => { - (useMonkAppParams as jest.Mock).mockImplementation(() => ({ ...appParams, authToken: null })); - (useAuth as jest.Mock).mockImplementation(() => ({ - login: jest.fn(() => Promise.reject(err)), - })); - const onError = jest.fn(); - const error = 'test-error'; - (useLoadingState as jest.Mock).mockImplementation(() => ({ - onError, - error, - start: jest.fn(), - })); - const { unmount } = render(); - - expect(Button).toHaveBeenCalled(); - const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; - - act(() => onClick()); - await waitFor(() => { - expect(onError).toHaveBeenCalledWith(label); - expect(screen.queryByText(error)).not.toBeNull(); - }); - - unmount(); - }); - }); }); diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx index 1676d2cee..1e311cb13 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx @@ -13,6 +13,7 @@ import { import { useTranslation } from 'react-i18next'; import { useAddDamageMode, + usePhotoCaptureImages, usePhotoCaptureSightState, usePictureTaken, useStartTasksOnComplete, @@ -149,6 +150,7 @@ export function PhotoCapture({ complianceIssues, useLiveCompliance, }); + const images = usePhotoCaptureImages(inspectionId); const handlePictureTaken = usePictureTaken({ sightState, addDamageHandle, @@ -201,6 +203,7 @@ export function PhotoCapture({ onClose, inspectionId, showCloseButton, + images, }; return ( diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx index 957dada44..155f3066d 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.tsx @@ -1,11 +1,11 @@ -import { useState } from 'react'; -import { Sight, MonkPicture } from '@monkvision/types'; +import { useMemo, useState } from 'react'; +import { Sight, MonkPicture, Image, ImageStatus } from '@monkvision/types'; import { useTranslation } from 'react-i18next'; import { BackdropDialog } from '@monkvision/common-ui-web'; import { CameraHUDProps } from '@monkvision/camera-web'; import { LoadingState } from '@monkvision/common'; import { PhotoCaptureHUDButtons } from './PhotoCaptureHUDButtons'; -import { useComplianceNotification, usePhotoCaptureHUDStyle } from './hooks'; +import { usePhotoCaptureHUDStyle } from './hooks'; import { PhotoCaptureMode } from '../hooks'; import { PhotoCaptureHUDOverlay } from './PhotoCaptureHUDOverlay'; import { PhotoCaptureHUDPreview } from './PhotoCaptureHUDPreview'; @@ -73,6 +73,10 @@ export interface PhotoCaptureHUDProps extends CameraHUDProps { * @default false */ showCloseButton?: boolean; + /** + * The current images taken by the user (ignoring retaken pictures etc.). + */ + images: Image[]; } /** @@ -97,11 +101,18 @@ export function PhotoCaptureHUD({ loading, handle, cameraPreview, + images, }: PhotoCaptureHUDProps) { const { t } = useTranslation(); const [showCloseModal, setShowCloseModal] = useState(false); const style = usePhotoCaptureHUDStyle(); - const showGalleryBadge = useComplianceNotification(inspectionId); + const showGalleryBadge = useMemo( + () => + images.some((image) => + [ImageStatus.NOT_COMPLIANT, ImageStatus.UPLOAD_FAILED].includes(image.status), + ), + [images], + ); const handleCloseConfirm = () => { setShowCloseModal(false); @@ -113,7 +124,6 @@ export function PhotoCaptureHUD({
{cameraPreview}
); } diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/PhotoCaptureHUDPreviewSight.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/PhotoCaptureHUDPreviewSight.tsx index 0be423b87..9fc21d71c 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/PhotoCaptureHUDPreviewSight.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/PhotoCaptureHUDPreviewSight.tsx @@ -1,4 +1,4 @@ -import { PixelDimensions, Sight } from '@monkvision/types'; +import { Image, PixelDimensions, Sight } from '@monkvision/types'; import { SightOverlay } from '@monkvision/common-ui-web'; import { SightSlider } from './SightSlider'; import { styles } from './PhotoCaptureHUDPreviewSight.styles'; @@ -11,10 +11,6 @@ import { PhotoCaptureMode } from '../../hooks'; * Props of the PhotoCaptureHUDPreviewSight component. */ export interface PhotoCaptureHUDSightPreviewProps { - /** - * The ID of the current inspection. - */ - inspectionId: string; /** * The list of sights provided to the PhotoCapture component. */ @@ -39,6 +35,10 @@ export interface PhotoCaptureHUDSightPreviewProps { * The dimensions of the Camera video stream. */ streamDimensions?: PixelDimensions | null; + /** + * The current images taken by the user (ignoring retaken pictures etc.). + */ + images: Image[]; } /** @@ -46,13 +46,13 @@ export interface PhotoCaptureHUDSightPreviewProps { * mode is SIGHT. */ export function PhotoCaptureHUDPreviewSight({ - inspectionId, sights, selectedSight, onSelectedSight = () => {}, onAddDamage = () => {}, sightsTaken, streamDimensions, + images, }: PhotoCaptureHUDSightPreviewProps) { const style = usePhotoCaptureHUDSightPreviewStyle(); const aspectRatio = `${streamDimensions?.width}/${streamDimensions?.height}`; @@ -71,11 +71,11 @@ export function PhotoCaptureHUDPreviewSight({ ); diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/SightSlider/SightSlider.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/SightSlider/SightSlider.tsx index 38b1563c5..f7638043c 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/SightSlider/SightSlider.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/SightSlider/SightSlider.tsx @@ -1,6 +1,6 @@ -import { ImageStatus, Sight } from '@monkvision/types'; +import { Image, ImageStatus, Sight } from '@monkvision/types'; import { RefObject, useEffect, useMemo, useRef } from 'react'; -import { useMonkState, useSightLabel } from '@monkvision/common'; +import { useSightLabel } from '@monkvision/common'; import { labels } from '@monkvision/sights'; import { SightSliderButton } from './SightSliderButton'; import { useSightSliderStyles } from './hooks'; @@ -9,10 +9,6 @@ import { useSightSliderStyles } from './hooks'; * Props of the SightSlider component. */ export interface SightSliderProps { - /** - * The ID of the current inspection. - */ - inspectionId: string; /** * The list of sights provided to the PhotoCapture component. */ @@ -29,6 +25,10 @@ export interface SightSliderProps { * Callback called when the user manually select a new sight. */ onSelectedSight?: (sight: Sight) => void; + /** + * The current images taken by the user (ignoring retaken pictures etc.). + */ + images: Image[]; } const scrollToSelectedSight = ( @@ -49,18 +49,14 @@ interface SightSliderItem { status?: ImageStatus; } -function useSightSliderItems(inspectionId: string, sights: Sight[]): SightSliderItem[] { - const { state } = useMonkState(); +function useSightSliderItems(sights: Sight[], images: Image[]): SightSliderItem[] { return useMemo( () => sights.map((sight) => ({ sight, - status: state.images.find( - (image) => - image.inspectionId === inspectionId && image.additionalData?.sight_id === sight.id, - )?.status, + status: images.find((image) => image.additionalData?.sight_id === sight.id)?.status, })), - [state.images, sights], + [sights, images], ); } @@ -70,13 +66,13 @@ function useSightSliderItems(inspectionId: string, sights: Sight[]): SightSlider * sight. */ export function SightSlider({ - inspectionId, sights, selectedSight, sightsTaken, onSelectedSight = () => {}, + images, }: SightSliderProps) { - const items = useSightSliderItems(inspectionId, sights); + const items = useSightSliderItems(sights, images); const { label } = useSightLabel({ labels }); const { container } = useSightSliderStyles(); const ref = useRef(null); diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/hooks/index.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/hooks/index.ts index 6b6b8297d..3153a7936 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/hooks/index.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/hooks/index.ts @@ -1,3 +1,2 @@ export * from './usePhotoCaptureHUDStyle'; export * from './usePhotoCaptureHUDButtonBackground'; -export * from './useComplianceNotification'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/hooks/useComplianceNotification.ts b/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/hooks/useComplianceNotification.ts deleted file mode 100644 index d537fc139..000000000 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCaptureHUD/hooks/useComplianceNotification.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ImageStatus } from '@monkvision/types'; -import { getInspectionImages, useMonkState } from '@monkvision/common'; -import { useMemo } from 'react'; - -/** - * Custom hook used to check if the notification indicating if there are some pictures to be retaken should be displayed - * or not. - */ -export function useComplianceNotification(inspectionId: string): boolean { - const { state } = useMonkState(); - - return useMemo( - () => - getInspectionImages(inspectionId, state.images, true).some((image) => - [ImageStatus.NOT_COMPLIANT, ImageStatus.UPLOAD_FAILED].includes(image.status), - ), - [state, inspectionId], - ); -} diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/index.ts b/packages/inspection-capture-web/src/PhotoCapture/hooks/index.ts index 0fc6cf00d..f9d117ed9 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/hooks/index.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/hooks/index.ts @@ -3,3 +3,4 @@ export * from './useStartTasksOnComplete'; export * from './useAddDamageMode'; export * from './useUploadQueue'; export * from './usePictureTaken'; +export * from './usePhotoCaptureImages'; diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureImages.ts b/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureImages.ts new file mode 100644 index 000000000..24de30bb2 --- /dev/null +++ b/packages/inspection-capture-web/src/PhotoCapture/hooks/usePhotoCaptureImages.ts @@ -0,0 +1,15 @@ +import { Image } from '@monkvision/types'; +import { getInspectionImages, useMonkState } from '@monkvision/common'; +import { useMemo } from 'react'; + +/** + * Custom hook that returns the current images taken in the inspection (it filters retakes etc.). + */ +export function usePhotoCaptureImages(inspectionId: string): Image[] { + const { state } = useMonkState(); + + return useMemo( + () => getInspectionImages(inspectionId, state.images, true), + [state.images, inspectionId], + ); +} diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx index 5e2ddf067..d3e1986b6 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx @@ -1,4 +1,20 @@ import { sights } from '@monkvision/sights'; +import { act, render, waitFor } from '@testing-library/react'; +import { Camera, CameraResolution, CompressionFormat } from '@monkvision/camera-web'; +import { expectPropsOnChildMock } from '@monkvision/test-utils'; +import { PhotoCapture, PhotoCaptureHUD, PhotoCaptureProps } from '../../src'; +import { + useAddDamageMode, + usePhotoCaptureImages, + usePhotoCaptureSightState, + usePictureTaken, + useStartTasksOnComplete, + useUploadQueue, +} from '../../src/PhotoCapture/hooks'; +import { useI18nSync, useLoadingState } from '@monkvision/common'; +import { ComplianceIssue, TaskName } from '@monkvision/types'; +import { InspectionGallery } from '@monkvision/common-ui-web'; +import { useMonitoring } from '@monkvision/monitoring'; const { PhotoCaptureMode } = jest.requireActual('../../src/PhotoCapture/hooks'); @@ -28,24 +44,9 @@ jest.mock('../../src/PhotoCapture/hooks', () => ({ length: 3, })), useStartTasksOnComplete: jest.fn(() => jest.fn()), + usePhotoCaptureImages: jest.fn(() => [{ id: 'test' }]), })); -import { act, render, waitFor } from '@testing-library/react'; -import { Camera, CameraResolution, CompressionFormat } from '@monkvision/camera-web'; -import { expectPropsOnChildMock } from '@monkvision/test-utils'; -import { PhotoCapture, PhotoCaptureHUD, PhotoCaptureProps } from '../../src'; -import { - useAddDamageMode, - usePhotoCaptureSightState, - usePictureTaken, - useStartTasksOnComplete, - useUploadQueue, -} from '../../src/PhotoCapture/hooks'; -import { useI18nSync, useLoadingState } from '@monkvision/common'; -import { ComplianceIssue, TaskName } from '@monkvision/types'; -import { InspectionGallery } from '@monkvision/common-ui-web'; -import { useMonitoring } from '@monkvision/monitoring'; - function createProps(): PhotoCaptureProps { return { sights: [sights['test-sight-1'], sights['test-sight-2'], sights['test-sight-3']], @@ -213,6 +214,8 @@ describe('PhotoCapture component', () => { const sightState = (usePhotoCaptureSightState as jest.Mock).mock.results[0].value; expect(useLoadingState).toHaveBeenCalled(); const loading = (useLoadingState as jest.Mock).mock.results[0].value; + expect(usePhotoCaptureImages).toHaveBeenCalledWith(props.inspectionId); + const images = (usePhotoCaptureImages as jest.Mock).mock.results[0].value; expectPropsOnChildMock(Camera, { hudProps: { sights: props.sights, @@ -229,6 +232,7 @@ describe('PhotoCapture component', () => { inspectionId: props.inspectionId, showCloseButton: props.showCloseButton, onOpenGallery: expect.any(Function), + images, }, }); 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 478a629bd..ad7693145 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUD.test.tsx @@ -1,3 +1,5 @@ +import { Image, ImageStatus } from '@monkvision/types'; + jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/hooks', () => ({ ...jest.requireActual('../../../src/PhotoCapture/PhotoCaptureHUD/hooks'), useComplianceNotification: jest.fn(() => false), @@ -25,7 +27,6 @@ import { PhotoCaptureHUDOverlay, PhotoCaptureHUDPreview, PhotoCaptureHUDProps, - useComplianceNotification, } from '../../../src'; import { PhotoCaptureMode } from '../../../src/PhotoCapture/hooks'; @@ -58,6 +59,9 @@ function createProps(): PhotoCaptureHUDProps { dimensions: { height: 2, width: 4 }, } as unknown as CameraHandle, cameraPreview:
, + images: [ + { additionalData: { sight_id: 'test-sight-1' }, status: ImageStatus.NOT_COMPLIANT }, + ] as Image[], }; } @@ -80,7 +84,6 @@ describe('PhotoCaptureHUD component', () => { const { unmount } = render(); expectPropsOnChildMock(PhotoCaptureHUDPreview, { - inspectionId: props.inspectionId, selectedSight: props.selectedSight, sights: props.sights, sightsTaken: props.sightsTaken, @@ -91,6 +94,7 @@ describe('PhotoCaptureHUD component', () => { isLoading: props.loading.isLoading || props.handle.isLoading, error: props.loading.error ?? props.handle.error, streamDimensions: props.handle.dimensions, + images: props.images, }); unmount(); @@ -163,16 +167,28 @@ describe('PhotoCaptureHUD component', () => { unmount(); }); - it('shoulddisplay the gallery badge if there are compliance notifications', () => { - (useComplianceNotification as jest.Mock).mockImplementationOnce(() => false); + const RETAKE_STATUSES = [ImageStatus.NOT_COMPLIANT, ImageStatus.UPLOAD_FAILED]; + + RETAKE_STATUSES.forEach((status) => { + it(`should display the gallery badge if there are images with the ${status} status`, () => { + const props = createProps(); + props.images = [{ status }] as Image[]; + const { unmount } = render(); + + expectPropsOnChildMock(PhotoCaptureHUDButtons, { showGalleryBadge: true }); + + unmount(); + }); + }); + + it('should not display the gallery badge if there are no images with retake statuses', () => { const props = createProps(); - const { unmount, rerender } = render(); + props.images = Object.values(ImageStatus) + .filter((status) => !RETAKE_STATUSES.includes(status)) + .map((status) => ({ status } as Image)); + const { unmount } = render(); expectPropsOnChildMock(PhotoCaptureHUDButtons, { showGalleryBadge: false }); - (useComplianceNotification as jest.Mock).mockImplementationOnce(() => true); - - rerender(); - expectPropsOnChildMock(PhotoCaptureHUDButtons, { showGalleryBadge: true }); unmount(); }); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreview.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreview.test.tsx index 28d415005..89656cb90 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreview.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreview.test.tsx @@ -1,3 +1,5 @@ +import { Image, ImageStatus } from '@monkvision/types'; + jest.mock('../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight', () => ({ PhotoCaptureHUDPreviewSight: jest.fn(() => <>), })); @@ -30,7 +32,6 @@ import { function createProps(): PhotoCaptureHUDPreviewProps { const captureSights = [sights['test-sight-1'], sights['test-sight-2'], sights['test-sight-3']]; return { - inspectionId: 'test-inspection-id-test', selectedSight: captureSights[1], sights: captureSights, sightsTaken: [captureSights[0]], @@ -41,6 +42,9 @@ function createProps(): PhotoCaptureHUDPreviewProps { streamDimensions: { height: 1234, width: 45678 }, isLoading: false, error: null, + images: [ + { additionalData: { sight_id: 'test-sight-1' }, status: ImageStatus.NOT_COMPLIANT }, + ] as Image[], }; } @@ -75,13 +79,13 @@ describe('PhotoCaptureHUDPreview component', () => { const { unmount } = render(); expectPropsOnChildMock(PhotoCaptureHUDPreviewSight, { - inspectionId: props.inspectionId, sights: props.sights, selectedSight: props.selectedSight, onSelectedSight: props.onSelectSight, sightsTaken: props.sightsTaken, onAddDamage: props.onAddDamage, streamDimensions: props.streamDimensions, + images: props.images, }); expect(PhotoCaptureHUDPreviewAddDamage1stShot).not.toHaveBeenCalled(); expect(PhotoCaptureHUDPreviewAddDamage2ndShot).not.toHaveBeenCalled(); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/PhotoCaptureHUDPreviewSight.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/PhotoCaptureHUDPreviewSight.test.tsx index 06a10eb71..df03cd5ee 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/PhotoCaptureHUDPreviewSight.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/PhotoCaptureHUDPreviewSight.test.tsx @@ -1,3 +1,5 @@ +import { Image, ImageStatus } from '@monkvision/types'; + jest.mock('../../../../src/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDCounter', () => ({ PhotoCaptureHUDCounter: jest.fn(() => <>), })); @@ -35,13 +37,16 @@ function createProps(): PhotoCaptureHUDSightPreviewProps { sights['test-sight-4'], ]; return { - inspectionId: 'inspection-id-test', sights: captureSights, selectedSight: captureSights[2], sightsTaken: [captureSights[0], captureSights[1]], onSelectedSight: jest.fn(), onAddDamage: jest.fn(), streamDimensions: { width: 4563, height: 992 }, + images: [ + { additionalData: { sight_id: 'test-sight-1' }, status: ImageStatus.NOT_COMPLIANT }, + { additionalData: { sight_id: 'test-sight-2' }, status: ImageStatus.SUCCESS }, + ] as Image[], }; } @@ -89,11 +94,11 @@ describe('PhotoCaptureHUDPreviewSight component', () => { const { unmount } = render(); expectPropsOnChildMock(SightSlider, { - inspectionId: props.inspectionId, sights: props.sights, selectedSight: props.selectedSight, sightsTaken: props.sightsTaken, onSelectedSight: props.onSelectedSight, + images: props.images, }); unmount(); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/SightSlider/SightSlider.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/SightSlider/SightSlider.test.tsx index 96650c5d6..063ace168 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/SightSlider/SightSlider.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/PhotoCaptureHUDPreviewSight/SightSlider/SightSlider.test.tsx @@ -7,12 +7,12 @@ jest.mock( import { sights } from '@monkvision/sights'; import { render } from '@testing-library/react'; -import { useMonkState, useSightLabel } from '@monkvision/common'; -import { ImageStatus, Sight } from '@monkvision/types'; +import { useSightLabel } from '@monkvision/common'; +import { Image, ImageStatus, Sight } from '@monkvision/types'; import { + SightSlider, SightSliderButton, SightSliderButtonProps, - SightSlider, SightSliderProps, } from '../../../../../src'; @@ -25,11 +25,14 @@ function createProps(): SightSliderProps { sights['test-sight-5'], ]; return { - inspectionId: 'test-inspection-id-test', sights: captureSights, selectedSight: captureSights[2], sightsTaken: [captureSights[0], captureSights[1]], onSelectedSight: jest.fn(), + images: [ + { additionalData: { sight_id: 'test-sight-1' }, status: ImageStatus.NOT_COMPLIANT }, + { additionalData: { sight_id: 'test-sight-2' }, status: ImageStatus.SUCCESS }, + ] as Image[], }; } @@ -39,26 +42,6 @@ describe('SightSlider component', () => { }); it('should display a Button for each sight with the proper props', () => { - const state = { - images: [ - { - inspectionId: createProps().inspectionId, - additionalData: { sight_id: 'test-sight-1' }, - status: ImageStatus.SUCCESS, - }, - { - inspectionId: createProps().inspectionId, - additionalData: { sight_id: 'test-sight-4' }, - status: ImageStatus.COMPLIANCE_RUNNING, - }, - { - inspectionId: createProps().inspectionId, - additionalData: { sight_id: 'test-sight-5' }, - status: ImageStatus.NOT_COMPLIANT, - }, - ], - }; - (useMonkState as jest.Mock).mockImplementation(() => ({ state })); (useSightLabel as jest.Mock).mockImplementation(() => ({ label: (sight: Sight) => sight.id })); const props = createProps(); const { unmount } = render(); @@ -73,11 +56,7 @@ describe('SightSlider component', () => { expect(buttonProps).toBeDefined(); expect(buttonProps.isSelected).toEqual(props.selectedSight === sight); expect(buttonProps.status).toEqual( - state.images.find( - (image) => - image.inspectionId === props.inspectionId && - image.additionalData?.sight_id === sight.id, - )?.status, + props.images.find((image) => image.additionalData?.sight_id === sight.id)?.status, ); expect(typeof buttonProps.onClick).toBe('function'); buttonProps.onClick?.(); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/hooks/useComplianceNotification.test.ts b/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/hooks/useComplianceNotification.test.ts deleted file mode 100644 index 873e4a422..000000000 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCaptureHUD/hooks/useComplianceNotification.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { useComplianceNotification } from '../../../../src'; -import { renderHook } from '@testing-library/react-hooks'; -import { useMonkState } from '@monkvision/common'; -import { ImageStatus } from '@monkvision/types'; - -const inspectionId = 'test-inspection-id'; - -describe('useComplianceNotification hook', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - [ImageStatus.NOT_COMPLIANT, ImageStatus.UPLOAD_FAILED].forEach((status) => { - it(`should return true if there are some pictures with the ${status} status`, () => { - (useMonkState as jest.Mock).mockImplementationOnce(() => ({ - state: { - images: [ - { id: '0', inspectionId: 'test', status: ImageStatus.SUCCESS }, - { id: '1', inspectionId, status: ImageStatus.SUCCESS }, - { id: '2', inspectionId, status: ImageStatus.SUCCESS }, - { - id: '3', - inspectionId, - status: ImageStatus.SUCCESS, - additionalData: { - sight_id: 'sight-1', - created_at: Date.parse('1998-01-01T01:01:01.001Z'), - }, - }, - { - id: '4', - inspectionId, - status, - additionalData: { - sight_id: 'sight-1', - created_at: Date.parse('2024-01-01T01:01:01.001Z'), - }, - }, - ], - }, - })); - const { result, unmount } = renderHook(useComplianceNotification, { - initialProps: inspectionId, - }); - - expect(result.current).toBe(true); - - unmount(); - }); - }); - - it('should return false if there are no pictures to retake', () => { - (useMonkState as jest.Mock).mockImplementationOnce(() => ({ - state: { - images: [ - { id: '0', inspectionId: 'test', status: ImageStatus.UPLOAD_FAILED }, - { id: '1', inspectionId, status: ImageStatus.SUCCESS }, - { id: '2', inspectionId, status: ImageStatus.UPLOADING }, - { id: '3', inspectionId, status: ImageStatus.COMPLIANCE_RUNNING }, - { - id: '4', - inspectionId, - status: ImageStatus.UPLOAD_FAILED, - additionalData: { - sight_id: 'sight-1', - created_at: Date.parse('1998-01-01T01:01:01.001Z'), - }, - }, - { - id: '5', - inspectionId, - status: ImageStatus.UPLOADING, - additionalData: { - sight_id: 'sight-1', - created_at: Date.parse('2024-01-01T01:01:01.001Z'), - }, - }, - { - id: '6', - inspectionId, - status: ImageStatus.NOT_COMPLIANT, - additionalData: { - sight_id: 'sight-1', - created_at: Date.parse('2000-01-01T01:01:01.001Z'), - }, - }, - ], - }, - })); - const { result, unmount } = renderHook(useComplianceNotification, { - initialProps: inspectionId, - }); - - expect(result.current).toBe(false); - - unmount(); - }); -}); diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureImages.test.ts b/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureImages.test.ts new file mode 100644 index 000000000..dcacc5ea3 --- /dev/null +++ b/packages/inspection-capture-web/test/PhotoCapture/hooks/usePhotoCaptureImages.test.ts @@ -0,0 +1,24 @@ +const stateMock = { images: [{ id: 'test' }] }; +const inspectionImagesMock = [{ id: 'test-hello' }]; + +jest.mock('@monkvision/common', () => ({ + useMonkState: jest.fn(() => ({ state: stateMock })), + getInspectionImages: jest.fn(() => inspectionImagesMock), +})); + +import { usePhotoCaptureImages } from '../../../src/PhotoCapture/hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import { getInspectionImages, useMonkState } from '@monkvision/common'; + +describe('usePhotoCaptureImages hook', () => { + it('should return the images of the inspection using the getInspectionImages function', () => { + const inspectionId = 'test-inspection-id-test'; + const { result, unmount } = renderHook(usePhotoCaptureImages, { initialProps: inspectionId }); + + expect(useMonkState).toHaveBeenCalled(); + expect(getInspectionImages).toHaveBeenCalledWith(inspectionId, stateMock.images, true); + expect(result.current).toEqual(inspectionImagesMock); + + unmount(); + }); +}); diff --git a/packages/network/src/api/inspection/mappers.ts b/packages/network/src/api/inspection/mappers.ts index a3b62a591..4b16d52ed 100644 --- a/packages/network/src/api/inspection/mappers.ts +++ b/packages/network/src/api/inspection/mappers.ts @@ -33,6 +33,7 @@ import { import { ApiCommentSeverityValue, ApiDamageDetectionTaskPostComponent, + ApiHinlTaskPostComponent, ApiImageRegion, ApiImagesOCRTaskPostComponent, ApiInspectionGet, @@ -402,6 +403,28 @@ export function mapApiInspectionGet( }; } +/** + * Options used to specify a callback that will be called by the API when a task is complete. + */ +export interface TaskCallbackOptions { + /** + * The URL of the callback. + */ + url: string; + /** + * The headers of the request. + */ + headers: Record; + /** + * The parameters of the callback. + */ + params?: Record; + /** + * The event of the callback. + */ + event?: string; +} + /** * Additional options that you can specify when adding the damage detection task to an inspection. */ @@ -440,11 +463,28 @@ export interface CreateDamageDetectionTaskOptions { generateSubimageParts?: boolean; } +/** + * Additional options that you can specify when adding the human in the loop task to an inspection. + */ +export interface CreateHinlTaskOptions { + /** + * The name of the task : `TaskName.HUMAN_IN_THE_LOOP`. + */ + name: TaskName.HUMAN_IN_THE_LOOP; + /** + * The callbacks called at the end of the Human in the Loop task. + */ + callbacks?: TaskCallbackOptions[]; +} + /** * The tasks of the inspection to be created. It is either simply the name of the task to add, or an object with the * task name as well as additional configuration options for the task. */ -export type InspectionCreateTask = TaskName | CreateDamageDetectionTaskOptions; +export type InspectionCreateTask = + | TaskName + | CreateDamageDetectionTaskOptions + | CreateHinlTaskOptions; /** * Options that can be specified when creating a new inspection. @@ -467,6 +507,23 @@ export interface CreateInspectionOptions { useDynamicCrops?: boolean; } +function getHumanInTheLoopOptions( + options: CreateInspectionOptions, +): ApiHinlTaskPostComponent | undefined { + if (options.tasks.includes(TaskName.HUMAN_IN_THE_LOOP)) { + return { status: ProgressStatus.NOT_STARTED }; + } + const taskOptions = options.tasks.find( + (task) => typeof task === 'object' && task.name === TaskName.HUMAN_IN_THE_LOOP, + ) as CreateHinlTaskOptions | undefined; + return taskOptions + ? { + status: ProgressStatus.NOT_STARTED, + callbacks: taskOptions.callbacks, + } + : undefined; +} + function getDamageDetectionOptions( options: CreateInspectionOptions, ): ApiDamageDetectionTaskPostComponent | undefined { @@ -527,6 +584,7 @@ function getTasksOptions(options: CreateInspectionOptions): ApiTasksComponent { damage_detection: getDamageDetectionOptions(options), wheel_analysis: getWheelAnalysisOptions(options), images_ocr: getImagesOCROptions(options), + human_in_the_loop: getHumanInTheLoopOptions(options), }; } diff --git a/packages/network/src/api/models/task.ts b/packages/network/src/api/models/task.ts index 908bb7b08..ee4da4512 100644 --- a/packages/network/src/api/models/task.ts +++ b/packages/network/src/api/models/task.ts @@ -61,7 +61,7 @@ export interface ApiCallback { url: string; headers: Record; callback_event?: ApiCallbackEvent; - params: Record; + params?: Record; } export type ApiCallbacks = ApiCallback[]; @@ -102,8 +102,14 @@ export interface ApiImagesOCRTaskPostComponent { callbacks?: ApiCallbacks; } +export interface ApiHinlTaskPostComponent { + status?: ApiTaskPostProgressStatus; + callbacks?: ApiCallbacks; +} + export interface ApiTasksComponent { damage_detection?: ApiDamageDetectionTaskPostComponent; wheel_analysis?: ApiWheelAnalysisTaskPostComponent; images_ocr?: ApiImagesOCRTaskPostComponent; + human_in_the_loop?: ApiHinlTaskPostComponent; }