diff --git a/.github/workflows/deploy-drive-app.yml b/.github/workflows/deploy-drive-app.yml deleted file mode 100644 index cb7620f2d..000000000 --- a/.github/workflows/deploy-drive-app.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Deploy Drive App -run-name: Deploy Drive App To Staging After Merge - -on: - push: - branches: [ main ] - -jobs: - build: - name: Build - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - steps: - - name: 💾 Checking out the repository - uses: actions/checkout@v4 - - name: ⚙️ Setting up the MonkJs project - uses: ./.github/actions/monkjs-set-up - - name: 📱 Building the Drive app - run: yarn build:drive-app:staging - - name: 📦 Uploading the artifact - uses: actions/upload-artifact@v4.3.1 - with: - name: build-drive-app-staging - path: apps/drive-app/build - - deploy: - name: Deploy - environment: staging - needs: - - build - container: - image: dtzar/helm-kubectl:3.14.2 - runs-on: ubuntu-latest - steps: - - name: 🔐 Authenticating to Google Cloud - uses: google-github-actions/auth@v2.1.2 - with: - credentials_json: "${{ secrets.GKE_SA_KEY }}" - - name: 🔐 Obtaining GKE credentials - uses: google-github-actions/get-gke-credentials@v2.1.0 - with: - cluster_name: ${{ secrets.GKE_CLUSTER }} - location: ${{ secrets.GKE_ZONE }} - project_id: ${{ secrets.GKE_PROJECT }} - - name: 📦 Downloading the artifact - uses: actions/download-artifact@v4.1.4 - with: - name: build-drive-app-staging - path: drive-staging - - name: 🧹 Cleaning up previous build - run: |- - kubectl -n poc exec -it $(kubectl get pods -n poc -l app.kubernetes.io/instance=poc-spa --no-headers | awk '{print $1}') -- rm -rf drive-staging - - name: 🌐 Deploying app - run: |- - kubectl -n poc cp drive-staging poc/$(kubectl get pods -n poc -l app.kubernetes.io/instance=poc-spa --no-headers | awk '{print $1}'):/app/ diff --git a/apps/demo-app/.env-cmdrc.json b/apps/demo-app/.env-cmdrc.json index 33cb4c456..3dd97fe90 100644 --- a/apps/demo-app/.env-cmdrc.json +++ b/apps/demo-app/.env-cmdrc.json @@ -4,7 +4,8 @@ "HTTPS": "true", "ESLINT_NO_DEV_ERRORS": "true", "REACT_APP_ENVIRONMENT": "local", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "demo-app-development", + "REACT_APP_USE_LOCAL_CONFIG": "true", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -14,7 +15,7 @@ }, "development": { "REACT_APP_ENVIRONMENT": "development", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "demo-app-development", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -23,7 +24,7 @@ }, "staging": { "REACT_APP_ENVIRONMENT": "staging", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "demo-app-staging", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -32,7 +33,7 @@ }, "preview": { "REACT_APP_ENVIRONMENT": "preview", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "demo-app-preview", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -41,7 +42,7 @@ }, "backend-staging-qa": { "REACT_APP_ENVIRONMENT": "backend-staging-qa", - "REACT_APP_API_DOMAIN": "api.staging.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "demo-app-backend-staging-qa", "REACT_APP_AUTH_DOMAIN": "idp.staging.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "DAeZWqeeOfgItYBcQzFeFwSrlvmUdN7L", diff --git a/apps/demo-app/src/components/App.tsx b/apps/demo-app/src/components/App.tsx index eeccadf35..763f3573d 100644 --- a/apps/demo-app/src/components/App.tsx +++ b/apps/demo-app/src/components/App.tsx @@ -1,25 +1,32 @@ import { Outlet, useNavigate } from 'react-router-dom'; -import { MonkAppStateProvider, MonkProvider, useMonkTheme } from '@monkvision/common'; +import { getEnvOrThrow, MonkProvider } from '@monkvision/common'; import { useTranslation } from 'react-i18next'; +import { LiveConfigAppProvider } from '@monkvision/common-ui-web'; +import { CaptureAppConfig } from '@monkvision/types'; import { Page } from '../pages'; -import { AppConfig } from '../config'; +import * as config from '../local-config.json'; +import { AppContainer } from './AppContainer'; + +const localConfig = + process.env['REACT_APP_USE_LOCAL_CONFIG'] === 'true' ? (config as CaptureAppConfig) : undefined; export function App() { const navigate = useNavigate(); const { i18n } = useTranslation(); - const { rootStyles } = useMonkTheme(); return ( - navigate(Page.CREATE_INSPECTION)} onFetchLanguage={(lang) => i18n.changeLanguage(lang)} + lang={i18n.language} > -
+ -
+
-
+ ); } diff --git a/apps/demo-app/src/components/AppContainer.tsx b/apps/demo-app/src/components/AppContainer.tsx new file mode 100644 index 000000000..2f56046c5 --- /dev/null +++ b/apps/demo-app/src/components/AppContainer.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from 'react'; +import { MonkThemeProvider, useMonkAppState, useMonkTheme } from '@monkvision/common'; + +function RootStylesContainer({ children }: PropsWithChildren) { + const { rootStyles } = useMonkTheme(); + + return ( +
+ {children} +
+ ); +} + +export function AppContainer({ children }: PropsWithChildren) { + const { config } = useMonkAppState(); + + return ( + + {children} + + ); +} diff --git a/apps/demo-app/src/components/index.ts b/apps/demo-app/src/components/index.ts index c831a21d5..724f3116e 100644 --- a/apps/demo-app/src/components/index.ts +++ b/apps/demo-app/src/components/index.ts @@ -1,2 +1,3 @@ export * from './App'; export * from './AppRouter'; +export * from './AppContainer'; diff --git a/apps/demo-app/src/config.ts b/apps/demo-app/src/config.ts deleted file mode 100644 index 71b8961f0..000000000 --- a/apps/demo-app/src/config.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { - CaptureAppConfig, - CAR_COVERAGE_COMPLIANCE_ISSUES, - ComplianceIssue, - DEFAULT_COMPLIANCE_ISSUES, - DeviceOrientation, - MonkApiPermission, - TaskName, - VehicleType, - ZOOM_LEVEL_COMPLIANCE_ISSUES, -} from '@monkvision/types'; -import { getEnvOrThrow } from '@monkvision/common'; - -export const AppConfig: CaptureAppConfig = { - enforceOrientation: DeviceOrientation.LANDSCAPE, - defaultVehicleType: VehicleType.CUV, - allowManualLogin: true, - allowVehicleTypeSelection: true, - fetchFromSearchParams: true, - allowCreateInspection: true, - useLiveCompliance: true, - allowSkipRetake: true, - createInspectionOptions: { tasks: [TaskName.DAMAGE_DETECTION, TaskName.WHEEL_ANALYSIS] }, - apiDomain: getEnvOrThrow('REACT_APP_API_DOMAIN'), - requiredApiPermissions: [ - MonkApiPermission.TASK_COMPLIANCES, - MonkApiPermission.TASK_DAMAGE_DETECTION, - MonkApiPermission.TASK_DAMAGE_IMAGES_OCR, - MonkApiPermission.TASK_WHEEL_ANALYSIS, - MonkApiPermission.INSPECTION_CREATE, - MonkApiPermission.INSPECTION_READ, - MonkApiPermission.INSPECTION_UPDATE, - MonkApiPermission.INSPECTION_WRITE, - ], - enableSteeringWheelPosition: false, - sights: { - [VehicleType.SUV]: [ - 'jgc21-QIvfeg0X', - 'jgc21-KyUUVU2P', - 'jgc21-zCrDwYWE', - 'jgc21-z15ZdJL6', - 'jgc21-RE3li6rE', - 'jgc21-omlus7Ui', - 'jgc21-m2dDoMup', - 'jgc21-3gjMwvQG', - 'jgc21-ezXzTRkj', - 'jgc21-tbF2Ax8v', - 'jgc21-3JJvM7_B', - 'jgc21-RAVpqaE4', - 'jgc21-F-PPd4qN', - 'jgc21-XXh8GWm8', - 'jgc21-TRN9Des4', - 'jgc21-s7WDTRmE', - 'jgc21-__JKllz9', - ], - [VehicleType.CUV]: [ - 'fesc20-H1dfdfvH', - 'fesc20-WMUaKDp1', - 'fesc20-LTe3X2bg', - 'fesc20-WIQsf_gX', - 'fesc20-hp3Tk53x', - 'fesc20-fOt832UV', - 'fesc20-NLdqASzl', - 'fesc20-4Wqx52oU', - 'fesc20-dfICsfSV', - 'fesc20-X8k7UFGf', - 'fesc20-LZc7p2kK', - 'fesc20-5Ts1UkPT', - 'fesc20-gg1Xyrpu', - 'fesc20-P0oSEh8p', - 'fesc20-j3H8Z415', - 'fesc20-dKVLig1i', - 'fesc20-Wzdtgqqz', - ], - [VehicleType.SEDAN]: [ - 'haccord-8YjMcu0D', - 'haccord-DUPnw5jj', - 'haccord-hsCc_Nct', - 'haccord-GQcZz48C', - 'haccord-QKfhXU7o', - 'haccord-mdZ7optI', - 'haccord-bSAv3Hrj', - 'haccord-W-Bn3bU1', - 'haccord-GdWvsqrm', - 'haccord-ps7cWy6K', - 'haccord-Jq65fyD4', - 'haccord-OXYy5gET', - 'haccord-5LlCuIfL', - 'haccord-Gtt0JNQl', - 'haccord-cXSAj2ez', - 'haccord-KN23XXkX', - 'haccord-Z84erkMb', - ], - [VehicleType.HATCHBACK]: [ - 'ffocus18-XlfgjQb9', - 'ffocus18-3TiCVAaN', - 'ffocus18-43ljK5xC', - 'ffocus18-x_1SE7X-', - 'ffocus18-QKfhXU7o', - 'ffocus18-yo9eBDW6', - 'ffocus18-cPUyM28L', - 'ffocus18-S3kgFOBb', - 'ffocus18-9MeSIqp7', - 'ffocus18-X2LDjCvr', - 'ffocus18-jWOq2CNN', - 'ffocus18-P2jFq1Ea', - 'ffocus18-U3Bcfc2Q', - 'ffocus18-ts3buSD1', - 'ffocus18-cXSAj2ez', - 'ffocus18-KkeGvT-F', - 'ffocus18-lRDlWiwR', - ], - [VehicleType.VAN]: [ - 'ftransit18-wyXf7MTv', - 'ftransit18-UNAZWJ-r', - 'ftransit18-5SiNC94w', - 'ftransit18-Y0vPhBVF', - 'ftransit18-xyp1rU0h', - 'ftransit18-6khKhof0', - 'ftransit18-eXJDDYmE', - 'ftransit18-3Sbfx_KZ', - 'ftransit18-iu1Vj2Oa', - 'ftransit18-aA2K898S', - 'ftransit18-NwBMLo3Z', - 'ftransit18-cf0e-pcB', - 'ftransit18-FFP5b34o', - 'ftransit18-RJ2D7DNz', - 'ftransit18-3fnjrISV', - 'ftransit18-eztNpSRX', - 'ftransit18-TkXihCj4', - 'ftransit18-4NMPqEV6', - 'ftransit18-IIVI_pnX', - ], - [VehicleType.MINIVAN]: [ - 'tsienna20-YwrRNr9n', - 'tsienna20-HykkFbXf', - 'tsienna20-TI4TVvT9', - 'tsienna20-65mfPdRD', - 'tsienna20-Ia0SGJ6z', - 'tsienna20-1LNxhgCR', - 'tsienna20-U_FqYq-a', - 'tsienna20-670P2H2V', - 'tsienna20-1n_z8bYy', - 'tsienna20-qA3aAUUq', - 'tsienna20--a2RmRcs', - 'tsienna20-SebsoqJm', - 'tsienna20-u57qDaN_', - 'tsienna20-Rw0Gtt7O', - 'tsienna20-TibS83Qr', - 'tsienna20-cI285Gon', - 'tsienna20-KHB_Cd9k', - ], - [VehicleType.PICKUP]: [ - 'ff150-zXbg0l3z', - 'ff150-3he9UOwy', - 'ff150-KgHVkQBW', - 'ff150-FqbrFVr2', - 'ff150-g_xBOOS2', - 'ff150-vwE3yqdh', - 'ff150-V-xzfWsx', - 'ff150-ouGGtRnf', - 'ff150--xPZZd83', - 'ff150-nF_oFvhI', - 'ff150-t3KBMPeD', - 'ff150-3rM9XB0Z', - 'ff150-eOjyMInj', - 'ff150-18YVVN-G', - 'ff150-BmXfb-qD', - 'ff150-gFp78fQO', - 'ff150-7nvlys8r', - ], - }, - complianceIssues: DEFAULT_COMPLIANCE_ISSUES.filter( - (issue) => - ![ - ComplianceIssue.WRONG_ANGLE, - ComplianceIssue.TOO_ZOOMED, - ComplianceIssue.NOT_ZOOMED_ENOUGH, - ].includes(issue), - ), - complianceIssuesPerSight: { - 'ff150-nF_oFvhI': DEFAULT_COMPLIANCE_ISSUES.filter( - (issue) => - ![...ZOOM_LEVEL_COMPLIANCE_ISSUES, ...CAR_COVERAGE_COMPLIANCE_ISSUES].includes(issue), - ), - }, -}; diff --git a/apps/demo-app/src/index.tsx b/apps/demo-app/src/index.tsx index b29dfeb03..9a5832da1 100644 --- a/apps/demo-app/src/index.tsx +++ b/apps/demo-app/src/index.tsx @@ -2,7 +2,7 @@ import ReactDOM from 'react-dom'; import { MonitoringProvider } from '@monkvision/monitoring'; import { AnalyticsProvider } from '@monkvision/analytics'; import { Auth0Provider } from '@auth0/auth0-react'; -import { getEnvOrThrow, MonkThemeProvider } from '@monkvision/common'; +import { getEnvOrThrow } from '@monkvision/common'; import { sentryMonitoringAdapter } from './sentry'; import { posthogAnalyticsAdapter } from './posthog'; import { AppRouter } from './components'; @@ -21,9 +21,7 @@ ReactDOM.render( prompt: 'login', }} > - - - + , diff --git a/apps/demo-app/src/local-config.json b/apps/demo-app/src/local-config.json new file mode 100644 index 000000000..37f48dcab --- /dev/null +++ b/apps/demo-app/src/local-config.json @@ -0,0 +1,196 @@ +{ + "allowSkipRetake": true, + "enableAddDamage": true, + "allowVehicleTypeSelection": true, + "allowManualLogin": true, + "fetchFromSearchParams": true, + "allowCreateInspection": true, + "createInspectionOptions": { + "tasks": ["damage_detection", "wheel_analysis"] + }, + "apiDomain": "api.preview.monk.ai/v1", + "startTasksOnComplete": true, + "showCloseButton": false, + "enforceOrientation": "landscape", + "maxUploadDurationWarning": 15000, + "useAdaptiveImageQuality": true, + "format": "image/jpeg", + "quality": 0.6, + "resolution": "4K", + "allowImageUpscaling": false, + "enableCompliance": true, + "useLiveCompliance": true, + "complianceIssues": [ + "blurriness", + "underexposure", + "overexposure", + "lens_flare", + "reflections", + "unknown_sight", + "unknown_viewpoint", + "no_vehicle", + "wrong_center_part", + "missing_parts", + "hidden_parts", + "missing" + ], + "complianceIssuesPerSight": { + "ff150-nF_oFvhI": [ + "blurriness", + "underexposure", + "overexposure", + "lens_flare", + "reflections", + "missing" + ] + }, + "defaultVehicleType": "cuv", + "enableSteeringWheelPosition": false, + "sights": { + "suv": [ + "jgc21-QIvfeg0X", + "jgc21-KyUUVU2P", + "jgc21-zCrDwYWE", + "jgc21-z15ZdJL6", + "jgc21-RE3li6rE", + "jgc21-omlus7Ui", + "jgc21-m2dDoMup", + "jgc21-3gjMwvQG", + "jgc21-ezXzTRkj", + "jgc21-tbF2Ax8v", + "jgc21-3JJvM7_B", + "jgc21-RAVpqaE4", + "jgc21-F-PPd4qN", + "jgc21-XXh8GWm8", + "jgc21-TRN9Des4", + "jgc21-s7WDTRmE", + "jgc21-__JKllz9" + ], + "cuv": [ + "fesc20-H1dfdfvH", + "fesc20-WMUaKDp1", + "fesc20-LTe3X2bg", + "fesc20-WIQsf_gX", + "fesc20-hp3Tk53x", + "fesc20-fOt832UV", + "fesc20-NLdqASzl", + "fesc20-4Wqx52oU", + "fesc20-dfICsfSV", + "fesc20-X8k7UFGf", + "fesc20-LZc7p2kK", + "fesc20-5Ts1UkPT", + "fesc20-gg1Xyrpu", + "fesc20-P0oSEh8p", + "fesc20-j3H8Z415", + "fesc20-dKVLig1i", + "fesc20-Wzdtgqqz" + ], + "sedan": [ + "haccord-8YjMcu0D", + "haccord-DUPnw5jj", + "haccord-hsCc_Nct", + "haccord-GQcZz48C", + "haccord-QKfhXU7o", + "haccord-mdZ7optI", + "haccord-bSAv3Hrj", + "haccord-W-Bn3bU1", + "haccord-GdWvsqrm", + "haccord-ps7cWy6K", + "haccord-Jq65fyD4", + "haccord-OXYy5gET", + "haccord-5LlCuIfL", + "haccord-Gtt0JNQl", + "haccord-cXSAj2ez", + "haccord-KN23XXkX", + "haccord-Z84erkMb" + ], + "hatchback": [ + "ffocus18-XlfgjQb9", + "ffocus18-3TiCVAaN", + "ffocus18-43ljK5xC", + "ffocus18-x_1SE7X-", + "ffocus18-QKfhXU7o", + "ffocus18-yo9eBDW6", + "ffocus18-cPUyM28L", + "ffocus18-S3kgFOBb", + "ffocus18-9MeSIqp7", + "ffocus18-X2LDjCvr", + "ffocus18-jWOq2CNN", + "ffocus18-P2jFq1Ea", + "ffocus18-U3Bcfc2Q", + "ffocus18-ts3buSD1", + "ffocus18-cXSAj2ez", + "ffocus18-KkeGvT-F", + "ffocus18-lRDlWiwR" + ], + "van": [ + "ftransit18-wyXf7MTv", + "ftransit18-UNAZWJ-r", + "ftransit18-5SiNC94w", + "ftransit18-Y0vPhBVF", + "ftransit18-xyp1rU0h", + "ftransit18-6khKhof0", + "ftransit18-eXJDDYmE", + "ftransit18-3Sbfx_KZ", + "ftransit18-iu1Vj2Oa", + "ftransit18-aA2K898S", + "ftransit18-NwBMLo3Z", + "ftransit18-cf0e-pcB", + "ftransit18-FFP5b34o", + "ftransit18-RJ2D7DNz", + "ftransit18-3fnjrISV", + "ftransit18-eztNpSRX", + "ftransit18-TkXihCj4", + "ftransit18-4NMPqEV6", + "ftransit18-IIVI_pnX" + ], + "minivan": [ + "tsienna20-YwrRNr9n", + "tsienna20-HykkFbXf", + "tsienna20-TI4TVvT9", + "tsienna20-65mfPdRD", + "tsienna20-Ia0SGJ6z", + "tsienna20-1LNxhgCR", + "tsienna20-U_FqYq-a", + "tsienna20-670P2H2V", + "tsienna20-1n_z8bYy", + "tsienna20-qA3aAUUq", + "tsienna20--a2RmRcs", + "tsienna20-SebsoqJm", + "tsienna20-u57qDaN_", + "tsienna20-Rw0Gtt7O", + "tsienna20-TibS83Qr", + "tsienna20-cI285Gon", + "tsienna20-KHB_Cd9k" + ], + "pickup": [ + "ff150-zXbg0l3z", + "ff150-3he9UOwy", + "ff150-KgHVkQBW", + "ff150-FqbrFVr2", + "ff150-g_xBOOS2", + "ff150-vwE3yqdh", + "ff150-V-xzfWsx", + "ff150-ouGGtRnf", + "ff150--xPZZd83", + "ff150-nF_oFvhI", + "ff150-t3KBMPeD", + "ff150-3rM9XB0Z", + "ff150-eOjyMInj", + "ff150-18YVVN-G", + "ff150-BmXfb-qD", + "ff150-gFp78fQO", + "ff150-7nvlys8r" + ] + }, + "requiredApiPermissions": [ + "monk_core_api:compliances", + "monk_core_api:damage_detection", + "monk_core_api:images_ocr", + "monk_core_api:wheel_analysis", + "monk_core_api:inspections:create", + "monk_core_api:inspections:read", + "monk_core_api:inspections:update", + "monk_core_api:inspections:write" + ] +} diff --git a/apps/drive-app/.env-cmdrc.json b/apps/drive-app/.env-cmdrc.json index f706f72af..05197acb4 100644 --- a/apps/drive-app/.env-cmdrc.json +++ b/apps/drive-app/.env-cmdrc.json @@ -3,12 +3,9 @@ "PORT": "17200", "HTTPS": "true", "ESLINT_NO_DEV_ERRORS": "true", - "REACT_APP_ALLOW_CREATE_INSPECTION": "true", - "REACT_APP_ALLOW_LOGIN": "true", - "REACT_APP_DISABLE_HINL": "true", - "REACT_APP_ALLOW_SKIP_RETAKE": "true", "REACT_APP_ENVIRONMENT": "local", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "drive-app-local", + "REACT_APP_USE_LOCAL_CONFIG": "true", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -17,8 +14,7 @@ }, "development": { "REACT_APP_ENVIRONMENT": "development", - "REACT_APP_ALLOW_SKIP_RETAKE": "true", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "drive-app-development", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -26,8 +22,7 @@ }, "staging": { "REACT_APP_ENVIRONMENT": "staging", - "REACT_APP_ALLOW_SKIP_RETAKE": "true", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "drive-app-staging", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -35,8 +30,7 @@ }, "preview": { "REACT_APP_ENVIRONMENT": "preview", - "REACT_APP_ALLOW_SKIP_RETAKE": "true", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "drive-app-preview", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -44,7 +38,7 @@ }, "production": { "REACT_APP_ENVIRONMENT": "production", - "REACT_APP_API_DOMAIN": "api.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "drive-app-production", "REACT_APP_AUTH_DOMAIN": "idp.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -52,8 +46,7 @@ }, "backend-staging-qa": { "REACT_APP_ENVIRONMENT": "backend-staging-qa", - "REACT_APP_ALLOW_SKIP_RETAKE": "true", - "REACT_APP_API_DOMAIN": "api.staging.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "drive-app-backend-staging-qa", "REACT_APP_AUTH_DOMAIN": "idp.staging.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "DAeZWqeeOfgItYBcQzFeFwSrlvmUdN7L", diff --git a/apps/drive-app/src/components/App.tsx b/apps/drive-app/src/components/App.tsx index eeccadf35..763f3573d 100644 --- a/apps/drive-app/src/components/App.tsx +++ b/apps/drive-app/src/components/App.tsx @@ -1,25 +1,32 @@ import { Outlet, useNavigate } from 'react-router-dom'; -import { MonkAppStateProvider, MonkProvider, useMonkTheme } from '@monkvision/common'; +import { getEnvOrThrow, MonkProvider } from '@monkvision/common'; import { useTranslation } from 'react-i18next'; +import { LiveConfigAppProvider } from '@monkvision/common-ui-web'; +import { CaptureAppConfig } from '@monkvision/types'; import { Page } from '../pages'; -import { AppConfig } from '../config'; +import * as config from '../local-config.json'; +import { AppContainer } from './AppContainer'; + +const localConfig = + process.env['REACT_APP_USE_LOCAL_CONFIG'] === 'true' ? (config as CaptureAppConfig) : undefined; export function App() { const navigate = useNavigate(); const { i18n } = useTranslation(); - const { rootStyles } = useMonkTheme(); return ( - navigate(Page.CREATE_INSPECTION)} onFetchLanguage={(lang) => i18n.changeLanguage(lang)} + lang={i18n.language} > -
+ -
+
-
+ ); } diff --git a/apps/drive-app/src/components/AppContainer.tsx b/apps/drive-app/src/components/AppContainer.tsx new file mode 100644 index 000000000..2f56046c5 --- /dev/null +++ b/apps/drive-app/src/components/AppContainer.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from 'react'; +import { MonkThemeProvider, useMonkAppState, useMonkTheme } from '@monkvision/common'; + +function RootStylesContainer({ children }: PropsWithChildren) { + const { rootStyles } = useMonkTheme(); + + return ( +
+ {children} +
+ ); +} + +export function AppContainer({ children }: PropsWithChildren) { + const { config } = useMonkAppState(); + + return ( + + {children} + + ); +} diff --git a/apps/drive-app/src/components/index.ts b/apps/drive-app/src/components/index.ts index c831a21d5..724f3116e 100644 --- a/apps/drive-app/src/components/index.ts +++ b/apps/drive-app/src/components/index.ts @@ -1,2 +1,3 @@ export * from './App'; export * from './AppRouter'; +export * from './AppContainer'; diff --git a/apps/drive-app/src/config.ts b/apps/drive-app/src/config.ts deleted file mode 100644 index 45b49e786..000000000 --- a/apps/drive-app/src/config.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { - CaptureAppConfig, - ComplianceIssue, - DEFAULT_COMPLIANCE_ISSUES, - DeviceOrientation, - IQA_COMPLIANCE_ISSUES, - MonkApiPermission, - SteeringWheelPosition, - TaskName, - VehicleType, -} from '@monkvision/types'; -import { flatten, getEnvOrThrow, uniq } from '@monkvision/common'; -import { sights } from '@monkvision/sights'; - -const complianceIssues = DEFAULT_COMPLIANCE_ISSUES.filter( - (issue) => - ![ - ComplianceIssue.WRONG_ANGLE, - ComplianceIssue.TOO_ZOOMED, - ComplianceIssue.NOT_ZOOMED_ENOUGH, - ].includes(issue), -); - -const frontRoofLeftComplianceIssues = complianceIssues.filter( - (issue) => issue !== ComplianceIssue.NO_VEHICLE, -); - -const sightIds = { - [SteeringWheelPosition.LEFT]: { - [VehicleType.SUV]: [ - 'jgc21-QIvfeg0X', // Front Low - 'jgc21-QwNQX0Cr', // Front Fender Right - 'jgc21-1j-oTPag', // Lateral Full Right - 'jgc21-3JJvM7_B', // Rear Right - 'jgc21-TyJPUs8E', // Rear Low - 'jgc21-ezXzTRkj', // Rear Left - 'jgc21-TEoi50Ff', // Lateral Full Left - 'jgc21-QIkcNhc_', // Front Fender Left - 'jgc21-imomJ2V0', // Front Roof Left - 'all-IqwSM3', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.CUV]: [ - 'fesc20-H1dfdfvH', // Front Low - 'fesc20-CEGtqHkk', // Front Fender Right - 'fesc20-HYz5ziHi', // Lateral Full Right - 'fesc20-LZc7p2kK', // Rear Right - 'fesc20-xBFiEy-_', // Rear Low - 'fesc20-dfICsfSV', // Rear Left - 'fesc20-26n47kaO', // Lateral Full Left - 'fesc20-GdIxD-_N', // Front Fender Left - 'fesc20-6GPUkfYn', // Front Roof Left - 'all-IqwSM3', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.SEDAN]: [ - 'haccord-8YjMcu0D', // Front Low - 'haccord-EfRIciFr', // Front Fender Right - 'haccord-PGr3RzzP', // Lateral Full Right - 'haccord-Jq65fyD4', // Rear Right - 'haccord-6kYUBv_e', // Rear Low - 'haccord-GdWvsqrm', // Rear Left - 'haccord-_YnTubBA', // Lateral Full Left - 'haccord-2a8VfA8m', // Front Fender Left - 'haccord-oiY_yPTR', // Front Roof Left - 'all-IqwSM3', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.HATCHBACK]: [ - 'ffocus18-XlfgjQb9', // Front Low - 'ffocus18-zgLKB-Do', // Front Fender Right - 'ffocus18-FdsQDaTW', // Lateral Full Right - 'ffocus18-jWOq2CNN', // Rear Right - 'ffocus18-L2UM_68Q', // Rear Low - 'ffocus18-9MeSIqp7', // Rear Left - 'ffocus18-6FX31ty1', // Lateral Full Left - 'ffocus18-GiTxaJUq', // Front Fender Left - 'ffocus18-ZXKOomlv', // Front Roof Left - 'all-IqwSM3', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.VAN]: [ - 'ftransit18-wyXf7MTv', // Front Low - 'ftransit18-IIVI_pnX', // Front Bumper Side Right - 'ftransit18-G24AdP6r', // Lateral Full Right - 'ftransit18-FFP5b34o', // Rear Right - 'ftransit18-3dkU10af', // Rear Low - 'ftransit18-iu1Vj2Oa', // Rear Left - 'ftransit18-rsXWUN8X', // Lateral Full Left - 'ftransit18-5SiNC94w', // Front Bumper Side Left - 'all-IqwSM3', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.MINIVAN]: [ - 'tsienna20-YwrRNr9n', // Front Low - 'tsienna20-xtDcn3GS', // Front Fender Right - 'tsienna20-uIHdpQ9y', // Lateral Full Right - 'tsienna20--a2RmRcs', // Rear Right - 'tsienna20-qA3aAUUq', // Rear - 'tsienna20-1n_z8bYy', // Rear Left - 'tsienna20-4ihRwDkS', // Lateral Full Left - 'tsienna20-gkvZE2c7', // Front Fender Left - 'tsienna20-is1tpnqR', // Front Roof Left - 'all-IqwSM3', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.PICKUP]: [ - 'ff150-zXbg0l3z', // Front Low - 'ff150-OviO2DlY', // Front Fender Right - 'ff150-_UIadfVL', // Lateral Full Right - 'ff150-t3KBMPeD', // Rear Right - 'ff150-3dkU10af', // Rear Low - 'ff150--xPZZd83', // Rear Left - 'ff150-GOx2s_9L', // Lateral Full Left - 'ff150-wO_fJ3DL', // Front Fender Left - 'ff150-Ttsc7q6V', // Front Roof Left - 'all-IqwSM3', // Front Seats - 'all-rSvk2C', // Dashboard - ], - }, - [SteeringWheelPosition.RIGHT]: { - [VehicleType.SUV]: [ - 'jgc21-QIvfeg0X', // Front Low - 'jgc21-imomJ2V0', // Front Roof Left - 'jgc21-QIkcNhc_', // Front Fender Left - 'jgc21-TEoi50Ff', // Lateral Full Left - 'jgc21-ezXzTRkj', // Rear Left - 'jgc21-TyJPUs8E', // Rear Low - 'jgc21-3JJvM7_B', // Rear Right - 'jgc21-1j-oTPag', // Lateral Full Right - 'jgc21-QwNQX0Cr', // Front Fender Right - 'all-T4HrF8KA', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.CUV]: [ - 'fesc20-H1dfdfvH', // Front Low - 'fesc20-6GPUkfYn', // Front Roof Left - 'fesc20-GdIxD-_N', // Front Fender Left - 'fesc20-26n47kaO', // Lateral Full Left - 'fesc20-dfICsfSV', // Rear Left - 'fesc20-xBFiEy-_', // Rear Low - 'fesc20-LZc7p2kK', // Rear Right - 'fesc20-HYz5ziHi', // Lateral Full Right - 'fesc20-CEGtqHkk', // Front Fender Right - 'all-T4HrF8KA', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.SEDAN]: [ - 'haccord-8YjMcu0D', // Front Low - 'haccord-oiY_yPTR', // Front Roof Left - 'haccord-2a8VfA8m', // Front Fender Left - 'haccord-_YnTubBA', // Lateral Full Left - 'haccord-GdWvsqrm', // Rear Left - 'haccord-6kYUBv_e', // Rear Low - 'haccord-Jq65fyD4', // Rear Right - 'haccord-PGr3RzzP', // Lateral Full Right - 'haccord-EfRIciFr', // Front Fender Right - 'all-T4HrF8KA', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.HATCHBACK]: [ - 'ffocus18-XlfgjQb9', // Front Low - 'ffocus18-ZXKOomlv', // Front Roof Left - 'ffocus18-GiTxaJUq', // Front Fender Left - 'ffocus18-6FX31ty1', // Lateral Full Left - 'ffocus18-9MeSIqp7', // Rear Left - 'ffocus18-L2UM_68Q', // Rear Low - 'ffocus18-jWOq2CNN', // Rear Right - 'ffocus18-FdsQDaTW', // Lateral Full Right - 'ffocus18-zgLKB-Do', // Front Fender Right - 'all-T4HrF8KA', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.VAN]: [ - 'ftransit18-wyXf7MTv', // Front Low - 'ftransit18-5SiNC94w', // Front Bumper Side Left - 'ftransit18-rsXWUN8X', // Lateral Full Left - 'ftransit18-iu1Vj2Oa', // Rear Left - 'ftransit18-3dkU10af', // Rear Low - 'ftransit18-FFP5b34o', // Rear Right - 'ftransit18-G24AdP6r', // Lateral Full Right - 'ftransit18-IIVI_pnX', // Front Bumper Side Right - 'all-T4HrF8KA', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.MINIVAN]: [ - 'tsienna20-YwrRNr9n', // Front Low - 'tsienna20-is1tpnqR', // Front Roof Left - 'tsienna20-gkvZE2c7', // Front Fender Left - 'tsienna20-4ihRwDkS', // Lateral Full Left - 'tsienna20-1n_z8bYy', // Rear Left - 'tsienna20-qA3aAUUq', // Rear - 'tsienna20--a2RmRcs', // Rear Right - 'tsienna20-uIHdpQ9y', // Lateral Full Right - 'tsienna20-xtDcn3GS', // Front Fender Right - 'all-T4HrF8KA', // Front Seats - 'all-rSvk2C', // Dashboard - ], - [VehicleType.PICKUP]: [ - 'ff150-zXbg0l3z', // Front Low - 'ff150-Ttsc7q6V', // Front Roof Left - 'ff150-wO_fJ3DL', // Front Fender Left - 'ff150-GOx2s_9L', // Lateral Full Left - 'ff150--xPZZd83', // Rear Left - 'ff150-3dkU10af', // Rear Low - 'ff150-t3KBMPeD', // Rear Right - 'ff150-_UIadfVL', // Lateral Full Right - 'ff150-OviO2DlY', // Front Fender Right - 'all-T4HrF8KA', // Front Seats - 'all-rSvk2C', // Dashboard - ], - }, -}; - -export const AppConfig: CaptureAppConfig = { - enforceOrientation: DeviceOrientation.LANDSCAPE, - defaultVehicleType: VehicleType.CUV, - allowManualLogin: process.env['REACT_APP_ALLOW_LOGIN'] === 'true', - allowVehicleTypeSelection: true, - fetchFromSearchParams: true, - enableAddDamage: false, - useLiveCompliance: true, - allowSkipRetake: process.env['REACT_APP_ALLOW_SKIP_RETAKE'] === 'true', - allowCreateInspection: process.env['REACT_APP_ALLOW_CREATE_INSPECTION'] === 'true', - createInspectionOptions: { tasks: [TaskName.DAMAGE_DETECTION, TaskName.WHEEL_ANALYSIS] }, - apiDomain: getEnvOrThrow('REACT_APP_API_DOMAIN'), - requiredApiPermissions: [ - MonkApiPermission.TASK_COMPLIANCES, - MonkApiPermission.TASK_DAMAGE_DETECTION, - MonkApiPermission.TASK_DAMAGE_IMAGES_OCR, - MonkApiPermission.TASK_WHEEL_ANALYSIS, - MonkApiPermission.INSPECTION_CREATE, - MonkApiPermission.INSPECTION_READ, - MonkApiPermission.INSPECTION_UPDATE, - MonkApiPermission.INSPECTION_WRITE, - ], - enableSteeringWheelPosition: true, - defaultSteeringWheelPosition: SteeringWheelPosition.LEFT, - tasksBySight: uniq( - flatten( - Object.values(sightIds).map((sightIdsByVehicleType) => Object.values(sightIdsByVehicleType)), - ), - ).reduce( - (prev, curr) => ({ - ...prev, - [curr]: - process.env['REACT_APP_DISABLE_HINL'] === 'true' - ? sights[curr].tasks - : [...sights[curr].tasks, TaskName.HUMAN_IN_THE_LOOP], - }), - {}, - ), - sights: sightIds, - complianceIssuesPerSight: { - 'all-IqwSM3': IQA_COMPLIANCE_ISSUES, - 'all-rSvk2C': IQA_COMPLIANCE_ISSUES, - 'all-T4HrF8KA': IQA_COMPLIANCE_ISSUES, - 'jgc21-imomJ2V0': frontRoofLeftComplianceIssues, - 'fesc20-6GPUkfYn': frontRoofLeftComplianceIssues, - 'haccord-oiY_yPTR': frontRoofLeftComplianceIssues, - 'ffocus18-ZXKOomlv': frontRoofLeftComplianceIssues, - 'tsienna20-is1tpnqR': frontRoofLeftComplianceIssues, - 'ff150-Ttsc7q6V': frontRoofLeftComplianceIssues, - 'ff150-3dkU10af': IQA_COMPLIANCE_ISSUES, - }, - complianceIssues, -}; diff --git a/apps/drive-app/src/index.tsx b/apps/drive-app/src/index.tsx index 4f6da940e..dcf7942ba 100644 --- a/apps/drive-app/src/index.tsx +++ b/apps/drive-app/src/index.tsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'; import { MonitoringProvider } from '@monkvision/monitoring'; import { AnalyticsProvider } from '@monkvision/analytics'; import { Auth0Provider } from '@auth0/auth0-react'; -import { getEnvOrThrow, MonkThemeProvider } from '@monkvision/common'; +import { getEnvOrThrow } from '@monkvision/common'; import { sentryMonitoringAdapter } from './sentry'; import { posthogAnalyticsAdapter } from './posthog'; import { AppRouter } from './components'; @@ -22,9 +22,7 @@ ReactDOM.render( prompt: 'login', }} > - - - + , diff --git a/apps/drive-app/src/local-config.json b/apps/drive-app/src/local-config.json new file mode 100644 index 000000000..d92ed9e15 --- /dev/null +++ b/apps/drive-app/src/local-config.json @@ -0,0 +1,239 @@ +{ + "allowSkipRetake": true, + "enableAddDamage": false, + "allowVehicleTypeSelection": true, + "allowManualLogin": true, + "fetchFromSearchParams": true, + "allowCreateInspection": true, + "createInspectionOptions": { + "tasks": ["damage_detection", "wheel_analysis"] + }, + "apiDomain": "api.preview.monk.ai/v1", + "startTasksOnComplete": true, + "showCloseButton": false, + "enforceOrientation": "landscape", + "maxUploadDurationWarning": 15000, + "useAdaptiveImageQuality": true, + "format": "image/jpeg", + "quality": 0.6, + "resolution": "4K", + "allowImageUpscaling": false, + "enableCompliance": true, + "useLiveCompliance": true, + "complianceIssues": [ + "blurriness", + "underexposure", + "overexposure", + "lens_flare", + "reflections", + "unknown_sight", + "unknown_viewpoint", + "no_vehicle", + "wrong_center_part", + "missing_parts", + "hidden_parts", + "missing" + ], + "complianceIssuesPerSight": { + "all-IqwSM3": ["blurriness", "underexposure", "overexposure", "lens_flare"], + "all-rSvk2C": ["blurriness", "underexposure", "overexposure", "lens_flare"], + "all-T4HrF8KA": ["blurriness", "underexposure", "overexposure", "lens_flare"] + }, + "defaultVehicleType": "cuv", + "enableSteeringWheelPosition": true, + "defaultSteeringWheelPosition": "left", + "sights": { + "left": { + "suv": [ + "jgc21-QIvfeg0X", + "jgc21-QwNQX0Cr", + "jgc21-1j-oTPag", + "jgc21-3JJvM7_B", + "jgc21-TyJPUs8E", + "jgc21-ezXzTRkj", + "jgc21-TEoi50Ff", + "jgc21-QIkcNhc_", + "jgc21-imomJ2V0", + "all-IqwSM3", + "all-rSvk2C" + ], + "cuv": [ + "fesc20-H1dfdfvH", + "fesc20-CEGtqHkk", + "fesc20-HYz5ziHi", + "fesc20-LZc7p2kK", + "fesc20-xBFiEy-_", + "fesc20-dfICsfSV", + "fesc20-26n47kaO", + "fesc20-GdIxD-_N", + "fesc20-6GPUkfYn", + "all-IqwSM3", + "all-rSvk2C" + ], + "sedan": [ + "haccord-8YjMcu0D", + "haccord-EfRIciFr", + "haccord-PGr3RzzP", + "haccord-Jq65fyD4", + "haccord-6kYUBv_e", + "haccord-GdWvsqrm", + "haccord-_YnTubBA", + "haccord-2a8VfA8m", + "haccord-oiY_yPTR", + "all-IqwSM3", + "all-rSvk2C" + ], + "hatchback": [ + "ffocus18-XlfgjQb9", + "ffocus18-zgLKB-Do", + "ffocus18-FdsQDaTW", + "ffocus18-jWOq2CNN", + "ffocus18-L2UM_68Q", + "ffocus18-9MeSIqp7", + "ffocus18-6FX31ty1", + "ffocus18-GiTxaJUq", + "ffocus18-ZXKOomlv", + "all-IqwSM3", + "all-rSvk2C" + ], + "van": [ + "ftransit18-wyXf7MTv", + "ftransit18-IIVI_pnX", + "ftransit18-G24AdP6r", + "ftransit18-FFP5b34o", + "ftransit18-3dkU10af", + "ftransit18-iu1Vj2Oa", + "ftransit18-rsXWUN8X", + "ftransit18-5SiNC94w", + "all-IqwSM3", + "all-rSvk2C" + ], + "minivan": [ + "tsienna20-YwrRNr9n", + "tsienna20-xtDcn3GS", + "tsienna20-uIHdpQ9y", + "tsienna20--a2RmRcs", + "tsienna20-qA3aAUUq", + "tsienna20-1n_z8bYy", + "tsienna20-4ihRwDkS", + "tsienna20-gkvZE2c7", + "tsienna20-is1tpnqR", + "all-IqwSM3", + "all-rSvk2C" + ], + "pickup": [ + "ff150-zXbg0l3z", + "ff150-OviO2DlY", + "ff150-_UIadfVL", + "ff150-t3KBMPeD", + "ff150-3dkU10af", + "ff150--xPZZd83", + "ff150-GOx2s_9L", + "ff150-wO_fJ3DL", + "ff150-Ttsc7q6V", + "all-IqwSM3", + "all-rSvk2C" + ] + }, + "right": { + "suv": [ + "jgc21-QIvfeg0X", + "jgc21-imomJ2V0", + "jgc21-QIkcNhc_", + "jgc21-TEoi50Ff", + "jgc21-ezXzTRkj", + "jgc21-TyJPUs8E", + "jgc21-3JJvM7_B", + "jgc21-1j-oTPag", + "jgc21-QwNQX0Cr", + "all-T4HrF8KA", + "all-rSvk2C" + ], + "cuv": [ + "fesc20-H1dfdfvH", + "fesc20-6GPUkfYn", + "fesc20-GdIxD-_N", + "fesc20-26n47kaO", + "fesc20-dfICsfSV", + "fesc20-xBFiEy-_", + "fesc20-LZc7p2kK", + "fesc20-HYz5ziHi", + "fesc20-CEGtqHkk", + "all-T4HrF8KA", + "all-rSvk2C" + ], + "sedan": [ + "haccord-8YjMcu0D", + "haccord-oiY_yPTR", + "haccord-2a8VfA8m", + "haccord-_YnTubBA", + "haccord-GdWvsqrm", + "haccord-6kYUBv_e", + "haccord-Jq65fyD4", + "haccord-PGr3RzzP", + "haccord-EfRIciFr", + "all-T4HrF8KA", + "all-rSvk2C" + ], + "hatchback": [ + "ffocus18-XlfgjQb9", + "ffocus18-ZXKOomlv", + "ffocus18-GiTxaJUq", + "ffocus18-6FX31ty1", + "ffocus18-9MeSIqp7", + "ffocus18-L2UM_68Q", + "ffocus18-jWOq2CNN", + "ffocus18-FdsQDaTW", + "ffocus18-zgLKB-Do", + "all-T4HrF8KA", + "all-rSvk2C" + ], + "van": [ + "ftransit18-wyXf7MTv", + "ftransit18-5SiNC94w", + "ftransit18-rsXWUN8X", + "ftransit18-iu1Vj2Oa", + "ftransit18-3dkU10af", + "ftransit18-FFP5b34o", + "ftransit18-G24AdP6r", + "ftransit18-IIVI_pnX", + "all-T4HrF8KA", + "all-rSvk2C" + ], + "minivan": [ + "tsienna20-YwrRNr9n", + "tsienna20-is1tpnqR", + "tsienna20-gkvZE2c7", + "tsienna20-4ihRwDkS", + "tsienna20-1n_z8bYy", + "tsienna20-qA3aAUUq", + "tsienna20--a2RmRcs", + "tsienna20-uIHdpQ9y", + "tsienna20-xtDcn3GS", + "all-T4HrF8KA", + "all-rSvk2C" + ], + "pickup": [ + "ff150-zXbg0l3z", + "ff150-Ttsc7q6V", + "ff150-wO_fJ3DL", + "ff150-GOx2s_9L", + "ff150--xPZZd83", + "ff150-3dkU10af", + "ff150-t3KBMPeD", + "ff150-_UIadfVL", + "ff150-OviO2DlY", + "all-T4HrF8KA", + "all-rSvk2C" + ] + } + }, + "requiredApiPermissions": [ + "monk_core_api:compliances", + "monk_core_api:damage_detection", + "monk_core_api:wheel_analysis", + "monk_core_api:inspections:read", + "monk_core_api:inspections:update", + "monk_core_api:inspections:write" + ] +} diff --git a/apps/drive-app/test/pages/PhotoCapturePage.test.tsx b/apps/drive-app/test/pages/PhotoCapturePage.test.tsx index a59eade85..ba8c06b71 100644 --- a/apps/drive-app/test/pages/PhotoCapturePage.test.tsx +++ b/apps/drive-app/test/pages/PhotoCapturePage.test.tsx @@ -1,16 +1,3 @@ -import { Sight, TaskName, VehicleType } from '@monkvision/types'; - -jest.mock('../../src/config', () => ({ - getSights: jest.fn(() => [ - { id: 'test-1', tasks: [TaskName.DAMAGE_DETECTION] }, - { id: 'test-2', tasks: [TaskName.DAMAGE_DETECTION, TaskName.WHEEL_ANALYSIS] }, - ]), - getTasksBySight: jest.fn(() => ({ - test: [TaskName.DAMAGE_DETECTION, TaskName.HUMAN_IN_THE_LOOP], - })), - enableCompliancePerSight: { hello: 'world' }, -})); - import { render } from '@testing-library/react'; import { expectPropsOnChildMock } from '@monkvision/test-utils'; import { PhotoCapture } from '@monkvision/inspection-capture-web'; diff --git a/apps/lux-demo-app/.env-cmdrc.json b/apps/lux-demo-app/.env-cmdrc.json index 33cb4c456..ba3dc3036 100644 --- a/apps/lux-demo-app/.env-cmdrc.json +++ b/apps/lux-demo-app/.env-cmdrc.json @@ -4,48 +4,20 @@ "HTTPS": "true", "ESLINT_NO_DEV_ERRORS": "true", "REACT_APP_ENVIRONMENT": "local", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "lux-demo-app-local", + "REACT_APP_USE_LOCAL_CONFIG": "true", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", - "REACT_APP_SENTRY_DEBUG": "true", - "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" - }, - "development": { - "REACT_APP_ENVIRONMENT": "development", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", - "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", - "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", - "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", - "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", - "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" - }, - "staging": { - "REACT_APP_ENVIRONMENT": "staging", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", - "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", - "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", - "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", - "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", - "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" + "REACT_APP_SENTRY_DEBUG": "true" }, "preview": { "REACT_APP_ENVIRONMENT": "preview", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "lux-demo-app-preview", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", - "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", - "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" - }, - "backend-staging-qa": { - "REACT_APP_ENVIRONMENT": "backend-staging-qa", - "REACT_APP_API_DOMAIN": "api.staging.monk.ai/v1", - "REACT_APP_AUTH_DOMAIN": "idp.staging.monk.ai", - "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", - "REACT_APP_AUTH_CLIENT_ID": "DAeZWqeeOfgItYBcQzFeFwSrlvmUdN7L", - "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", - "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.staging.monk.ai" + "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720" } } diff --git a/apps/lux-demo-app/src/components/App.tsx b/apps/lux-demo-app/src/components/App.tsx index eeccadf35..763f3573d 100644 --- a/apps/lux-demo-app/src/components/App.tsx +++ b/apps/lux-demo-app/src/components/App.tsx @@ -1,25 +1,32 @@ import { Outlet, useNavigate } from 'react-router-dom'; -import { MonkAppStateProvider, MonkProvider, useMonkTheme } from '@monkvision/common'; +import { getEnvOrThrow, MonkProvider } from '@monkvision/common'; import { useTranslation } from 'react-i18next'; +import { LiveConfigAppProvider } from '@monkvision/common-ui-web'; +import { CaptureAppConfig } from '@monkvision/types'; import { Page } from '../pages'; -import { AppConfig } from '../config'; +import * as config from '../local-config.json'; +import { AppContainer } from './AppContainer'; + +const localConfig = + process.env['REACT_APP_USE_LOCAL_CONFIG'] === 'true' ? (config as CaptureAppConfig) : undefined; export function App() { const navigate = useNavigate(); const { i18n } = useTranslation(); - const { rootStyles } = useMonkTheme(); return ( - navigate(Page.CREATE_INSPECTION)} onFetchLanguage={(lang) => i18n.changeLanguage(lang)} + lang={i18n.language} > -
+ -
+
-
+ ); } diff --git a/apps/lux-demo-app/src/components/AppContainer.tsx b/apps/lux-demo-app/src/components/AppContainer.tsx new file mode 100644 index 000000000..2f56046c5 --- /dev/null +++ b/apps/lux-demo-app/src/components/AppContainer.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from 'react'; +import { MonkThemeProvider, useMonkAppState, useMonkTheme } from '@monkvision/common'; + +function RootStylesContainer({ children }: PropsWithChildren) { + const { rootStyles } = useMonkTheme(); + + return ( +
+ {children} +
+ ); +} + +export function AppContainer({ children }: PropsWithChildren) { + const { config } = useMonkAppState(); + + return ( + + {children} + + ); +} diff --git a/apps/lux-demo-app/src/components/index.ts b/apps/lux-demo-app/src/components/index.ts index c831a21d5..724f3116e 100644 --- a/apps/lux-demo-app/src/components/index.ts +++ b/apps/lux-demo-app/src/components/index.ts @@ -1,2 +1,3 @@ export * from './App'; export * from './AppRouter'; +export * from './AppContainer'; diff --git a/apps/lux-demo-app/src/config.ts b/apps/lux-demo-app/src/config.ts deleted file mode 100644 index 283eed77a..000000000 --- a/apps/lux-demo-app/src/config.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { - CaptureAppConfig, - CAR_COVERAGE_COMPLIANCE_ISSUES, - ComplianceIssue, - DEFAULT_COMPLIANCE_ISSUES, - DeviceOrientation, - MonkApiPermission, - TaskName, - VehicleType, - ZOOM_LEVEL_COMPLIANCE_ISSUES, -} from '@monkvision/types'; -import { getEnvOrThrow } from '@monkvision/common'; - -export const AppConfig: CaptureAppConfig = { - enforceOrientation: DeviceOrientation.LANDSCAPE, - defaultVehicleType: VehicleType.CUV, - allowManualLogin: true, - allowVehicleTypeSelection: true, - fetchFromSearchParams: true, - allowCreateInspection: true, - useLiveCompliance: true, - allowSkipRetake: true, - enableAddDamage: false, - createInspectionOptions: { tasks: [TaskName.DAMAGE_DETECTION, TaskName.WHEEL_ANALYSIS] }, - apiDomain: getEnvOrThrow('REACT_APP_API_DOMAIN'), - requiredApiPermissions: [ - MonkApiPermission.TASK_COMPLIANCES, - MonkApiPermission.TASK_DAMAGE_DETECTION, - MonkApiPermission.TASK_DAMAGE_IMAGES_OCR, - MonkApiPermission.TASK_WHEEL_ANALYSIS, - MonkApiPermission.INSPECTION_CREATE, - MonkApiPermission.INSPECTION_READ, - MonkApiPermission.INSPECTION_UPDATE, - MonkApiPermission.INSPECTION_WRITE, - ], - enableSteeringWheelPosition: false, - sights: { - [VehicleType.SUV]: [ - 'jgc21-QIvfeg0X', - 'jgc21-__JKllz9', - 'jgc21-QwNQX0Cr', - 'jgc21-1j-oTPag', - 'jgc21-JerG7oW5', - 'jgc21-3JJvM7_B', - 'jgc21-TyJPUs8E', - 'jgc21-ezXzTRkj', - 'jgc21-ESc0HCzy', - 'jgc21-TEoi50Ff', - 'jgc21-imomJ2V0', - 'jgc21-zCrDwYWE', - 'jgc21-QIkcNhc_', - ], - [VehicleType.CUV]: [ - 'fesc20-H1dfdfvH', - 'fesc20-Wzdtgqqz', - 'fesc20-CEGtqHkk', - 'fesc20-HYz5ziHi', - 'fesc20-r_UeXQRO', - 'fesc20-LZc7p2kK', - 'fesc20-xBFiEy-_', - 'fesc20-dfICsfSV', - 'fesc20-P470Q-jm', - 'fesc20-26n47kaO', - 'fesc20-6GPUkfYn', - 'fesc20-LTe3X2bg', - 'fesc20-GdIxD-_N', - ], - [VehicleType.SEDAN]: [ - 'haccord-8YjMcu0D', - 'haccord-Z84erkMb', - 'haccord-EfRIciFr', - 'haccord-PGr3RzzP', - 'haccord-sorgeRJ7', - 'haccord-Jq65fyD4', - 'haccord-6kYUBv_e', - 'haccord-GdWvsqrm', - 'haccord-Qel0qUky', - 'haccord-_YnTubBA', - 'haccord-oiY_yPTR', - 'haccord-hsCc_Nct', - 'haccord-2a8VfA8m', - ], - [VehicleType.HATCHBACK]: [ - 'ffocus18-XlfgjQb9', - 'ffocus18-lRDlWiwR', - 'ffocus18-zgLKB-Do', - 'ffocus18-FdsQDaTW', - 'ffocus18-Wo8PkcLF', - 'ffocus18-jWOq2CNN', - 'ffocus18-L2UM_68Q', - 'ffocus18-9MeSIqp7', - 'ffocus18-vFR9PKjB', - 'ffocus18-6FX31ty1', - 'ffocus18-ZXKOomlv', - 'ffocus18-43ljK5xC', - 'ffocus18-GiTxaJUq', - ], - [VehicleType.VAN]: [ - 'ftransit18-wyXf7MTv', - 'ftransit18-IIVI_pnX', - 'front-lateral-right', - 'ftransit18-G24AdP6r', - 'ftransit18-qmLP7A-b', - 'ftransit18-FFP5b34o', - 'ftransit18-3dkU10af', - 'ftransit18-iu1Vj2Oa', - 'ftransit18-klUp8BS4', - 'ftransit18-rsXWUN8X', - 'ftransit18-5SiNC94w', - 'ftransit18-6X8IAjy0', - ], - [VehicleType.MINIVAN]: [ - 'tsienna20-YwrRNr9n', - 'tsienna20-KHB_Cd9k', - 'tsienna20-xtDcn3GS', - 'tsienna20-uIHdpQ9y', - 'tsienna20-QIMXlb0L', - 'tsienna20--a2RmRcs', - 'tsienna20-qA3aAUUq', - 'tsienna20-1n_z8bYy', - 'tsienna20-ouPvuX-j', - 'tsienna20-4ihRwDkS', - 'tsienna20-is1tpnqR', - 'tsienna20-TI4TVvT9', - 'tsienna20-gkvZE2c7', - ], - [VehicleType.PICKUP]: [ - 'ff150-zXbg0l3z', - 'ff150-gFp78fQO', - 'ff150-OviO2DlY', - 'ff150-_UIadfVL', - 'ff150-2WUJ179s', - 'ff150-t3KBMPeD', - 'ff150-3dkU10af', - 'ff150-xbOhu7nK', - 'ff150--xPZZd83', - 'ff150-GOx2s_9L', - 'ff150-Ttsc7q6V', - 'ff150-KgHVkQBW', - 'ff150-wO_fJ3DL', - ], - }, - complianceIssues: DEFAULT_COMPLIANCE_ISSUES.filter( - (issue) => - ![ - ComplianceIssue.WRONG_ANGLE, - ComplianceIssue.TOO_ZOOMED, - ComplianceIssue.NOT_ZOOMED_ENOUGH, - ].includes(issue), - ), - complianceIssuesPerSight: { - 'ff150-3dkU10af': DEFAULT_COMPLIANCE_ISSUES.filter( - (issue) => - ![...ZOOM_LEVEL_COMPLIANCE_ISSUES, ...CAR_COVERAGE_COMPLIANCE_ISSUES].includes(issue), - ), - }, - palette: { - primary: { - xdark: '#663d00', - dark: '#b36b00', - base: '#ff9900', - light: '#ffb84d', - xlight: '#ffd699', - }, - secondary: { - xdark: '#082c48', - dark: '#0e4d7e', - base: '#146eb4', - light: '#5b9acb', - xlight: '#a1c5e1', - }, - }, -}; diff --git a/apps/lux-demo-app/src/index.tsx b/apps/lux-demo-app/src/index.tsx index a08df416f..9a5832da1 100644 --- a/apps/lux-demo-app/src/index.tsx +++ b/apps/lux-demo-app/src/index.tsx @@ -2,13 +2,12 @@ import ReactDOM from 'react-dom'; import { MonitoringProvider } from '@monkvision/monitoring'; import { AnalyticsProvider } from '@monkvision/analytics'; import { Auth0Provider } from '@auth0/auth0-react'; -import { getEnvOrThrow, MonkThemeProvider } from '@monkvision/common'; +import { getEnvOrThrow } from '@monkvision/common'; import { sentryMonitoringAdapter } from './sentry'; import { posthogAnalyticsAdapter } from './posthog'; import { AppRouter } from './components'; import './index.css'; import './i18n'; -import { AppConfig } from './config'; ReactDOM.render( @@ -22,9 +21,7 @@ ReactDOM.render( prompt: 'login', }} > - - - + , diff --git a/apps/lux-demo-app/src/local-config.json b/apps/lux-demo-app/src/local-config.json new file mode 100644 index 000000000..7a222d723 --- /dev/null +++ b/apps/lux-demo-app/src/local-config.json @@ -0,0 +1,181 @@ +{ + "allowSkipRetake": true, + "enableAddDamage": false, + "allowVehicleTypeSelection": true, + "allowManualLogin": true, + "fetchFromSearchParams": true, + "allowCreateInspection": true, + "createInspectionOptions": { + "tasks": ["damage_detection", "wheel_analysis"] + }, + "apiDomain": "api.preview.monk.ai/v1", + "startTasksOnComplete": true, + "showCloseButton": false, + "enforceOrientation": "landscape", + "maxUploadDurationWarning": 15000, + "useAdaptiveImageQuality": true, + "format": "image/jpeg", + "quality": 0.6, + "resolution": "4K", + "allowImageUpscaling": false, + "enableCompliance": true, + "useLiveCompliance": true, + "complianceIssues": [ + "blurriness", + "underexposure", + "overexposure", + "lens_flare", + "reflections", + "unknown_sight", + "unknown_viewpoint", + "no_vehicle", + "wrong_center_part", + "missing_parts", + "hidden_parts", + "missing" + ], + "complianceIssuesPerSight": { + "ff150-3dkU10af": [ + "blurriness", + "underexposure", + "overexposure", + "lens_flare", + "reflections", + "missing" + ] + }, + "defaultVehicleType": "cuv", + "enableSteeringWheelPosition": false, + "sights": { + "suv": [ + "jgc21-QIvfeg0X", + "jgc21-__JKllz9", + "jgc21-QwNQX0Cr", + "jgc21-1j-oTPag", + "jgc21-JerG7oW5", + "jgc21-3JJvM7_B", + "jgc21-TyJPUs8E", + "jgc21-ezXzTRkj", + "jgc21-ESc0HCzy", + "jgc21-TEoi50Ff", + "jgc21-imomJ2V0", + "jgc21-zCrDwYWE", + "jgc21-QIkcNhc_" + ], + "cuv": [ + "fesc20-H1dfdfvH", + "fesc20-Wzdtgqqz", + "fesc20-CEGtqHkk", + "fesc20-HYz5ziHi", + "fesc20-r_UeXQRO", + "fesc20-LZc7p2kK", + "fesc20-xBFiEy-_", + "fesc20-dfICsfSV", + "fesc20-P470Q-jm", + "fesc20-26n47kaO", + "fesc20-6GPUkfYn", + "fesc20-LTe3X2bg", + "fesc20-GdIxD-_N" + ], + "sedan": [ + "haccord-8YjMcu0D", + "haccord-Z84erkMb", + "haccord-EfRIciFr", + "haccord-PGr3RzzP", + "haccord-sorgeRJ7", + "haccord-Jq65fyD4", + "haccord-6kYUBv_e", + "haccord-GdWvsqrm", + "haccord-Qel0qUky", + "haccord-_YnTubBA", + "haccord-oiY_yPTR", + "haccord-hsCc_Nct", + "haccord-2a8VfA8m" + ], + "hatchback": [ + "ffocus18-XlfgjQb9", + "ffocus18-lRDlWiwR", + "ffocus18-zgLKB-Do", + "ffocus18-FdsQDaTW", + "ffocus18-Wo8PkcLF", + "ffocus18-jWOq2CNN", + "ffocus18-L2UM_68Q", + "ffocus18-9MeSIqp7", + "ffocus18-vFR9PKjB", + "ffocus18-6FX31ty1", + "ffocus18-ZXKOomlv", + "ffocus18-43ljK5xC", + "ffocus18-GiTxaJUq" + ], + "van": [ + "ftransit18-wyXf7MTv", + "ftransit18-IIVI_pnX", + "front-lateral-right", + "ftransit18-G24AdP6r", + "ftransit18-qmLP7A-b", + "ftransit18-FFP5b34o", + "ftransit18-3dkU10af", + "ftransit18-iu1Vj2Oa", + "ftransit18-klUp8BS4", + "ftransit18-rsXWUN8X", + "ftransit18-5SiNC94w", + "ftransit18-6X8IAjy0" + ], + "minivan": [ + "tsienna20-YwrRNr9n", + "tsienna20-KHB_Cd9k", + "tsienna20-xtDcn3GS", + "tsienna20-uIHdpQ9y", + "tsienna20-QIMXlb0L", + "tsienna20--a2RmRcs", + "tsienna20-qA3aAUUq", + "tsienna20-1n_z8bYy", + "tsienna20-ouPvuX-j", + "tsienna20-4ihRwDkS", + "tsienna20-is1tpnqR", + "tsienna20-TI4TVvT9", + "tsienna20-gkvZE2c7" + ], + "pickup": [ + "ff150-zXbg0l3z", + "ff150-gFp78fQO", + "ff150-OviO2DlY", + "ff150-_UIadfVL", + "ff150-2WUJ179s", + "ff150-t3KBMPeD", + "ff150-3dkU10af", + "ff150-xbOhu7nK", + "ff150--xPZZd83", + "ff150-GOx2s_9L", + "ff150-Ttsc7q6V", + "ff150-KgHVkQBW", + "ff150-wO_fJ3DL" + ] + }, + "requiredApiPermissions": [ + "monk_core_api:compliances", + "monk_core_api:damage_detection", + "monk_core_api:images_ocr", + "monk_core_api:wheel_analysis", + "monk_core_api:inspections:create", + "monk_core_api:inspections:read", + "monk_core_api:inspections:update", + "monk_core_api:inspections:write" + ], + "palette": { + "primary": { + "xdark": "#66D300", + "dark": "#B36B00", + "base": "#FF9900", + "light": "#FFB84D", + "xlight": "#FFD699" + }, + "secondary": { + "xdark": "#082C48", + "dark": "#0E4D7E", + "base": "#146EB4", + "light": "#5B9ACB", + "xlight": "#A1C5E1" + } + } +} diff --git a/apps/renault-demo-app/.env-cmdrc.json b/apps/renault-demo-app/.env-cmdrc.json index e5f6c346c..a3dfdaec1 100644 --- a/apps/renault-demo-app/.env-cmdrc.json +++ b/apps/renault-demo-app/.env-cmdrc.json @@ -4,7 +4,8 @@ "HTTPS": "true", "ESLINT_NO_DEV_ERRORS": "true", "REACT_APP_ENVIRONMENT": "local", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "renault-demo-app-local", + "REACT_APP_USE_LOCAL_CONFIG": "true", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", @@ -12,27 +13,9 @@ "REACT_APP_SENTRY_DEBUG": "true", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" }, - "development": { - "REACT_APP_ENVIRONMENT": "development", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", - "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", - "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", - "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", - "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", - "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" - }, - "staging": { - "REACT_APP_ENVIRONMENT": "staging", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", - "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", - "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", - "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", - "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", - "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" - }, "preview": { "REACT_APP_ENVIRONMENT": "preview", - "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_LIVE_CONFIG_ID": "renault-demo-app-preview", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", diff --git a/apps/renault-demo-app/src/components/App.tsx b/apps/renault-demo-app/src/components/App.tsx index eeccadf35..763f3573d 100644 --- a/apps/renault-demo-app/src/components/App.tsx +++ b/apps/renault-demo-app/src/components/App.tsx @@ -1,25 +1,32 @@ import { Outlet, useNavigate } from 'react-router-dom'; -import { MonkAppStateProvider, MonkProvider, useMonkTheme } from '@monkvision/common'; +import { getEnvOrThrow, MonkProvider } from '@monkvision/common'; import { useTranslation } from 'react-i18next'; +import { LiveConfigAppProvider } from '@monkvision/common-ui-web'; +import { CaptureAppConfig } from '@monkvision/types'; import { Page } from '../pages'; -import { AppConfig } from '../config'; +import * as config from '../local-config.json'; +import { AppContainer } from './AppContainer'; + +const localConfig = + process.env['REACT_APP_USE_LOCAL_CONFIG'] === 'true' ? (config as CaptureAppConfig) : undefined; export function App() { const navigate = useNavigate(); const { i18n } = useTranslation(); - const { rootStyles } = useMonkTheme(); return ( - navigate(Page.CREATE_INSPECTION)} onFetchLanguage={(lang) => i18n.changeLanguage(lang)} + lang={i18n.language} > -
+ -
+
-
+ ); } diff --git a/apps/renault-demo-app/src/components/AppContainer.tsx b/apps/renault-demo-app/src/components/AppContainer.tsx new file mode 100644 index 000000000..2f56046c5 --- /dev/null +++ b/apps/renault-demo-app/src/components/AppContainer.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from 'react'; +import { MonkThemeProvider, useMonkAppState, useMonkTheme } from '@monkvision/common'; + +function RootStylesContainer({ children }: PropsWithChildren) { + const { rootStyles } = useMonkTheme(); + + return ( +
+ {children} +
+ ); +} + +export function AppContainer({ children }: PropsWithChildren) { + const { config } = useMonkAppState(); + + return ( + + {children} + + ); +} diff --git a/apps/renault-demo-app/src/components/index.ts b/apps/renault-demo-app/src/components/index.ts index c831a21d5..724f3116e 100644 --- a/apps/renault-demo-app/src/components/index.ts +++ b/apps/renault-demo-app/src/components/index.ts @@ -1,2 +1,3 @@ export * from './App'; export * from './AppRouter'; +export * from './AppContainer'; diff --git a/apps/renault-demo-app/src/config.ts b/apps/renault-demo-app/src/config.ts deleted file mode 100644 index fb853e770..000000000 --- a/apps/renault-demo-app/src/config.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { - CaptureAppConfig, - CAR_COVERAGE_COMPLIANCE_ISSUES, - ComplianceIssue, - DEFAULT_COMPLIANCE_ISSUES, - DeviceOrientation, - MonkApiPermission, - TaskName, - VehicleType, - ZOOM_LEVEL_COMPLIANCE_ISSUES, -} from '@monkvision/types'; -import { getEnvOrThrow } from '@monkvision/common'; - -export const AppConfig: CaptureAppConfig = { - enforceOrientation: DeviceOrientation.LANDSCAPE, - defaultVehicleType: VehicleType.CUV, - allowManualLogin: true, - allowVehicleTypeSelection: true, - fetchFromSearchParams: true, - allowCreateInspection: true, - useLiveCompliance: true, - allowSkipRetake: true, - createInspectionOptions: { tasks: [TaskName.DAMAGE_DETECTION, TaskName.WHEEL_ANALYSIS] }, - apiDomain: getEnvOrThrow('REACT_APP_API_DOMAIN'), - requiredApiPermissions: [ - MonkApiPermission.TASK_COMPLIANCES, - MonkApiPermission.TASK_DAMAGE_DETECTION, - MonkApiPermission.TASK_DAMAGE_IMAGES_OCR, - MonkApiPermission.TASK_WHEEL_ANALYSIS, - MonkApiPermission.INSPECTION_CREATE, - MonkApiPermission.INSPECTION_READ, - MonkApiPermission.INSPECTION_UPDATE, - MonkApiPermission.INSPECTION_WRITE, - ], - enableSteeringWheelPosition: false, - sights: { - [VehicleType.SUV]: [ - 'jgc21-QIvfeg0X', - 'jgc21-KyUUVU2P', - 'jgc21-zCrDwYWE', - 'jgc21-z15ZdJL6', - 'jgc21-RE3li6rE', - 'jgc21-omlus7Ui', - 'jgc21-m2dDoMup', - 'jgc21-3gjMwvQG', - 'jgc21-ezXzTRkj', - 'jgc21-tbF2Ax8v', - 'jgc21-3JJvM7_B', - 'jgc21-RAVpqaE4', - 'jgc21-F-PPd4qN', - 'jgc21-XXh8GWm8', - 'jgc21-TRN9Des4', - 'jgc21-s7WDTRmE', - 'jgc21-__JKllz9', - ], - [VehicleType.CUV]: [ - 'fesc20-H1dfdfvH', - 'fesc20-WMUaKDp1', - 'fesc20-LTe3X2bg', - 'fesc20-WIQsf_gX', - 'fesc20-hp3Tk53x', - 'fesc20-fOt832UV', - 'fesc20-NLdqASzl', - 'fesc20-4Wqx52oU', - 'fesc20-dfICsfSV', - 'fesc20-X8k7UFGf', - 'fesc20-LZc7p2kK', - 'fesc20-5Ts1UkPT', - 'fesc20-gg1Xyrpu', - 'fesc20-P0oSEh8p', - 'fesc20-j3H8Z415', - 'fesc20-dKVLig1i', - 'fesc20-Wzdtgqqz', - ], - [VehicleType.SEDAN]: [ - 'haccord-8YjMcu0D', - 'haccord-DUPnw5jj', - 'haccord-hsCc_Nct', - 'haccord-GQcZz48C', - 'haccord-QKfhXU7o', - 'haccord-mdZ7optI', - 'haccord-bSAv3Hrj', - 'haccord-W-Bn3bU1', - 'haccord-GdWvsqrm', - 'haccord-ps7cWy6K', - 'haccord-Jq65fyD4', - 'haccord-OXYy5gET', - 'haccord-5LlCuIfL', - 'haccord-Gtt0JNQl', - 'haccord-cXSAj2ez', - 'haccord-KN23XXkX', - 'haccord-Z84erkMb', - ], - [VehicleType.HATCHBACK]: [ - 'ffocus18-XlfgjQb9', - 'ffocus18-3TiCVAaN', - 'ffocus18-43ljK5xC', - 'ffocus18-x_1SE7X-', - 'ffocus18-QKfhXU7o', - 'ffocus18-yo9eBDW6', - 'ffocus18-cPUyM28L', - 'ffocus18-S3kgFOBb', - 'ffocus18-9MeSIqp7', - 'ffocus18-X2LDjCvr', - 'ffocus18-jWOq2CNN', - 'ffocus18-P2jFq1Ea', - 'ffocus18-U3Bcfc2Q', - 'ffocus18-ts3buSD1', - 'ffocus18-cXSAj2ez', - 'ffocus18-KkeGvT-F', - 'ffocus18-lRDlWiwR', - ], - [VehicleType.VAN]: [ - 'ftransit18-wyXf7MTv', - 'ftransit18-UNAZWJ-r', - 'ftransit18-5SiNC94w', - 'ftransit18-Y0vPhBVF', - 'ftransit18-xyp1rU0h', - 'ftransit18-6khKhof0', - 'ftransit18-eXJDDYmE', - 'ftransit18-3Sbfx_KZ', - 'ftransit18-iu1Vj2Oa', - 'ftransit18-aA2K898S', - 'ftransit18-NwBMLo3Z', - 'ftransit18-cf0e-pcB', - 'ftransit18-FFP5b34o', - 'ftransit18-RJ2D7DNz', - 'ftransit18-3fnjrISV', - 'ftransit18-eztNpSRX', - 'ftransit18-TkXihCj4', - 'ftransit18-4NMPqEV6', - 'ftransit18-IIVI_pnX', - ], - [VehicleType.MINIVAN]: [ - 'tsienna20-YwrRNr9n', - 'tsienna20-HykkFbXf', - 'tsienna20-TI4TVvT9', - 'tsienna20-65mfPdRD', - 'tsienna20-Ia0SGJ6z', - 'tsienna20-1LNxhgCR', - 'tsienna20-U_FqYq-a', - 'tsienna20-670P2H2V', - 'tsienna20-1n_z8bYy', - 'tsienna20-qA3aAUUq', - 'tsienna20--a2RmRcs', - 'tsienna20-SebsoqJm', - 'tsienna20-u57qDaN_', - 'tsienna20-Rw0Gtt7O', - 'tsienna20-TibS83Qr', - 'tsienna20-cI285Gon', - 'tsienna20-KHB_Cd9k', - ], - [VehicleType.PICKUP]: [ - 'ff150-zXbg0l3z', - 'ff150-3he9UOwy', - 'ff150-KgHVkQBW', - 'ff150-FqbrFVr2', - 'ff150-g_xBOOS2', - 'ff150-vwE3yqdh', - 'ff150-V-xzfWsx', - 'ff150-ouGGtRnf', - 'ff150--xPZZd83', - 'ff150-nF_oFvhI', - 'ff150-t3KBMPeD', - 'ff150-3rM9XB0Z', - 'ff150-eOjyMInj', - 'ff150-18YVVN-G', - 'ff150-BmXfb-qD', - 'ff150-gFp78fQO', - 'ff150-7nvlys8r', - ], - }, - complianceIssues: DEFAULT_COMPLIANCE_ISSUES.filter( - (issue) => - ![ - ComplianceIssue.WRONG_ANGLE, - ComplianceIssue.TOO_ZOOMED, - ComplianceIssue.NOT_ZOOMED_ENOUGH, - ].includes(issue), - ), - complianceIssuesPerSight: { - 'ff150-nF_oFvhI': DEFAULT_COMPLIANCE_ISSUES.filter( - (issue) => - ![...ZOOM_LEVEL_COMPLIANCE_ISSUES, ...CAR_COVERAGE_COMPLIANCE_ISSUES].includes(issue), - ), - }, - palette: { - primary: { - xdark: '#33290A', - dark: '#997A1F', - base: '#DEA726', - light: '#FFE085', - xlight: '#FFF5D6', - }, - background: { - dark: '#070707', - base: '#171717', - light: '#2d2d2d', - }, - }, -}; diff --git a/apps/renault-demo-app/src/index.tsx b/apps/renault-demo-app/src/index.tsx index f0b111530..9a5832da1 100644 --- a/apps/renault-demo-app/src/index.tsx +++ b/apps/renault-demo-app/src/index.tsx @@ -2,11 +2,10 @@ import ReactDOM from 'react-dom'; import { MonitoringProvider } from '@monkvision/monitoring'; import { AnalyticsProvider } from '@monkvision/analytics'; import { Auth0Provider } from '@auth0/auth0-react'; -import { getEnvOrThrow, MonkThemeProvider } from '@monkvision/common'; +import { getEnvOrThrow } from '@monkvision/common'; import { sentryMonitoringAdapter } from './sentry'; import { posthogAnalyticsAdapter } from './posthog'; import { AppRouter } from './components'; -import { AppConfig } from './config'; import './index.css'; import './i18n'; @@ -22,9 +21,7 @@ ReactDOM.render( prompt: 'login', }} > - - - + , diff --git a/apps/renault-demo-app/src/local-config.json b/apps/renault-demo-app/src/local-config.json new file mode 100644 index 000000000..37f48dcab --- /dev/null +++ b/apps/renault-demo-app/src/local-config.json @@ -0,0 +1,196 @@ +{ + "allowSkipRetake": true, + "enableAddDamage": true, + "allowVehicleTypeSelection": true, + "allowManualLogin": true, + "fetchFromSearchParams": true, + "allowCreateInspection": true, + "createInspectionOptions": { + "tasks": ["damage_detection", "wheel_analysis"] + }, + "apiDomain": "api.preview.monk.ai/v1", + "startTasksOnComplete": true, + "showCloseButton": false, + "enforceOrientation": "landscape", + "maxUploadDurationWarning": 15000, + "useAdaptiveImageQuality": true, + "format": "image/jpeg", + "quality": 0.6, + "resolution": "4K", + "allowImageUpscaling": false, + "enableCompliance": true, + "useLiveCompliance": true, + "complianceIssues": [ + "blurriness", + "underexposure", + "overexposure", + "lens_flare", + "reflections", + "unknown_sight", + "unknown_viewpoint", + "no_vehicle", + "wrong_center_part", + "missing_parts", + "hidden_parts", + "missing" + ], + "complianceIssuesPerSight": { + "ff150-nF_oFvhI": [ + "blurriness", + "underexposure", + "overexposure", + "lens_flare", + "reflections", + "missing" + ] + }, + "defaultVehicleType": "cuv", + "enableSteeringWheelPosition": false, + "sights": { + "suv": [ + "jgc21-QIvfeg0X", + "jgc21-KyUUVU2P", + "jgc21-zCrDwYWE", + "jgc21-z15ZdJL6", + "jgc21-RE3li6rE", + "jgc21-omlus7Ui", + "jgc21-m2dDoMup", + "jgc21-3gjMwvQG", + "jgc21-ezXzTRkj", + "jgc21-tbF2Ax8v", + "jgc21-3JJvM7_B", + "jgc21-RAVpqaE4", + "jgc21-F-PPd4qN", + "jgc21-XXh8GWm8", + "jgc21-TRN9Des4", + "jgc21-s7WDTRmE", + "jgc21-__JKllz9" + ], + "cuv": [ + "fesc20-H1dfdfvH", + "fesc20-WMUaKDp1", + "fesc20-LTe3X2bg", + "fesc20-WIQsf_gX", + "fesc20-hp3Tk53x", + "fesc20-fOt832UV", + "fesc20-NLdqASzl", + "fesc20-4Wqx52oU", + "fesc20-dfICsfSV", + "fesc20-X8k7UFGf", + "fesc20-LZc7p2kK", + "fesc20-5Ts1UkPT", + "fesc20-gg1Xyrpu", + "fesc20-P0oSEh8p", + "fesc20-j3H8Z415", + "fesc20-dKVLig1i", + "fesc20-Wzdtgqqz" + ], + "sedan": [ + "haccord-8YjMcu0D", + "haccord-DUPnw5jj", + "haccord-hsCc_Nct", + "haccord-GQcZz48C", + "haccord-QKfhXU7o", + "haccord-mdZ7optI", + "haccord-bSAv3Hrj", + "haccord-W-Bn3bU1", + "haccord-GdWvsqrm", + "haccord-ps7cWy6K", + "haccord-Jq65fyD4", + "haccord-OXYy5gET", + "haccord-5LlCuIfL", + "haccord-Gtt0JNQl", + "haccord-cXSAj2ez", + "haccord-KN23XXkX", + "haccord-Z84erkMb" + ], + "hatchback": [ + "ffocus18-XlfgjQb9", + "ffocus18-3TiCVAaN", + "ffocus18-43ljK5xC", + "ffocus18-x_1SE7X-", + "ffocus18-QKfhXU7o", + "ffocus18-yo9eBDW6", + "ffocus18-cPUyM28L", + "ffocus18-S3kgFOBb", + "ffocus18-9MeSIqp7", + "ffocus18-X2LDjCvr", + "ffocus18-jWOq2CNN", + "ffocus18-P2jFq1Ea", + "ffocus18-U3Bcfc2Q", + "ffocus18-ts3buSD1", + "ffocus18-cXSAj2ez", + "ffocus18-KkeGvT-F", + "ffocus18-lRDlWiwR" + ], + "van": [ + "ftransit18-wyXf7MTv", + "ftransit18-UNAZWJ-r", + "ftransit18-5SiNC94w", + "ftransit18-Y0vPhBVF", + "ftransit18-xyp1rU0h", + "ftransit18-6khKhof0", + "ftransit18-eXJDDYmE", + "ftransit18-3Sbfx_KZ", + "ftransit18-iu1Vj2Oa", + "ftransit18-aA2K898S", + "ftransit18-NwBMLo3Z", + "ftransit18-cf0e-pcB", + "ftransit18-FFP5b34o", + "ftransit18-RJ2D7DNz", + "ftransit18-3fnjrISV", + "ftransit18-eztNpSRX", + "ftransit18-TkXihCj4", + "ftransit18-4NMPqEV6", + "ftransit18-IIVI_pnX" + ], + "minivan": [ + "tsienna20-YwrRNr9n", + "tsienna20-HykkFbXf", + "tsienna20-TI4TVvT9", + "tsienna20-65mfPdRD", + "tsienna20-Ia0SGJ6z", + "tsienna20-1LNxhgCR", + "tsienna20-U_FqYq-a", + "tsienna20-670P2H2V", + "tsienna20-1n_z8bYy", + "tsienna20-qA3aAUUq", + "tsienna20--a2RmRcs", + "tsienna20-SebsoqJm", + "tsienna20-u57qDaN_", + "tsienna20-Rw0Gtt7O", + "tsienna20-TibS83Qr", + "tsienna20-cI285Gon", + "tsienna20-KHB_Cd9k" + ], + "pickup": [ + "ff150-zXbg0l3z", + "ff150-3he9UOwy", + "ff150-KgHVkQBW", + "ff150-FqbrFVr2", + "ff150-g_xBOOS2", + "ff150-vwE3yqdh", + "ff150-V-xzfWsx", + "ff150-ouGGtRnf", + "ff150--xPZZd83", + "ff150-nF_oFvhI", + "ff150-t3KBMPeD", + "ff150-3rM9XB0Z", + "ff150-eOjyMInj", + "ff150-18YVVN-G", + "ff150-BmXfb-qD", + "ff150-gFp78fQO", + "ff150-7nvlys8r" + ] + }, + "requiredApiPermissions": [ + "monk_core_api:compliances", + "monk_core_api:damage_detection", + "monk_core_api:images_ocr", + "monk_core_api:wheel_analysis", + "monk_core_api:inspections:create", + "monk_core_api:inspections:read", + "monk_core_api:inspections:update", + "monk_core_api:inspections:write" + ] +} diff --git a/configs/test-utils/src/__mocks__/@monkvision/common.tsx b/configs/test-utils/src/__mocks__/@monkvision/common.tsx index 7d78b042d..5777da8b0 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/common.tsx +++ b/configs/test-utils/src/__mocks__/@monkvision/common.tsx @@ -100,6 +100,7 @@ export = { isMobileDevice: jest.fn(() => false), zlibCompress: jest.fn(() => ''), zlibDecompress: jest.fn(() => ''), + MonkAppStateProvider: jest.fn(() => <>), useMonkAppState: jest.fn(() => ({ loading: createMockLoadingState(), config: {}, diff --git a/configs/test-utils/src/__mocks__/@monkvision/network.ts b/configs/test-utils/src/__mocks__/@monkvision/network.ts index 96bf03a31..781ce9dd7 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/network.ts +++ b/configs/test-utils/src/__mocks__/@monkvision/network.ts @@ -6,6 +6,7 @@ const MonkApi = { addImage: jest.fn(() => Promise.resolve()), updateTaskStatus: jest.fn(() => Promise.resolve()), startInspectionTasks: jest.fn(() => Promise.resolve()), + getLiveConfig: jest.fn(() => Promise.resolve({})), }; export = { diff --git a/documentation/docs/configuration.md b/documentation/docs/configuration.md index 92d0e7e0f..ffa8e36b1 100644 --- a/documentation/docs/configuration.md +++ b/documentation/docs/configuration.md @@ -40,6 +40,7 @@ The following table lists the available configuration options in the `CaptureApp | useAdaptiveImageQuality | `boolean` | Boolean indicating if the image quality should be downgraded automatically in case of low connection. | | `true` | | showCloseButton | `boolean` | Indicates if the close button should be displayed in the HUD on top of the Camera preview. | | `false` | | startTasksOnComplete | `boolean | TaskName[]` | Value indicating if tasks should be started at the end of the inspection. See the `inspection-capture-web` package doc for more info. | | `true` | +| additionalTasks | `TaskName[]` | An optional list of additional tasks to run on every Sight of the inspection. | | | | tasksBySight | `Record` | Record associating each sight with a list of tasks to execute for it. If not provided, the default tasks of the sight will be used. | | | | resolution | `CameraResolution` | Indicates the resolution of the pictures taken by the Camera. | | `CameraResolution.UHD_4K` | | allowImageUpscaling | `boolean` | Allow images to be scaled up if the device does not support the specified resolution in the `resolution` prop. | | `false` | diff --git a/packages/common-ui-web/README.md b/packages/common-ui-web/README.md index 5f13edd9d..4e82dde57 100644 --- a/packages/common-ui-web/README.md +++ b/packages/common-ui-web/README.md @@ -134,6 +134,36 @@ function App() { --- +## Checkbox +### Description +Custom component implementing a simple checkbox. + +### Example +```tsx +import { useState } from 'react'; +import { Checkbox } from '@monkvision/common-ui-web'; + +function MyComponent() { + const [checked, setchecked] = useState(false); + + return ; +} +``` + +### Props +| Prop | Type | Description | Required | Default Value | +|----------------|----------------------------|---------------------------------------------------------------------------|----------|----------------------| +| checked | boolean | Boolean indicating if the checkbox is checked or not. | | `false` | +| disabled | boolean | Boolean indicating if the checkbox is disabed or not. | | `false` | +| onChange | (checked: boolean) => void | Callback called when the checkbox "checked" value is changed. | | | +| primaryColor | ColorProp | The background color of the checkbox when it is checked. | | `'primary-base'` | +| secondaryColor | ColorProp | The color of the checked icon when the checkbox is checked. | | `'text-primary'` | +| tertiaryColor | ColorProp | The color of the checkbox when it is not checked (background and border). | | `'background-light'` | +| labelColor | ColorProp | The color of the label. | | `'text-primary'` | +| label | string | The label of the checkbox. | | | + +--- + ## CreateInspection ### Description This component is a ready-to-use CreateInspection page that is used throughout the different Monk webapps to handle @@ -142,7 +172,6 @@ inspection creation. **Note : For this component to work properly, it must be the child of a `MonkAppStateProvider` component.** ### Example - ```tsx import { CreateInspection } from '@monkvision/common-ui-web'; import { useNavigate } from 'react-router-dom'; @@ -292,9 +321,44 @@ function App() { --- -## CreateInspection +## LiveConfigAppProvider ### Description -This component is a ready-to-use CreateInspection page that is used throughout the different Monk webapps to handle authentication. +This component is used in Monk web applications that support Live Configurations. It acts as both an automatic live +configuration fetcher and a MonkAppStateProvider. + +### Example + +```tsx +import React, { useCallback } from 'react'; +import { LiveConfigAppProvider } from '@monkvision/common-ui-web'; + +function App() { + return ( + + ... + + ); +} +``` + +### Props +This component accepts the same props as the `MonkAppStateProvider` component (except for the `config` prop which is +replaced by the live config). Please refer to the +[@monkvision/common package documentation](https://github.com/monkvision/monkjs/blob/main/packages/common/README/APP_UTILS.md) +for more details. + +| Prop | Type | Description | Required | Default Value | +|-------------|---------------------------------|-----------------------------------------------------------------------|----------|---------------| +| id | string | The ID of the application Live Config. | ✔️ | | +| localConfig | CaptureAppConfig | Use this prop to configure a configuration on your local environment. | | | +| lang | string | null | The language used by this component. | | `en` | + +--- + +## LoginPage +### Description +This component is a ready-to-use CreateInspection page that is used throughout the different Monk webapps to handle +authentication. ### Example @@ -463,6 +527,46 @@ function App() { --- +## TextField +### Description +Custom component implementing a simple one-liner text field. + +### Example + +```tsx +import { useState } from 'react'; +import { TextField } from '@monkvision/common-ui-web'; + +function App() { + const [value, setValue] = useState(''); + + return ; +} +``` + +### Props +| Prop | Type | Description | Required | Default Value | +|-----------------|-------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|----------|----------------------| +| type | 'email' | 'password' | 'tel' | 'text' | The type of the underlying HTMLInput element. | | `'text'` | +| value | string | The value of the text field. | | `''` | +| onChange | (newValue: string) => void | Callback called when the value of the text field changes. | | | +| disabled | boolean | Boolean indicating if the text field is disabled or not. | | `false` | +| highlighted | boolean | Boolean indicating if the input should be highlighted (ex: in case of errors). | | `false` | +| monospace | boolean | Boolean indicating if the font family of the input should be monospace. | | `false` | +| label | string | The label of the text field. | | `''` | +| placeholder | string | The placeholder of the input. | | `''` | +| unit | string | The unit symbol of the text field. | | | +| unitPosition | 'left' | 'right' | The position of the unit symbol. | | `'left'` | +| icon | IconName | The name of the icon on the left of the text field. | | | +| showClearButton | boolean | Boolean indicating if the button allowing the user to clear the field should be displayed or not. | | `true` | +| assistiveText | string | Assistive text label under the text field. | | | +| fixedWidth | number | Fixed width for the text field. If not set, the text field expands to the max width of its container. | | | +| focusColor | ColorProp | The accent color of the text field when focused. | | `'primary-base'` | +| neutralColor | ColorProp | The accent color of the text field when not focused. | | `'text-primary'` | +| backgroundColor | ColorProp | The background color of the text field. | | `'background-light'` | + +--- + ## VehicleTypeAsset ### Description This component displays an example image for the given vehicle type. diff --git a/packages/common-ui-web/src/components/Checkbox/Checkbox.styles.ts b/packages/common-ui-web/src/components/Checkbox/Checkbox.styles.ts new file mode 100644 index 000000000..f9bc3bbae --- /dev/null +++ b/packages/common-ui-web/src/components/Checkbox/Checkbox.styles.ts @@ -0,0 +1,39 @@ +import { Styles } from '@monkvision/types'; + +export const styles: Styles = { + container: { + display: 'flex', + alignItems: 'center', + }, + checkbox: { + width: 22, + height: 22, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + margin: 0, + padding: 0, + outline: 'none', + borderStyle: 'solid', + borderWidth: 1, + borderRadius: 3, + cursor: 'pointer', + }, + checkboxDisabled: { + opacity: 0.37, + cursor: 'default', + }, + interactiveOverlay: { + width: 44, + height: 44, + borderRadius: 99999, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + label: { + paddingLeft: 4, + fontSize: 16, + fontWeight: 500, + }, +}; diff --git a/packages/common-ui-web/src/components/Checkbox/Checkbox.tsx b/packages/common-ui-web/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 000000000..59771ec05 --- /dev/null +++ b/packages/common-ui-web/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,46 @@ +import { useInteractiveStatus } from '@monkvision/common'; +import { CheckboxProps, useCheckboxStyles } from './hooks'; +import { Icon } from '../../icons'; +import { styles } from './Checkbox.styles'; + +/** + * Custom component implementing a simple checkbox. + */ +export function Checkbox({ + checked = false, + disabled = false, + onChange, + primaryColor = 'primary-base', + secondaryColor = 'text-primary', + tertiaryColor = 'background-light', + labelColor = 'text-primary', + label, +}: CheckboxProps) { + const { status, eventHandlers } = useInteractiveStatus({ disabled }); + const { checkboxStyles, icon, interactiveOverlayStyle, labelStyle } = useCheckboxStyles({ + checked, + status, + primaryColor, + secondaryColor, + tertiaryColor, + labelColor, + }); + + return ( +
+
+ +
+ {label &&
{label}
} +
+ ); +} diff --git a/packages/common-ui-web/src/components/Checkbox/hooks.ts b/packages/common-ui-web/src/components/Checkbox/hooks.ts new file mode 100644 index 000000000..bf97b9151 --- /dev/null +++ b/packages/common-ui-web/src/components/Checkbox/hooks.ts @@ -0,0 +1,110 @@ +import { ColorProp, InteractiveStatus } from '@monkvision/types'; +import { changeAlpha, useMonkTheme } from '@monkvision/common'; +import { CSSProperties, useMemo } from 'react'; +import { styles } from './Checkbox.styles'; + +/** + * Props accepted by the Checkbox component. + */ +export interface CheckboxProps { + /** + * Boolean indicating if the checkbox is checked or not. + * + * @default false + */ + checked?: boolean; + /** + * Boolean indicating if the checkbox is disabed or not. + * + * @default false + */ + disabled?: boolean; + /** + * Callback called when the checkbox "isChecked" value is changed. + */ + onChange?: (checked: boolean) => void; + /** + * The background color of the checkbox when it is checked. + * + * @default 'primary-base' + */ + primaryColor?: ColorProp; + /** + * The color of the checked icon when the checkbox is checked. + * + * @default 'text-primary' + */ + secondaryColor?: ColorProp; + /** + * The color of the checkbox when it is not checked (background and border). + * + * @default 'background-light' + */ + tertiaryColor?: ColorProp; + /** + * The color of the label. + * + * @default 'text-primary' + */ + labelColor?: ColorProp; + /** + * The label of the checkbox. + */ + label?: string; +} + +export function useCheckboxStyles( + props: Required< + Pick< + CheckboxProps, + 'checked' | 'primaryColor' | 'secondaryColor' | 'tertiaryColor' | 'labelColor' + > + > & { status: InteractiveStatus }, +) { + const { utils } = useMonkTheme(); + + const colors = useMemo(() => { + const primary = utils.getColor(props.primaryColor); + const secondary = utils.getColor(props.secondaryColor); + const tertiary = utils.getColor(props.tertiaryColor); + const label = utils.getColor(props.labelColor); + return { + primary, + primary5: changeAlpha(primary, 0.05), + primary18: changeAlpha(primary, 0.18), + secondary, + tertiary, + tertiary5: changeAlpha(tertiary, 0.05), + tertiary18: changeAlpha(tertiary, 0.18), + tertiary50: changeAlpha(tertiary, 0.5), + label, + }; + }, [props.primaryColor, props.secondaryColor, props.tertiaryColor]); + + let interactiveOverlayBackgroundColor = 'transparent'; + if (props.status === InteractiveStatus.HOVERED) { + interactiveOverlayBackgroundColor = props.checked ? colors.primary5 : colors.tertiary5; + } else if (props.status === InteractiveStatus.ACTIVE) { + interactiveOverlayBackgroundColor = props.checked ? colors.primary18 : colors.tertiary18; + } + + return { + checkboxStyles: { + ...styles['checkbox'], + ...(props.status === InteractiveStatus.DISABLED ? styles['checkboxDisabled'] : {}), + backgroundColor: props.checked ? colors.primary : colors.tertiary50, + borderColor: props.checked ? colors.primary : colors.tertiary, + } as CSSProperties, + icon: { + primaryColor: colors.secondary, + }, + interactiveOverlayStyle: { + ...styles['interactiveOverlay'], + backgroundColor: interactiveOverlayBackgroundColor, + }, + labelStyle: { + ...styles['label'], + color: colors.label, + }, + }; +} diff --git a/packages/common-ui-web/src/components/Checkbox/index.ts b/packages/common-ui-web/src/components/Checkbox/index.ts new file mode 100644 index 000000000..8e69829af --- /dev/null +++ b/packages/common-ui-web/src/components/Checkbox/index.ts @@ -0,0 +1,2 @@ +export { Checkbox } from './Checkbox'; +export { type CheckboxProps } from './hooks'; diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.styles.ts b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.styles.ts new file mode 100644 index 000000000..af2f85857 --- /dev/null +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.styles.ts @@ -0,0 +1,16 @@ +import { Styles } from '@monkvision/types'; + +export const styles: Styles = { + container: { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + errorMessage: { + paddingBottom: 20, + maxWidth: '60%', + }, +}; diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx new file mode 100644 index 000000000..c1d857275 --- /dev/null +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx @@ -0,0 +1,108 @@ +import { + i18nWrap, + MonkAppStateProvider, + MonkAppStateProviderProps, + useI18nSync, + useLoadingState, +} from '@monkvision/common'; +import { PropsWithChildren, useCallback, useEffect, useState } from 'react'; +import { CaptureAppConfig } from '@monkvision/types'; +import { MonkApi } from '@monkvision/network'; +import { useMonitoring } from '@monkvision/monitoring'; +import { useTranslation } from 'react-i18next'; +import { styles } from './LiveConfigAppProvider.styles'; +import { Spinner } from '../Spinner'; +import { i18nLiveConfigAppProvider } from './i18n'; +import { Button } from '../Button'; + +/** + * Props accepted by the LiveConfigAppProvider component. + */ +export interface LiveConfigAppProviderProps extends Omit { + /** + * The ID of the application's live configuration. + */ + id: string; + /** + * Use this prop to configure a configuration on your local environment. Using this prop will prevent this component + * from fetching a local config from the API. + */ + localConfig?: CaptureAppConfig; + /** + * The language used by this component. + * + * @default en + */ + lang?: string | null; +} + +/** + * This component is used in Monk web applications that support Live Configurations. It acts as both an automatic + * live configuration fetcher and a MonkAppStateProvider. + * + * @see MonkAppStateProvider + */ +export const LiveConfigAppProvider = i18nWrap< + unknown, + PropsWithChildren +>( + ({ + id, + localConfig, + lang, + children, + ...passThroughProps + }: PropsWithChildren) => { + useI18nSync(lang); + const loading = useLoadingState(true); + const [config, setConfig] = useState(null); + const { handleError } = useMonitoring(); + const { t } = useTranslation(); + + const fetchLiveConfig = useCallback(() => { + if (localConfig) { + loading.onSuccess(); + setConfig(localConfig); + return; + } + loading.start(); + setConfig(null); + MonkApi.getLiveConfig(id) + .then((result) => { + loading.onSuccess(); + setConfig(result); + }) + .catch((err) => { + handleError(err); + loading.onError(); + }); + }, [id, localConfig]); + + useEffect(() => { + fetchLiveConfig(); + }, [fetchLiveConfig]); + + if (loading.isLoading || loading.error || !config) { + return ( +
+ {loading.isLoading && } + {!loading.isLoading && ( + <> +
{t('error.message')}
+ + + )} +
+ ); + } + + return ( + + {children} + + ); + }, + i18nLiveConfigAppProvider, +); diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/i18n.ts b/packages/common-ui-web/src/components/LiveConfigAppProvider/i18n.ts new file mode 100644 index 000000000..8a746713c --- /dev/null +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/i18n.ts @@ -0,0 +1,16 @@ +import { i18nCreateSDKInstance } from '@monkvision/common'; +import en from './translations/en.json'; +import fr from './translations/fr.json'; +import de from './translations/de.json'; +import nl from './translations/nl.json'; + +const i18nLiveConfigAppProvider = i18nCreateSDKInstance({ + resources: { + en: { translation: en }, + fr: { translation: fr }, + de: { translation: de }, + nl: { translation: nl }, + }, +}); + +export { i18nLiveConfigAppProvider }; diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/index.ts b/packages/common-ui-web/src/components/LiveConfigAppProvider/index.ts new file mode 100644 index 000000000..d80023bc6 --- /dev/null +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/index.ts @@ -0,0 +1 @@ +export { LiveConfigAppProvider, type LiveConfigAppProviderProps } from './LiveConfigAppProvider'; diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/de.json b/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/de.json new file mode 100644 index 000000000..614c2e8a1 --- /dev/null +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/de.json @@ -0,0 +1,6 @@ +{ + "error": { + "message": "Die Anwendungskonfiguration kann nicht abgerufen werden. Bitte versuchen Sie es in ein paar Minuten erneut.", + "rerty": "Wiederholung" + } +} diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/en.json b/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/en.json new file mode 100644 index 000000000..48355b763 --- /dev/null +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/en.json @@ -0,0 +1,6 @@ +{ + "error": { + "message": "Unable to fetch application configuration. Please try again in a few minutes.", + "retry": "Retry" + } +} diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/fr.json b/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/fr.json new file mode 100644 index 000000000..c05b1286e --- /dev/null +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/fr.json @@ -0,0 +1,6 @@ +{ + "error": { + "message": "Impossible de récupérer la configuration de l'application. Veuillez réessayer dans quelques minutes.", + "retry": "Réessayer" + } +} diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/nl.json b/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/nl.json new file mode 100644 index 000000000..1f0cec02c --- /dev/null +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/translations/nl.json @@ -0,0 +1,6 @@ +{ + "error": { + "message": "Applicatieconfiguratie kan niet worden opgehaald. Probeer het opnieuw over een paar minuten.", + "retry": "Opnieuw proberen" + } +} diff --git a/packages/common-ui-web/src/components/TextField/TextField.styles.ts b/packages/common-ui-web/src/components/TextField/TextField.styles.ts new file mode 100644 index 000000000..dda17dfc5 --- /dev/null +++ b/packages/common-ui-web/src/components/TextField/TextField.styles.ts @@ -0,0 +1,79 @@ +import { Styles } from '@monkvision/types'; + +export const styles: Styles = { + mainContainer: { + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + }, + mainContainerDisabled: { + opacity: 0.37, + cursor: 'default', + }, + componentsContainer: { + display: 'flex', + alignItems: 'center', + paddingTop: 16, + paddingBottom: 4, + borderStyle: 'solid', + minHeight: 53, + boxSizing: 'border-box', + borderRadius: '4px 4px 0 0', + }, + icon: { + marginRight: 16, + transform: 'translateY(-4px)', + }, + inputContainer: { + position: 'relative', + display: 'flex', + flex: 1, + alignSelf: 'stretch', + alignItems: 'center', + }, + label: { + position: 'absolute', + left: 0, + bottom: 12, + fontSize: 16, + fontWeight: 500, + transition: 'all 0.2s ease-out, color 0s', + margin: 0, + padding: 0, + pointerEvents: 'none', + }, + labelFloating: { + fontSize: 12, + fontWeight: 400, + bottom: '85%', + opacity: 0.7, + }, + unit: { + fontSize: 16, + fontWeight: 500, + opacity: 0.5, + }, + input: { + border: 'none', + outline: 'none', + height: 20, + fontSize: 16, + fontWeight: 500, + display: 'flex', + flex: 1, + margin: 0, + padding: 0, + background: 'none', + }, + clearButton: { + padding: 4, + marginLeft: 16, + transform: 'translateY(-4px)', + }, + assistiveText: { + padding: '5px 16px 0 16px', + boxSizing: 'border-box', + maxWidth: '100%', + fontSize: 12, + }, +}; diff --git a/packages/common-ui-web/src/components/TextField/TextField.tsx b/packages/common-ui-web/src/components/TextField/TextField.tsx new file mode 100644 index 000000000..23c0d6b19 --- /dev/null +++ b/packages/common-ui-web/src/components/TextField/TextField.tsx @@ -0,0 +1,96 @@ +import { useRef, useState } from 'react'; +import { TextFieldProps, useTextFieldStyles } from './hooks'; +import { Icon } from '../../icons'; +import { Button } from '../Button'; + +/** + * Custom component implementing a simple one-liner text field. + */ +export function TextField({ + type = 'text', + value, + onChange, + disabled = false, + highlighted = false, + monospace = false, + label, + placeholder, + unit, + unitPosition = 'left', + icon, + showClearButton = true, + assistiveText, + fixedWidth, + focusColor = 'primary-base', + neutralColor = 'text-primary', + backgroundColor = 'background-light', +}: TextFieldProps) { + const inputRef = useRef(null); + const [isFocused, setIsFocused] = useState(false); + const isFilled = !!value && value.length > 0; + const { + mainContainerStyle, + componentsContainerStyle, + leftIcon, + inputContainerStyle, + unitStyle, + labelStyle, + inputStyle, + clearButton, + assistiveTextStyle, + } = useTextFieldStyles({ + disabled, + highlighted, + monospace, + unitPosition, + showClearButton, + focusColor, + neutralColor, + backgroundColor, + fixedWidth, + isFocused, + isFilled, + }); + return ( +
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} +
inputRef.current?.focus()}> + {icon && ( + + )} +
+ + {(isFocused || isFilled) && unit &&
{unit}
} + onChange?.(e.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + data-testid='input' + /> +
+ {showClearButton && ( +
+ {assistiveText &&
{assistiveText}
} +
+ ); +} diff --git a/packages/common-ui-web/src/components/TextField/hooks.ts b/packages/common-ui-web/src/components/TextField/hooks.ts new file mode 100644 index 000000000..fe706ab7f --- /dev/null +++ b/packages/common-ui-web/src/components/TextField/hooks.ts @@ -0,0 +1,187 @@ +import { ColorProp, RequiredProperties } from '@monkvision/types'; +import { changeAlpha, useMonkTheme } from '@monkvision/common'; +import { CSSProperties, useMemo } from 'react'; +import { IconName } from '../../icons'; +import { styles } from './TextField.styles'; + +/** + * Props accepted by the TextField component. + */ +export interface TextFieldProps { + /** + * The type of the underlying HTMLInput element. + * + * @default 'text' + */ + type?: 'email' | 'password' | 'tel' | 'text'; + /** + * The value of the text field. + */ + value?: string; + /** + * Callback called when the value of the text field changes. + */ + onChange?: (newValue: string) => void; + /** + * Boolean indicating if the text field is disabled or not. + * + * @default false + */ + disabled?: boolean; + /** + * Boolean indicating if the input should be highlighted (ex: in case of errors). + * + * @default false + */ + highlighted?: boolean; + /** + * Boolean indicating if the font family of the input should be monospace. + * + * @default false + */ + monospace?: boolean; + /** + * The label of the text field. + */ + label?: string; + /** + * The placeholder of the input. + */ + placeholder?: string; + /** + * The unit symbol of the text field. + */ + unit?: string; + /** + * The position of the unit symbol. + * + * @default 'left' + */ + unitPosition?: 'left' | 'right'; + /** + * The name of the icon on the left of the text field. + */ + icon?: IconName; + /** + * Boolean indicating if the button allowing the user to clear the field should be displayed or not. + * + * @default true + */ + showClearButton?: boolean; + /** + * Assistive text label under the text field. + */ + assistiveText?: string; + /** + * Fixed width for the text field. If not set, the text field expands to the max width of its container. + */ + fixedWidth?: number; + /** + * The accent color of the text field when focused. + * + * @default 'primary-base' + */ + focusColor?: ColorProp; + /** + * The accent color of the text field when not focused. + * + * @default 'text-primary' + */ + neutralColor?: ColorProp; + /** + * The background color of the text field. + * + * @default 'background-light' + */ + backgroundColor?: ColorProp; +} + +export type TextFieldStylesParams = RequiredProperties< + TextFieldProps, + | 'disabled' + | 'highlighted' + | 'monospace' + | 'unitPosition' + | 'showClearButton' + | 'focusColor' + | 'neutralColor' + | 'backgroundColor' +> & { isFocused: boolean; isFilled: boolean }; + +export function useTextFieldStyles(props: TextFieldStylesParams) { + const { utils } = useMonkTheme(); + + const colors = useMemo(() => { + const focus = utils.getColor(props.focusColor); + const neutral = utils.getColor(props.neutralColor); + const backgroundColor = utils.getColor(props.backgroundColor); + + return { + focus, + neutral, + neutral30: changeAlpha(neutral, 0.3), + backgroundColor, + }; + }, [props.focusColor, props.neutralColor, props.backgroundColor]); + + let borderColor = colors.neutral30; + if (props.isFilled) { + borderColor = colors.neutral; + } + if (props.isFocused || props.highlighted) { + borderColor = colors.focus; + } + + return { + mainContainerStyle: { + ...styles['mainContainer'], + ...(props.disabled ? styles['mainContainerDisabled'] : {}), + width: props.fixedWidth ?? '100%', + }, + componentsContainerStyle: { + ...styles['componentsContainer'], + paddingLeft: props.icon ? 12 : 16, + paddingRight: props.showClearButton ? 12 : 16, + borderWidth: props.highlighted ? 1 : 0, + borderBottomWidth: props.isFocused ? 2 : 1, + borderColor, + marginBottom: props.isFocused ? 0 : 1, + backgroundColor: colors.backgroundColor, + cursor: props.disabled ? 'default' : 'text', + }, + leftIcon: { + size: 24, + primaryColor: colors.neutral, + style: styles['icon'], + }, + inputContainerStyle: { + ...styles['inputContainer'], + flexDirection: props.unitPosition === 'left' ? 'row' : 'row-reverse', + } as CSSProperties, + unitStyle: { + ...styles['unit'], + color: colors.neutral, + paddingLeft: props.unitPosition === 'left' ? 0 : 6, + paddingRight: props.unitPosition === 'left' ? 6 : 0, + }, + labelStyle: { + ...styles['label'], + ...(props.isFilled || props.isFocused ? styles['labelFloating'] : {}), + color: props.isFocused || props.highlighted ? colors.focus : colors.neutral, + }, + inputStyle: { + ...styles['input'], + color: colors.neutral, + fontFamily: props.monospace ? 'monospace' : 'sans-serif', + }, + clearButton: { + primaryColor: colors.neutral, + style: styles['clearButton'], + visibility: props.isFilled ? 'visible' : 'hidden', + }, + assistiveTextStyle: { + ...styles['assistiveText'], + color: props.isFocused || props.highlighted ? colors.focus : colors.neutral, + }, + }; +} diff --git a/packages/common-ui-web/src/components/TextField/index.ts b/packages/common-ui-web/src/components/TextField/index.ts new file mode 100644 index 000000000..bb509623e --- /dev/null +++ b/packages/common-ui-web/src/components/TextField/index.ts @@ -0,0 +1,2 @@ +export { TextField } from './TextField'; +export { type TextFieldProps } from './hooks'; diff --git a/packages/common-ui-web/src/components/index.ts b/packages/common-ui-web/src/components/index.ts index 45f0095fd..c3b6a9e10 100644 --- a/packages/common-ui-web/src/components/index.ts +++ b/packages/common-ui-web/src/components/index.ts @@ -1,15 +1,18 @@ export * from './AuthGuard'; export * from './BackdropDialog'; export * from './Button'; +export * from './Checkbox'; export * from './CreateInspection'; export * from './DynamicSVG'; export * from './ImageDetailedView'; export * from './InspectionGallery'; +export * from './LiveConfigAppProvider'; export * from './Login'; export * from './SightOverlay'; export * from './Slider'; export * from './Spinner'; export * from './SwitchButton'; export * from './TakePictureButton'; +export * from './TextField'; export * from './VehicleTypeAsset'; export * from './VehicleTypeSelection'; diff --git a/packages/common-ui-web/test/components/Checkbox.test.tsx b/packages/common-ui-web/test/components/Checkbox.test.tsx new file mode 100644 index 000000000..ad2f5dcc8 --- /dev/null +++ b/packages/common-ui-web/test/components/Checkbox.test.tsx @@ -0,0 +1,103 @@ +import { changeAlpha } from '@monkvision/common'; + +jest.mock('../../src/icons', () => ({ + Icon: jest.fn(() => <>), +})); + +import '@testing-library/jest-dom'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { Checkbox, Icon } from '../../src'; +import { expectPropsOnChildMock } from '@monkvision/test-utils'; + +const CHECKBOX_TEST_ID = 'checkbox-btn'; + +describe('Checkbox component', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should display managed checkbox', () => { + const checked = true; + const onChange = jest.fn(); + const { unmount, rerender } = render(); + + let checkbox = screen.getByTestId(CHECKBOX_TEST_ID); + expect(onChange).not.toHaveBeenCalled(); + fireEvent.click(checkbox); + expect(onChange).toHaveBeenCalledWith(!checked); + + rerender(); + checkbox = screen.getByTestId(CHECKBOX_TEST_ID); + onChange.mockClear(); + expect(onChange).not.toHaveBeenCalled(); + fireEvent.click(checkbox); + expect(onChange).toHaveBeenCalledWith(checked); + + unmount(); + }); + + it('should be unchecked by default', () => { + const onChange = jest.fn(); + const { unmount } = render(); + + const checkbox = screen.getByTestId(CHECKBOX_TEST_ID); + fireEvent.click(checkbox); + expect(onChange).toHaveBeenCalledWith(true); + + unmount(); + }); + + it('should disable the checkbox when disabled is set to true', () => { + const onChange = jest.fn(); + const { unmount } = render(); + + const checkbox = screen.getByTestId(CHECKBOX_TEST_ID); + fireEvent.click(checkbox); + expect(onChange).not.toHaveBeenCalled(); + + unmount(); + }); + + it('should display the labelon the screen', () => { + const label = 'test-label'; + const { unmount } = render(); + + expect(screen.queryByText(label)).not.toBeNull(); + + unmount(); + }); + + it('should have the proper style when unchecked', () => { + const tertiaryColor = '#012345'; + const alphaChanged = '#123456'; + (changeAlpha as jest.Mock).mockImplementation(() => alphaChanged); + const { unmount } = render(); + + expect(changeAlpha).toHaveBeenCalledWith(tertiaryColor, 0.5); + const checkbox = screen.getByTestId(CHECKBOX_TEST_ID); + expect(checkbox).toHaveStyle({ + backgroundColor: alphaChanged, + borderColor: tertiaryColor, + }); + expect(Icon).not.toHaveBeenCalled(); + + unmount(); + }); + + it('should have the proper colors when checked', () => { + const primaryColor = '#654321'; + const secondaryColor = '#009988'; + const { unmount } = render( + , + ); + + const checkbox = screen.getByTestId(CHECKBOX_TEST_ID); + expect(checkbox).toHaveStyle({ + backgroundColor: primaryColor, + borderColor: primaryColor, + }); + expectPropsOnChildMock(Icon, { icon: 'check', primaryColor: secondaryColor }); + + unmount(); + }); +}); diff --git a/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx b/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx new file mode 100644 index 000000000..bb0f62711 --- /dev/null +++ b/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx @@ -0,0 +1,116 @@ +import { CaptureAppConfig } from '@monkvision/types'; + +jest.mock('../../src/components/Button', () => ({ + Button: jest.fn(() => <>), +})); +jest.mock('../../src/components/Spinner', () => ({ + Spinner: jest.fn(() => <>), +})); + +import { act, render, screen, waitFor } from '@testing-library/react'; +import { createFakePromise, expectPropsOnChildMock } from '@monkvision/test-utils'; +import { MonkAppStateProvider, useLoadingState } from '@monkvision/common'; +import { MonkApi } from '@monkvision/network'; +import { Button, LiveConfigAppProvider, Spinner } from '../../src'; + +describe('LiveConfigAppProvider component', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch the live config and pass it to the MonkAppStateProvider component', async () => { + const config = { hello: 'world' }; + (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve(config)); + const id = 'test-id-test'; + const { unmount } = render(); + + await waitFor(() => { + expectPropsOnChildMock(MonkAppStateProvider, { config }); + }); + + unmount(); + }); + + it('should pass down the props and children to the MonkAppStateProvider component', async () => { + const onFetchAuthToken = jest.fn(); + const onFetchLanguage = jest.fn(); + const children = 'test-children'; + const { unmount } = render( + + {children} + , + ); + + await waitFor(() => { + expectPropsOnChildMock(MonkAppStateProvider, { onFetchAuthToken, onFetchLanguage, children }); + }); + + unmount(); + }); + + it('should display a spinner while waiting for the live config to be fetched', async () => { + (useLoadingState as jest.Mock).mockImplementation( + jest.requireActual('@monkvision/common').useLoadingState, + ); + const spinnerTestId = 'spinner-test'; + (Spinner as unknown as jest.Mock).mockImplementation(() => ( +
+ )); + const promise = createFakePromise(); + (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => promise); + const id = 'test-id-test'; + const { unmount } = render(); + + expect(screen.queryByTestId(spinnerTestId)).not.toBeNull(); + expect(MonkAppStateProvider).not.toHaveBeenCalled(); + await act(async () => { + promise.resolve({}); + await promise; + }); + expect(screen.queryByTestId(spinnerTestId)).toBeNull(); + expect(MonkAppStateProvider).toHaveBeenCalled(); + + unmount(); + }); + + it('should display an error message with a retry button in case of error', async () => { + (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error())); + const id = 'test-id-test'; + const { unmount } = render(); + + expect(MonkAppStateProvider).not.toHaveBeenCalled(); + await waitFor(() => { + expect(screen.getByText('error.message')).not.toBeNull(); + expectPropsOnChildMock(Button, { children: 'error.retry', onClick: expect.any(Function) }); + }); + const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; + (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve({})); + act(() => { + onClick(); + }); + expect(MonkAppStateProvider).not.toHaveBeenCalled(); + await waitFor(() => { + expect(screen.queryByText('error.message')).toBeNull(); + expect(MonkAppStateProvider).toHaveBeenCalled(); + }); + + unmount(); + }); + + it('should not fetch the live config and return the local config if it is used', async () => { + const localConfig = { hello: 'world' } as unknown as CaptureAppConfig; + const id = 'test-id-test'; + const { unmount } = render(); + + await waitFor(() => { + expectPropsOnChildMock(MonkAppStateProvider, { config: localConfig }); + expect(MonkApi.getLiveConfig).not.toHaveBeenCalled(); + }); + + unmount(); + }); +}); diff --git a/packages/common-ui-web/test/components/TextField.test.tsx b/packages/common-ui-web/test/components/TextField.test.tsx new file mode 100644 index 000000000..340aabf6b --- /dev/null +++ b/packages/common-ui-web/test/components/TextField.test.tsx @@ -0,0 +1,149 @@ +jest.mock('../../src/icons', () => ({ + Icon: jest.fn(() => <>), +})); +jest.mock('../../src/components/Button', () => ({ + Button: jest.fn(() => <>), +})); + +import { expectPropsOnChildMock } from '@monkvision/test-utils'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { Button, Icon, TextField } from '../../src'; + +const INPUT_TEST_ID = 'input'; + +describe('TextField component', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should display a managed input on the screen', () => { + const value = 'test-value'; + const onChange = jest.fn(); + const { unmount } = render(); + + const input = screen.getByTestId(INPUT_TEST_ID); + expect(input.value).toEqual(value); + const newValue = 'new-value-test'; + expect(onChange).not.toHaveBeenCalled(); + fireEvent.change(input, { target: { value: newValue } }); + expect(onChange).toHaveBeenCalledWith(newValue); + + unmount(); + }); + + it('should display the label of the button', () => { + const label = 'test-label'; + const { unmount } = render(); + + expect(screen.queryByText(label)).not.toBeNull(); + + unmount(); + }); + + it('should pass the proper placeholder to the input when focused', () => { + const placeholder = 'test-placeholder'; + const { unmount } = render(); + + const input = screen.getByTestId(INPUT_TEST_ID); + fireEvent.focus(input); + expect(input.placeholder).toEqual(placeholder); + + unmount(); + }); + + it('should not have any placeholder if the input is not focused', () => { + const { unmount } = render(); + + const input = screen.getByTestId(INPUT_TEST_ID); + expect(input.placeholder).toEqual(''); + + unmount(); + }); + + it('should display the unit symbol when the input is focused', () => { + const unit = '$'; + const { unmount } = render(); + + const input = screen.getByTestId(INPUT_TEST_ID); + fireEvent.focus(input); + expect(screen.queryByText(unit)).not.toBeNull(); + + unmount(); + }); + + it('should not display the unit symbol if the input is not focused', () => { + const unit = '$'; + const { unmount } = render(); + + expect(screen.queryByText(unit)).toBeNull(); + + unmount(); + }); + + it('should display the asked Icon', () => { + const icon = 'image'; + const { unmount } = render(); + + expectPropsOnChildMock(Icon, { icon }); + + unmount(); + }); + + it('should not display any Icon if the icon prop is not provided', () => { + const { unmount } = render(); + + expect(Icon).not.toHaveBeenCalled(); + + unmount(); + }); + + it('should display the assistive text', () => { + const assistiveText = 'Hello World!!!'; + const { unmount } = render(); + + expect(screen.queryByText(assistiveText)).not.toBeNull(); + + unmount(); + }); + + it('should show a clear input button', () => { + const onChange = jest.fn(); + const { unmount } = render(); + + expectPropsOnChildMock(Button, { icon: 'close', onClick: expect.any(Function) }); + const { onClick } = (Button as unknown as jest.Mock).mock.calls.find( + (args) => args[0].icon === 'close', + )[0]; + expect(onChange).not.toHaveBeenCalled(); + onClick(); + expect(onChange).toHaveBeenCalledWith(''); + + unmount(); + }); + + it('should not show a clear input button if showClearButton is false', () => { + const { unmount } = render(); + + expect(Button).not.toHaveBeenCalled(); + + unmount(); + }); + + it('should show a clear input button by default', () => { + const { unmount } = render(); + + expectPropsOnChildMock(Button, { icon: 'close' }); + + unmount(); + }); + + it('should properly disable the input', () => { + const { unmount } = render(); + + const input = screen.getByTestId(INPUT_TEST_ID); + expect(input.disabled).toBe(true); + expectPropsOnChildMock(Button, { icon: 'close', disabled: true }); + + unmount(); + }); +}); diff --git a/packages/inspection-capture-web/README.md b/packages/inspection-capture-web/README.md index 6e0f169c7..c66b357e0 100644 --- a/packages/inspection-capture-web/README.md +++ b/packages/inspection-capture-web/README.md @@ -73,6 +73,7 @@ export function MonkPhotoCapturePage({ authToken }) { | useAdaptiveImageQuality | `boolean` | Boolean indicating if the image quality should be downgraded automatically in case of low connection. | | `true` | | showCloseButton | `boolean` | Indicates if the close button should be displayed in the HUD on top of the Camera preview. | | `false` | | startTasksOnComplete | `boolean | TaskName[]` | Value indicating if tasks should be started at the end of the inspection. See the `inspection-capture-web` package doc for more info. | | `true` | +| additionalTasks | `TaskName[]` | An optional list of additional tasks to run on every Sight of the inspection. | | | | tasksBySight | `Record` | Record associating each sight with a list of tasks to execute for it. If not provided, the default tasks of the sight will be used. | | | | format | `CompressionFormat` | The output format of the compression. | | `CompressionFormat.JPEG` | | quality | `number` | Value indicating image quality for the compression output. | | `0.6` | diff --git a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx index b52f3756d..f220806a2 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx +++ b/packages/inspection-capture-web/src/PhotoCapture/PhotoCapture.tsx @@ -49,6 +49,7 @@ export interface PhotoCaptureProps | keyof CameraConfig | 'maxUploadDurationWarning' | 'useAdaptiveImageQuality' + | 'additionalTasks' | 'tasksBySight' | 'startTasksOnComplete' | 'showCloseButton' @@ -100,6 +101,7 @@ export function PhotoCapture({ sights, inspectionId, apiConfig, + additionalTasks, tasksBySight, startTasksOnComplete = true, onClose, @@ -147,6 +149,7 @@ export function PhotoCapture({ inspectionId, apiConfig, sights, + additionalTasks, tasksBySight, startTasksOnComplete, loading, @@ -171,6 +174,7 @@ export function PhotoCapture({ const uploadQueue = useUploadQueue({ inspectionId, apiConfig, + additionalTasks, complianceOptions, eventHandlers: [adaptiveUploadEventHandlers, badConnectionWarningUploadEventHandlers], }); diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/useStartTasksOnComplete.ts b/packages/inspection-capture-web/src/PhotoCapture/hooks/useStartTasksOnComplete.ts index 4de214829..fe1a2039c 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/hooks/useStartTasksOnComplete.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/hooks/useStartTasksOnComplete.ts @@ -1,4 +1,4 @@ -import { Sight, TaskName } from '@monkvision/types'; +import { CaptureAppConfig, Sight, TaskName } from '@monkvision/types'; import { flatMap, LoadingState, uniq } from '@monkvision/common'; import { MonkApiConfig, useMonkApi } from '@monkvision/network'; import { useMonitoring } from '@monkvision/monitoring'; @@ -7,7 +7,8 @@ import { useCallback } from 'react'; /** * Parameters of the useStartTasksOnComplete hook. */ -export interface UseStartTasksOnCompleteParams { +export interface UseStartTasksOnCompleteParams + extends Pick { /** * The inspection ID. */ @@ -24,19 +25,6 @@ export interface UseStartTasksOnCompleteParams { * Global loading state of the PhotoCapture component. */ loading: LoadingState; - /** - * Record associating each sight with a list of tasks to execute for it. If not provided, the default tasks of the - * sight will be used. - */ - tasksBySight?: Record; - /** - * Value indicating if tasks should be started at the end of the inspection : - * - If not provided or if value is set to `false`, no tasks will be started. - * - If set to `true`, the tasks described by the `tasksBySight` param (or, if not provided, the default tasks of each - * sight) will be started. - * - If an array of tasks is provided, the tasks started will be the ones contained in the array. - */ - startTasksOnComplete?: boolean | TaskName[]; } /** @@ -48,17 +36,23 @@ const TASKS_NOT_TO_START = [TaskName.HUMAN_IN_THE_LOOP]; function getTasksToStart({ sights, + additionalTasks, tasksBySight, startTasksOnComplete, }: Pick< UseStartTasksOnCompleteParams, - 'sights' | 'tasksBySight' | 'startTasksOnComplete' + 'sights' | 'additionalTasks' | 'tasksBySight' | 'startTasksOnComplete' >): TaskName[] { let tasks: TaskName[]; if (Array.isArray(startTasksOnComplete)) { tasks = startTasksOnComplete; } else { tasks = uniq(flatMap(sights, (sight) => tasksBySight?.[sight.id] ?? sight.tasks)); + additionalTasks?.forEach((additionalTask) => { + if (!tasks.includes(additionalTask)) { + tasks.push(additionalTask); + } + }); } return tasks.filter((task) => !TASKS_NOT_TO_START.includes(task)); } @@ -71,6 +65,7 @@ export function useStartTasksOnComplete({ inspectionId, apiConfig, sights, + additionalTasks, tasksBySight, startTasksOnComplete, loading, @@ -82,7 +77,7 @@ export function useStartTasksOnComplete({ if (!startTasksOnComplete) { return; } - const names = getTasksToStart({ sights, tasksBySight, startTasksOnComplete }); + const names = getTasksToStart({ sights, additionalTasks, tasksBySight, startTasksOnComplete }); loading.start(); try { diff --git a/packages/inspection-capture-web/src/PhotoCapture/hooks/useUploadQueue.ts b/packages/inspection-capture-web/src/PhotoCapture/hooks/useUploadQueue.ts index e6afa3bf0..35373e656 100644 --- a/packages/inspection-capture-web/src/PhotoCapture/hooks/useUploadQueue.ts +++ b/packages/inspection-capture-web/src/PhotoCapture/hooks/useUploadQueue.ts @@ -1,6 +1,12 @@ -import { Queue, useQueue } from '@monkvision/common'; +import { Queue, uniq, useQueue } from '@monkvision/common'; import { AddImageOptions, MonkApiConfig, useMonkApi } from '@monkvision/network'; -import { ComplianceOptions, ImageType, MonkPicture, TaskName } from '@monkvision/types'; +import { + CaptureAppConfig, + ComplianceOptions, + ImageType, + MonkPicture, + TaskName, +} from '@monkvision/types'; import { useRef } from 'react'; import { useMonitoring } from '@monkvision/monitoring'; import { PhotoCaptureMode } from './useAddDamageMode'; @@ -24,7 +30,7 @@ export interface UploadEventHandlers { /** * Parameters of the useUploadQueue hook. */ -export interface UploadQueueParams { +export interface UploadQueueParams extends Pick { /** * The inspection ID. */ @@ -105,6 +111,7 @@ function createAddImageOptions( upload: PictureUpload, inspectionId: string, siblingId: number, + additionalTasks?: CaptureAppConfig['additionalTasks'], compliance?: ComplianceOptions, ): AddImageOptions { if (upload.mode === PhotoCaptureMode.SIGHT) { @@ -112,7 +119,7 @@ function createAddImageOptions( type: ImageType.BEAUTY_SHOT, picture: upload.picture, sightId: upload.sightId, - tasks: upload.tasks, + tasks: additionalTasks ? uniq([...upload.tasks, ...additionalTasks]) : upload.tasks, inspectionId, compliance, }; @@ -133,6 +140,7 @@ function createAddImageOptions( export function useUploadQueue({ inspectionId, apiConfig, + additionalTasks, complianceOptions, eventHandlers, }: UploadQueueParams): Queue { @@ -147,7 +155,13 @@ export function useUploadQueue({ try { const startTs = Date.now(); await addImage( - createAddImageOptions(upload, inspectionId, siblingIdRef.current, complianceOptions), + createAddImageOptions( + upload, + inspectionId, + siblingIdRef.current, + additionalTasks, + complianceOptions, + ), ); const uploadDurationMs = Date.now() - startTs; eventHandlers?.forEach((handlers) => handlers.onUploadSuccess?.(uploadDurationMs)); diff --git a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx index f46d8383d..de55f6a37 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx +++ b/packages/inspection-capture-web/test/PhotoCapture/PhotoCapture.test.tsx @@ -70,6 +70,9 @@ function createProps(): PhotoCaptureProps { sights: [sights['test-sight-1'], sights['test-sight-2'], sights['test-sight-3']], inspectionId: 'test-inspection-test', apiConfig: { apiDomain: 'test-api-domain-test', authToken: 'test-auth-token-test' }, + additionalTasks: [TaskName.DASHBOARD_OCR], + tasksBySight: { 'test-sight-1': [TaskName.IMAGE_EDITING] }, + startTasksOnComplete: [TaskName.COMPLIANCES], enableCompliance: true, enableCompliancePerSight: ['test-sight-id'], useLiveCompliance: true, @@ -129,11 +132,7 @@ describe('PhotoCapture component', () => { }); it('should pass the proper params to the useStartTasksOnComplete hook', () => { - const props = { - ...createProps(), - startTasksOnComplete: true, - tasksBySight: { test: [TaskName.DAMAGE_DETECTION] }, - }; + const props = createProps(); const { unmount } = render(); expect(useLoadingState).toHaveBeenCalled(); @@ -142,6 +141,7 @@ describe('PhotoCapture component', () => { inspectionId: props.inspectionId, apiConfig: props.apiConfig, sights: props.sights, + additionalTasks: props.additionalTasks, tasksBySight: props.tasksBySight, startTasksOnComplete: props.startTasksOnComplete, loading, @@ -201,6 +201,7 @@ describe('PhotoCapture component', () => { expect(useUploadQueue).toHaveBeenCalledWith({ inspectionId: props.inspectionId, apiConfig: props.apiConfig, + additionalTasks: props.additionalTasks, complianceOptions: { enableCompliance: props.enableCompliance, enableCompliancePerSight: props.enableCompliancePerSight, diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/useStartTasksOnComplete.test.ts b/packages/inspection-capture-web/test/PhotoCapture/hooks/useStartTasksOnComplete.test.ts index cfb8c791e..33cc11ed1 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/useStartTasksOnComplete.test.ts +++ b/packages/inspection-capture-web/test/PhotoCapture/hooks/useStartTasksOnComplete.test.ts @@ -58,6 +58,13 @@ describe('useStartTasksOnComplete hook', () => { it('should start the tasks given in the startTasksOnComplete param', () => { const initialProps = { ...createParams(), + tasksBySight: { + 'test-sight-1': [TaskName.DAMAGE_DETECTION], + 'test-sight-2': [TaskName.DAMAGE_DETECTION], + 'test-sight-3': [TaskName.DAMAGE_DETECTION], + 'test-sight-4': [TaskName.DAMAGE_DETECTION], + }, + additionalTasks: [TaskName.IMAGE_EDITING], startTasksOnComplete: [TaskName.DASHBOARD_OCR, TaskName.WHEEL_ANALYSIS], }; const { result, unmount } = renderHook(useStartTasksOnComplete, { initialProps }); @@ -106,7 +113,7 @@ describe('useStartTasksOnComplete hook', () => { unmount(); }); - it('should start the sight tasks of tasksBySight in priority and fill with the default tasks', () => { + it('should start the sight tasks of tasksBySight in priority and fill with the default and additional tasks', () => { const defaultProps = createParams(); const initialProps = { ...defaultProps, @@ -116,7 +123,7 @@ describe('useStartTasksOnComplete hook', () => { { id: 'test-sight-2', tasks: [TaskName.WHEEL_ANALYSIS, TaskName.PRICING] }, { id: 'test-sight-3', tasks: [TaskName.IMAGES_OCR, TaskName.PRICING] }, { - id: 'test-sight-3', + id: 'test-sight-4', tasks: [TaskName.DAMAGE_DETECTION, TaskName.PRICING, TaskName.DASHBOARD_OCR], }, ] as Sight[], @@ -124,6 +131,7 @@ describe('useStartTasksOnComplete hook', () => { 'test-sight-1': [TaskName.DAMAGE_DETECTION, TaskName.PRICING, TaskName.IMAGE_EDITING], 'test-sight-2': [TaskName.REPAIR_ESTIMATE], }, + additionalTasks: [TaskName.INSPECTION_PDF, TaskName.DASHBOARD_OCR], }; const { result, unmount } = renderHook(useStartTasksOnComplete, { initialProps }); @@ -140,14 +148,13 @@ describe('useStartTasksOnComplete hook', () => { TaskName.REPAIR_ESTIMATE, TaskName.IMAGES_OCR, TaskName.DASHBOARD_OCR, + TaskName.INSPECTION_PDF, ], }); unmount(); }); - it('should start both the tasks from the tasksBySight and sights tasks lists when both are defined', () => {}); - it('should properly handle loading and error in case of success', async () => { const promise = createFakePromise(); const startInspectionTasksMock = jest.fn(() => promise); diff --git a/packages/inspection-capture-web/test/PhotoCapture/hooks/useUploadQueue.test.ts b/packages/inspection-capture-web/test/PhotoCapture/hooks/useUploadQueue.test.ts index 2be14a4e1..6b015f74a 100644 --- a/packages/inspection-capture-web/test/PhotoCapture/hooks/useUploadQueue.test.ts +++ b/packages/inspection-capture-web/test/PhotoCapture/hooks/useUploadQueue.test.ts @@ -89,6 +89,34 @@ describe('useUploadQueue hook', () => { unmount(); }); + it('should properly add a sight image with additional tasks', async () => { + const initialProps = createParams(); + const tasks = [TaskName.DAMAGE_DETECTION, TaskName.IMAGE_EDITING]; + initialProps.additionalTasks = [TaskName.INSPECTION_PDF, TaskName.IMAGE_EDITING]; + const { unmount } = renderHook(useUploadQueue, { initialProps }); + expect(useMonkApi).toHaveBeenCalled(); + const addImageMock = (useMonkApi as jest.Mock).mock.results[0].value.addImage; + expect(useQueue).toHaveBeenCalled(); + const process = (useQueue as jest.Mock).mock.calls[0][0]; + + await process({ ...defaultUploadOptions, tasks }); + + expect(addImageMock).toHaveBeenCalledWith({ + type: ImageType.BEAUTY_SHOT, + picture: defaultUploadOptions.picture, + sightId: defaultUploadOptions.sightId, + tasks: expect.arrayContaining([ + TaskName.DAMAGE_DETECTION, + TaskName.IMAGE_EDITING, + TaskName.INSPECTION_PDF, + ]), + compliance: initialProps.complianceOptions, + inspectionId: initialProps.inspectionId, + }); + + unmount(); + }); + it('should properly add an add damage images', async () => { const initialProps = createParams(); const { unmount } = renderHook(useUploadQueue, { initialProps }); diff --git a/packages/network/README.md b/packages/network/README.md index fc0b9f97c..91b30bf60 100644 --- a/packages/network/README.md +++ b/packages/network/README.md @@ -91,6 +91,18 @@ this documentation for the `updateTaskStatus` function.** |-----------|-----------------------------|----------------------------------------------------------|----------| | options | StartInspectionTasksOptions | The options of the request. | ✔️ | +### getLiveConfig +```typescript +import { MonkApi } from '@monkvision/network';https://acvauctions.fls.jetbrains.com + +MonkApi.getLiveConfig(id); +``` + +Fetch a webapp live configuration from the API. + +| Parameter | Type | Description | Required | +|-----------|--------|-----------------------------------|----------| +| id | string | The ID of the live config to get. | ✔️ | # React Tools In order to simply integrate the Monk Api requests into your React app, you can make use of the `useMonkApi` hook. This diff --git a/packages/network/src/api/api.ts b/packages/network/src/api/api.ts index 73d3590a3..54ea3a42d 100644 --- a/packages/network/src/api/api.ts +++ b/packages/network/src/api/api.ts @@ -1,6 +1,7 @@ import { getInspection, createInspection } from './inspection'; import { addImage } from './image'; import { startInspectionTasks, updateTaskStatus } from './task'; +import { getLiveConfig } from './liveConfigs'; /** * Object regrouping the different API requests available to communicate with the API using the `@monkvision/network` @@ -12,4 +13,5 @@ export const MonkApi = { addImage, updateTaskStatus, startInspectionTasks, + getLiveConfig, }; diff --git a/packages/network/src/api/config.ts b/packages/network/src/api/config.ts index 935d4eeaf..94b6cba29 100644 --- a/packages/network/src/api/config.ts +++ b/packages/network/src/api/config.ts @@ -18,19 +18,30 @@ export interface MonkApiConfig { authToken: string; } -export function getDefaultOptions(config: MonkApiConfig): Options { +function getPrefixUrl(config?: MonkApiConfig): string | undefined { + if (!config) { + return undefined; + } const apiDomain = config.apiDomain.endsWith('/') ? config.apiDomain.substring(0, config.apiDomain.length - 1) : config.apiDomain; - const authorizationHeader = config.authToken.startsWith('Bearer ') - ? config.authToken - : `Bearer ${config.authToken}`; + return `https://${apiDomain}`; +} + +function getAuthorizationHeader(config?: MonkApiConfig): string | undefined { + if (!config) { + return undefined; + } + return config.authToken.startsWith('Bearer ') ? config.authToken : `Bearer ${config.authToken}`; +} + +export function getDefaultOptions(config?: MonkApiConfig): Options { return { - prefixUrl: `https://${apiDomain}`, + prefixUrl: getPrefixUrl(config), headers: { 'Accept': 'application/json, text/plain, */*', 'Access-Control-Allow-Origin': '*', - 'Authorization': authorizationHeader, + 'Authorization': getAuthorizationHeader(config), 'X-Monk-SDK-Version': sdkVersion, }, hooks: { diff --git a/packages/network/src/api/liveConfigs/index.ts b/packages/network/src/api/liveConfigs/index.ts new file mode 100644 index 000000000..c3dff2f3f --- /dev/null +++ b/packages/network/src/api/liveConfigs/index.ts @@ -0,0 +1 @@ +export * from './requests'; diff --git a/packages/network/src/api/liveConfigs/requests.ts b/packages/network/src/api/liveConfigs/requests.ts new file mode 100644 index 000000000..9da5a578e --- /dev/null +++ b/packages/network/src/api/liveConfigs/requests.ts @@ -0,0 +1,17 @@ +import { LiveConfig } from '@monkvision/types'; +import ky from 'ky'; +import { getDefaultOptions } from '../config'; + +/** + * Fetch a webapp live configuration from the API. + * + * @param id The ID of the live config to get. + */ +export async function getLiveConfig(id: string): Promise { + const kyOptions = getDefaultOptions(); + const response = await ky.get( + `https://storage.googleapis.com/monk-front-public/live-configurations/${id}.json?nocache=${Date.now()}`, + kyOptions, + ); + return response.json(); +} diff --git a/packages/network/src/api/react.ts b/packages/network/src/api/react.ts index a800f3738..40a139c18 100644 --- a/packages/network/src/api/react.ts +++ b/packages/network/src/api/react.ts @@ -9,6 +9,20 @@ type MonkApiRequest

, A extends MonkAction, R extends Pr ...params: [...P, MonkApiConfig, Dispatch?] ) => R; +function handleAPIError( + err: unknown, + handleError: (err: unknown, context?: Omit) => void, +): void { + const { body } = err as MonkHTTPError; + handleError(err, { + extras: { + body, + completeResponse: JSON.stringify(body), + }, + }); + throw err; +} + function reactify

, A extends MonkAction, R extends Promise>( request: MonkApiRequest, config: MonkApiConfig, @@ -17,16 +31,7 @@ function reactify

, A extends MonkAction, R extends Prom ): (...params: P) => R { return useCallback( (...params: P) => - request(...params, config, dispatch).catch((err) => { - const { body } = err as MonkHTTPError; - handleError(err, { - extras: { - body, - completeResponse: JSON.stringify(body), - }, - }); - throw err; - }) as R, + request(...params, config, dispatch).catch((err) => handleAPIError(err, handleError)) as R, [], ); } @@ -86,5 +91,14 @@ export function useMonkApi(config: MonkApiConfig) { * @see updateTaskStatus */ startInspectionTasks: reactify(MonkApi.startInspectionTasks, config, dispatch, handleError), + /** + * Fetch a webapp live configuration from the API. + * + * @param id The ID of the live config to get. + */ + getLiveConfig: useCallback( + (id: string) => MonkApi.getLiveConfig(id).catch((err) => handleAPIError(err, handleError)), + [handleError], + ), }; } diff --git a/packages/network/test/api/config.test.ts b/packages/network/test/api/config.test.ts index bd4a0159f..749b0074f 100644 --- a/packages/network/test/api/config.test.ts +++ b/packages/network/test/api/config.test.ts @@ -15,6 +15,12 @@ describe('Network package API global config utils', () => { authToken: 'Bearer testtoken', }; + it('should return no prefixUrl and Authorization header if no API config is provided', () => { + const options = getDefaultOptions(); + expect(options.prefixUrl).toBeUndefined(); + expect(options.headers).toEqual(expect.objectContaining({ Authorization: undefined })); + }); + it('should return the proper prefixUrl', () => { expect(getDefaultOptions(baseConfig).prefixUrl).toEqual(`https://${baseConfig.apiDomain}`); }); diff --git a/packages/network/test/api/liveConfigs/requests.test.ts b/packages/network/test/api/liveConfigs/requests.test.ts new file mode 100644 index 000000000..b9bad4a75 --- /dev/null +++ b/packages/network/test/api/liveConfigs/requests.test.ts @@ -0,0 +1,28 @@ +jest.mock('../../../src/api/config', () => ({ + getDefaultOptions: jest.fn(() => ({ prefixUrl: 'getDefaultOptionsTest' })), +})); + +import ky from 'ky'; +import { getDefaultOptions } from '../../../src/api/config'; +import { getLiveConfig } from '../../../src/api/liveConfigs'; + +describe('Live Configs API requests', () => { + describe('getLiveConfig request', () => { + it('should fetch the live config at the proper URL and prevent caching from the browser', async () => { + const id = 'test-live-config-id-test'; + const result = await getLiveConfig(id); + + expect(getDefaultOptions).toHaveBeenCalledWith(); + expect(ky.get).toHaveBeenCalledWith(expect.any(String), getDefaultOptions()); + const url = (ky.get as jest.Mock).mock.calls[0][0]; + expect( + url.startsWith( + `https://storage.googleapis.com/monk-front-public/live-configurations/${id}.json?nocache=`, + ), + ).toBe(true); + const response = await (ky.get as jest.Mock).mock.results[0].value; + const body = await response.json(); + expect(result).toEqual(body); + }); + }); +}); diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 0b6b6dca5..4b5c533a6 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -38,6 +38,10 @@ export type CameraConfig = Partial & { */ export type CaptureAppConfig = CameraConfig & Partial & { + /** + * An optional list of additional tasks to run on every Sight of the inspection. + */ + additionalTasks?: TaskName[]; /** * Record associating each sight with a list of tasks to execute for it. If not provided, the default tasks of the * sight will be used. @@ -172,7 +176,7 @@ export type LiveConfig = CaptureAppConfig & { */ id: string; /** - * The name of the live config. + * The description of the configuration. */ - name?: string; + description: string; };