diff --git a/.github/workflows/build-preview-environment.yml b/.github/workflows/build-preview-environment.yml new file mode 100644 index 0000000000..b419b3d335 --- /dev/null +++ b/.github/workflows/build-preview-environment.yml @@ -0,0 +1,121 @@ +# Deploys a temporary environment for testing a version of the code when a pull request is created / updated with a 'deploy-pr' label +name: Deploy PR Environment +concurrency: + group: "deploy-${{ github.event.pull_request.head.ref }}" + cancel-in-progress: false + +on: + workflow_dispatch: + pull_request: + types: [ labeled, synchronize ] + +permissions: + id-token: write + contents: read + pull-requests: read + +env: + REF: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event_name == 'pull_request' && github.event.pull_request.head.ref }} + +jobs: + deploy-dev-pr-environment: + if: contains(github.event.pull_request.labels.*.name, 'deploy-pr') + runs-on: ubuntu-latest + outputs: + env_name: ${{ steps.env-name.outputs.PR_ENV_NAME }} + ref: ${{ steps.clean-ref.outputs.ref }} + steps: + - name: Clean Ref + id: clean-ref + shell: bash + run: | + BRANCH_NAME=${{ env.REF }} + CLEAN_BRANCH_NAME=${BRANCH_NAME#refs/heads/} + echo "ref=$CLEAN_BRANCH_NAME" >> $GITHUB_OUTPUT + + - name: Checkout the Tool and actions + uses: actions/checkout@v4 + with: + ref: ${{ steps.clean-ref.outputs.ref }} + fetch-depth: 1 + + - name: "Sanitize ENV name" + id: sanitize_env + shell: bash + run: | + SANITIZED_BRANCH_NAME=$(echo -n "${{ steps.clean-ref.outputs.ref }}" | tr "/" "-") + echo "Sanitized branch name: $SANITIZED_BRANCH_NAME" + TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-8) + echo "sanitized_env_name=$SANITIZED_BRANCH_NAME" >> $GITHUB_OUTPUT; + echo "trimmed_env_name=$TRIMMED_BRANCH_NAME" >> $GITHUB_OUTPUT; + + - name: Environment deployment + id: env-name + run: | + echo "deploying environment" + echo "PR_ENV_NAME=${{ steps.sanitize_env.outputs.trimmed_env_name }}" >> $GITHUB_ENV + echo "PR_ENV_NAME=${{ steps.sanitize_env.outputs.trimmed_env_name }}" >> $GITHUB_OUTPUT + + build-wf-service: + needs: deploy-dev-pr-environment + uses: ./.github/workflows/build-push-docker-images.yml + with: + registry: ghcr.io/${{ github.repository_owner }} + context: services/workflows-service + image_name: workflows-service + ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }} + tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }} + + build-backoffice: + needs: deploy-dev-pr-environment + uses: ./.github/workflows/build-push-docker-images.yml + with: + registry: ghcr.io/${{ github.repository_owner }} + context: apps/backoffice-v2 + image_name: backoffice + ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }} + tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }} + + build-kyb: + needs: deploy-dev-pr-environment + uses: ./.github/workflows/build-push-docker-images.yml + with: + registry: ghcr.io/${{ github.repository_owner }} + context: apps/kyb-app + image_name: kyb-app + ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }} + tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }} + + build-dashboard: + needs: deploy-dev-pr-environment + uses: ./.github/workflows/build-push-docker-images.yml + with: + registry: ghcr.io/${{ github.repository_owner }} + context: apps/workflows-dashboard + image_name: workflows-dashboard + ref: ${{ needs.deploy-dev-pr-environment.outputs.ref }} + tag: ${{ needs.deploy-dev-pr-environment.outputs.env_name }} + + deploy-preview: + needs: [deploy-dev-pr-environment,build-wf-service,build-backoffice,build-kyb,build-dashboard] + runs-on: ubuntu-latest + steps: + - name: Trigger workflow in another repo + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GIT_TOKEN }} + script: | + try { + await github.rest.repos.createDispatchEvent({ + owner: 'ballerine-io', + repo: 'cloud-infra-config', + event_type: 'deploy-preview', + client_payload: { + 'ref': '${{ needs.deploy-dev-pr-environment.outputs.env_name }}' + } + }); + console.log('Successfully triggered deploy-preview event'); + } catch (error) { + console.error('Failed to trigger deploy-preview event:', error); + throw error; + } \ No newline at end of file diff --git a/.github/workflows/build-push-docker-images.yml b/.github/workflows/build-push-docker-images.yml new file mode 100644 index 0000000000..12e75b06d0 --- /dev/null +++ b/.github/workflows/build-push-docker-images.yml @@ -0,0 +1,134 @@ +name: Build and Push Docker Images + +on: + workflow_call: + inputs: + registry: + required: true + description: "The Docker registry URL" + type: string + context: + required: true + description: "The build context path for the Docker image" + type: string + image_name: + required: true + description: "The name of the Docker image" + type: string + ref: + required: true + description: "Branch name of the Preview" + type: string + tag: + required: true + description: "Tag name of the Preview Image" + type: string + +permissions: + id-token: write + contents: write + pull-requests: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + fetch-depth: 1 + persist-credentials: false + sparse-checkout: | + ${{ inputs.context }} + sparse-checkout-cone-mode: true + + - name: Get tags + run: git fetch --tags origin + + - name: Get version + if: ${{ inputs.image_name }} == 'workflows-service' + id: version + run: | + TAG=$(git tag -l "$(echo ${{ inputs.image_name }}@)*" | sort -V -r | head -n 1) + echo "tag=$TAG" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "TAG=$TAG" >> "$GITHUB_ENV" + SHORT_SHA=$(git rev-parse --short HEAD) + echo "sha_short=$SHORT_SHA" >> "$GITHUB_OUTPUT" + echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV" + + - name: Bump version + id: bump-version + if: ${{ inputs.image_name }} == 'workflows-service' + uses: ./.github/actions/bump-version + with: + tag: ${{ steps.version.outputs.tag }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: 'arm64,arm' + + - name: Cache Docker layers + id: cache + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }} + restore-keys: | + ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }} + ${{ runner.os }}-docker- + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images + id: docker_meta + uses: docker/metadata-action@v4 + with: + images: ${{ inputs.registry }}/${{ inputs.image_name }} + tags: | + type=raw,value=${{ inputs.tag }} + type=sha,format=short + + - name: Print docker version outputs + run: | + echo "Metadata: ${{ steps.docker_meta.outputs.tags }}" + if [[ "${{ inputs.image_name }}" == "workflows-service" ]]; then + echo "sha_short: ${{ steps.version.outputs.sha_short }}" + echo "bump-version-version: ${{ steps.bump-version.outputs.version }}" + echo "bump-version-tag: ${{ steps.bump-version.outputs.tag }}" + fi + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ${{ inputs.context }} + platforms: linux/amd64 + push: true + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + tags: ${{ steps.docker_meta.outputs.tags }} + build-args: | + ${{ inputs.image_name == 'workflows-service' && format('"RELEASE={0}"\n"SHORT_SHA={1}"', steps.version.outputs.tag, steps.version.outputs.sha_short) || '' }} + + - name: Scan Docker Image + uses: aquasecurity/trivy-action@master + continue-on-error: true + with: + image-ref: ${{ steps.docker_meta.outputs.tags }} + format: 'table' + ignore-unfixed: true + exit-code: 1 + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + timeout: '5m' diff --git a/.github/workflows/db-ops.yaml b/.github/workflows/db-ops.yaml index 3ac73e61c1..90bd64becb 100644 --- a/.github/workflows/db-ops.yaml +++ b/.github/workflows/db-ops.yaml @@ -165,6 +165,7 @@ jobs: runs-on: ubuntu-latest needs: [update-helm-chart,build-and-push-ee-image] if: ${{ needs.update-helm-chart.result == 'success' }} + environment: ${{ github.event.client_payload.environment }} permissions: contents: read packages: write diff --git a/.github/workflows/deploy-wf-service.yml b/.github/workflows/deploy-wf-service.yml index b63ae57fdc..086d510694 100644 --- a/.github/workflows/deploy-wf-service.yml +++ b/.github/workflows/deploy-wf-service.yml @@ -256,6 +256,7 @@ jobs: runs-on: ubuntu-latest needs: [update-helm-chart,build-and-push-ee-image] if: ${{ needs.update-helm-chart.result == 'success' }} + environment: ${{ inputs.environment }} permissions: contents: read packages: write diff --git a/.github/workflows/destroy-preview-environment.yml b/.github/workflows/destroy-preview-environment.yml new file mode 100644 index 0000000000..ee4896cf50 --- /dev/null +++ b/.github/workflows/destroy-preview-environment.yml @@ -0,0 +1,83 @@ +# Destroys a temporary environment that was created forwhen a pull request is created / updated with a 'deploy-pr' label or triggerred manually +name: Destroy PR Environment +concurrency: + group: "deploy-${{ github.event.pull_request.head.ref }}" + cancel-in-progress: false + +on: + workflow_dispatch: + pull_request: + types: [ closed, unlabeled ] + +permissions: + id-token: write + contents: write + +env: + REF: ${{ github.event_name == 'workflow_dispatch' && github.ref || github.event_name == 'pull_request' && github.event.pull_request.head.ref }} + +jobs: + deploy-dev-pr-environment: + if: contains(github.event.pull_request.labels.*.name, 'deploy-pr') + runs-on: ubuntu-latest + outputs: + env_name: ${{ steps.env-name.outputs.PR_ENV_NAME }} + steps: + - name: Clean Ref + id: clean-ref + shell: bash + run: | + BRANCH_NAME=${{ env.REF }} + CLEAN_BRANCH_NAME=${BRANCH_NAME#refs/heads/} + echo "ref=$CLEAN_BRANCH_NAME" >> $GITHUB_OUTPUT + + - name: Checkout the Tool and actions + uses: actions/checkout@v4 + with: + ref: ${{ steps.clean-ref.outputs.ref }} + fetch-depth: 1 + + - name: "Sanitize ENV name" + id: sanitize_env + shell: bash + run: | + SANITIZED_BRANCH_NAME=$(echo -n ${{ steps.clean-ref.outputs.ref }} | tr "/" "-") + echo "Sanitized branch name: $SANITIZED_BRANCH_NAME" + TRIMMED_BRANCH_NAME=$(echo -n "$SANITIZED_BRANCH_NAME" | cut -c 1-8) + echo "sanitized_env_name=$SANITIZED_BRANCH_NAME" >> $GITHUB_OUTPUT; + echo "trimmed_env_name=$TRIMMED_BRANCH_NAME" >> $GITHUB_OUTPUT; + + - name: Environment deployment + id: env-name + run: | + echo "deploying environment" + echo "PR_ENV_NAME=${{ steps.sanitize_env.outputs.trimmed_env_name }}" >> $GITHUB_ENV + echo "PR_ENV_NAME=${{ steps.sanitize_env.outputs.trimmed_env_name }}" >> $GITHUB_OUTPUT + + destroy-preview: + needs: deploy-dev-pr-environment + if: | + (github.event_name == 'pull_request' && github.event.action == 'unlabeled' && github.event.label.name == 'deploy-pr') + || + (github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'deploy-pr')) + runs-on: ubuntu-latest + steps: + - name: Trigger workflow in another repo + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GIT_TOKEN }} + script: | + try { + await github.rest.repos.createDispatchEvent({ + owner: 'ballerine-io', + repo: 'cloud-infra-config', + event_type: 'destroy-preview', + client_payload: { + 'ref': '${{ needs.deploy-dev-pr-environment.outputs.env_name }}' + } + }); + console.log('Successfully triggered deploy-preview event'); + } catch (error) { + console.error('Failed to trigger deploy-preview event:', error); + throw error; + } \ No newline at end of file diff --git a/.github/workflows/hotfix-wf-service.yml b/.github/workflows/hotfix-wf-service.yml index 38bb066649..2f4c9f4ee2 100644 --- a/.github/workflows/hotfix-wf-service.yml +++ b/.github/workflows/hotfix-wf-service.yml @@ -329,6 +329,7 @@ jobs: send-to-slack: runs-on: ubuntu-latest needs: [update-helm-chart,build-and-push-ee-image,build-and-push-image] + environment: ${{ inputs.environment }} if: ${{ needs.update-helm-chart.result == 'success' }} permissions: contents: read diff --git a/apps/backoffice-v2/CHANGELOG.md b/apps/backoffice-v2/CHANGELOG.md index 02a48d91d1..d8ffb11998 100644 --- a/apps/backoffice-v2/CHANGELOG.md +++ b/apps/backoffice-v2/CHANGELOG.md @@ -1,5 +1,65 @@ # @ballerine/backoffice-v2 +## 0.7.81 + +### Patch Changes + +- Bump +- Updated dependencies + - @ballerine/blocks@0.2.29 + - @ballerine/common@0.9.58 + - @ballerine/react-pdf-toolkit@1.2.50 + - @ballerine/ui@0.5.50 + - @ballerine/workflow-browser-sdk@0.6.77 + - @ballerine/workflow-node-sdk@0.6.77 + +## 0.7.80 + +### Patch Changes + +- @ballerine/workflow-browser-sdk@0.6.76 +- @ballerine/workflow-node-sdk@0.6.76 + +## 0.7.79 + +### Patch Changes + +- Updated dependencies + - @ballerine/common@0.9.57 + - @ballerine/workflow-browser-sdk@0.6.75 + - @ballerine/workflow-node-sdk@0.6.75 + +## 0.7.78 + +### Patch Changes + +- version bump +- Updated dependencies + - @ballerine/common@0.9.56 + - @ballerine/workflow-browser-sdk@0.6.74 + - @ballerine/workflow-node-sdk@0.6.74 + +## 0.7.77 + +### Patch Changes + +- bump +- Updated dependencies + - @ballerine/blocks@0.2.28 + - @ballerine/common@0.9.55 + - @ballerine/react-pdf-toolkit@1.2.48 + - @ballerine/ui@0.5.48 + - @ballerine/workflow-browser-sdk@0.6.73 + - @ballerine/workflow-node-sdk@0.6.73 + +## 0.7.76 + +### Patch Changes + +- Updated dependencies + - @ballerine/workflow-browser-sdk@0.6.72 + - @ballerine/workflow-node-sdk@0.6.72 + ## 0.7.75 ### Patch Changes @@ -21,6 +81,10 @@ ### Patch Changes +- Updated dependencies + - @ballerine/common@0.9.54 + - @ballerine/workflow-browser-sdk@0.6.69 + - @ballerine/workflow-node-sdk@0.6.69 - @ballerine/workflow-browser-sdk@0.6.69 - @ballerine/workflow-node-sdk@0.6.69 @@ -103,6 +167,12 @@ - bump - Updated dependencies + - @ballerine/react-pdf-toolkit@1.2.40 + - @ballerine/workflow-browser-sdk@0.6.56 + - @ballerine/workflow-node-sdk@0.6.56 + - @ballerine/common@0.9.44 + - @ballerine/ui@0.5.40 + - @ballerine/blocks@0.2.24 - @ballerine/ui@0.5.43 - @ballerine/react-pdf-toolkit@1.2.43 diff --git a/apps/backoffice-v2/package.json b/apps/backoffice-v2/package.json index 4e0620effc..e72821f055 100644 --- a/apps/backoffice-v2/package.json +++ b/apps/backoffice-v2/package.json @@ -1,6 +1,6 @@ { "name": "@ballerine/backoffice-v2", - "version": "0.7.75", + "version": "0.7.81", "description": "Ballerine - Backoffice", "homepage": "https://github.com/ballerine-io/ballerine", "type": "module", @@ -51,12 +51,12 @@ "preview": "vite preview" }, "dependencies": { - "@ballerine/blocks": "0.2.27", - "@ballerine/common": "0.9.54", - "@ballerine/react-pdf-toolkit": "^1.2.47", - "@ballerine/ui": "^0.5.47", - "@ballerine/workflow-browser-sdk": "0.6.71", - "@ballerine/workflow-node-sdk": "0.6.71", + "@ballerine/blocks": "0.2.29", + "@ballerine/common": "0.9.58", + "@ballerine/react-pdf-toolkit": "^1.2.50", + "@ballerine/ui": "^0.5.50", + "@ballerine/workflow-browser-sdk": "0.6.77", + "@ballerine/workflow-node-sdk": "0.6.77", "@botpress/webchat": "^2.1.10", "@botpress/webchat-generator": "^0.2.9", "@fontsource/inter": "^4.5.15", @@ -117,6 +117,7 @@ "i18next-http-backend": "^2.1.1", "leaflet": "^1.9.4", "libphonenumber-js": "^1.10.49", + "lodash-es": "^4.17.21", "lowlight": "^3.1.0", "lucide-react": "0.445.0", "match-sorter": "^6.3.1", @@ -144,11 +145,11 @@ "tesseract.js": "^4.0.1", "ts-pattern": "^5.0.8", "vite-plugin-terminal": "^1.1.0", - "zod": "^3.22.3" + "zod": "^3.23.4" }, "devDependencies": { - "@ballerine/config": "^1.1.25", - "@ballerine/eslint-config-react": "^2.0.25", + "@ballerine/config": "^1.1.27", + "@ballerine/eslint-config-react": "^2.0.27", "@cspell/cspell-types": "^6.31.1", "@faker-js/faker": "^7.6.0", "@playwright/test": "^1.32.1", @@ -161,17 +162,18 @@ "@storybook/react-vite": "^7.0.0-rc.10", "@storybook/testing-library": "^0.0.14-next.1", "@tanstack/react-query-devtools": "4.22.0", - "@testing-library/jest-dom": "^5.16.4", - "@testing-library/react": "^13.3.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", "@total-typescript/ts-reset": "^0.5.1", "@types/d3-hierarchy": "^3.1.7", "@types/dompurify": "^3.0.5", "@types/leaflet": "^1.9.3", + "@types/lodash-es": "^4.17.12", "@types/node": "^18.11.13", "@types/qs": "^6.9.7", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", - "@types/testing-library__jest-dom": "^5.14.5", "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", "@vitejs/plugin-react-swc": "^3.0.1", @@ -197,7 +199,7 @@ "vite-plugin-mkcert": "^1.16.0", "vite-plugin-top-level-await": "^1.4.4", "vite-tsconfig-paths": "^5.0.1", - "vitest": "^0.29.8" + "vitest": "^2.1.8" }, "peerDependencies": { "react": "^17.0.0", diff --git a/apps/backoffice-v2/public/locales/en/toast.json b/apps/backoffice-v2/public/locales/en/toast.json index 7c6a044557..e7aa7685c7 100644 --- a/apps/backoffice-v2/public/locales/en/toast.json +++ b/apps/backoffice-v2/public/locales/en/toast.json @@ -100,5 +100,9 @@ "note_created": { "success": "Note added successfully.", "error": "Error occurred while adding note." + }, + "update_details": { + "success": "Details updated successfully.", + "error": "Error occurred while updating details." } } diff --git a/apps/backoffice-v2/src/Router/Router.tsx b/apps/backoffice-v2/src/Router/Router.tsx index 561776160a..60df33144b 100644 --- a/apps/backoffice-v2/src/Router/Router.tsx +++ b/apps/backoffice-v2/src/Router/Router.tsx @@ -1,35 +1,35 @@ -import React, { FunctionComponent } from 'react'; -import { env } from '@/common/env/env'; -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import { RootError } from '@/pages/Root/Root.error'; -import { Root } from '@/pages/Root/Root.page'; -import { SignIn } from '@/pages/SignIn/SignIn.page'; -import { Entity } from '@/pages/Entity/Entity.page'; -import { Entities } from '@/pages/Entities/Entities.page'; import { RouteError } from '@/common/components/atoms/RouteError/RouteError'; -import { CaseManagement } from '@/pages/CaseManagement/CaseManagement.page'; -import { rootLoader } from '@/pages/Root/Root.loader'; -import { entitiesLoader } from '@/pages/Entities/Entities.loader'; -import { authenticatedLayoutLoader } from '@/domains/auth/components/AuthenticatedLayout/AuthenticatedLayout.loader'; -import { entityLoader } from '@/pages/Entity/Entity.loader'; +import { RouteErrorWithProviders } from '@/common/components/atoms/RouteError/RouteErrorWithProviders'; +import { env } from '@/common/env/env'; import { AuthenticatedLayout } from '@/domains/auth/components/AuthenticatedLayout'; +import { authenticatedLayoutLoader } from '@/domains/auth/components/AuthenticatedLayout/AuthenticatedLayout.loader'; import { UnauthenticatedLayout } from '@/domains/auth/components/UnauthenticatedLayout'; -import { Locale } from '@/pages/Locale/Locale.page'; import { unauthenticatedLayoutLoader } from '@/domains/auth/components/UnauthenticatedLayout/UnauthenticatedLayout.loader'; +import { MerchantMonitoringLayout } from '@/domains/business-reports/components/MerchantMonitoringLayout/MerchantMonitoringLayout'; +import { CaseManagement } from '@/pages/CaseManagement/CaseManagement.page'; import { Document } from '@/pages/Document/Document.page'; -import { TransactionMonitoringAlerts } from '@/pages/TransactionMonitoringAlerts/TransactionMonitoringAlerts.page'; -import { TransactionMonitoring } from '@/pages/TransactionMonitoring/TransactionMonitoring'; -import { TransactionMonitoringAlertsAnalysisPage } from '@/pages/TransactionMonitoringAlertsAnalysis/TransactionMonitoringAlertsAnalysis.page'; +import { entitiesLoader } from '@/pages/Entities/Entities.loader'; +import { Entities } from '@/pages/Entities/Entities.page'; +import { entityLoader } from '@/pages/Entity/Entity.loader'; +import { Entity } from '@/pages/Entity/Entity.page'; import { Home } from '@/pages/Home/Home.page'; -import { Statistics } from '@/pages/Statistics/Statistics.page'; -import { Workflows } from '@/pages/Workflows/Workflows.page'; +import { Locale } from '@/pages/Locale/Locale.page'; import { MerchantMonitoring } from '@/pages/MerchantMonitoring/MerchantMonitoring.page'; -import { MerchantMonitoringCreateCheckPage } from '@/pages/MerchantMonitoringCreateCheck/MerchantMonitoringCreateCheck.page'; import { MerchantMonitoringBusinessReport } from '@/pages/MerchantMonitoringBusinessReport/MerchantMonitoringBusinessReport.page'; -import { MerchantMonitoringLayout } from '@/domains/business-reports/components/MerchantMonitoringLayout/MerchantMonitoringLayout'; -import { NotFoundRedirectWithProviders } from '@/pages/NotFound/NotFoundRedirectWithProviders'; -import { RouteErrorWithProviders } from '@/common/components/atoms/RouteError/RouteErrorWithProviders'; +import { MerchantMonitoringCreateCheckPage } from '@/pages/MerchantMonitoringCreateCheck/MerchantMonitoringCreateCheck.page'; import { MerchantMonitoringUploadMultiplePage } from '@/pages/MerchantMonitoringUploadMultiple/MerchantMonitoringUploadMultiple.page'; +import { NotFoundRedirectWithProviders } from '@/pages/NotFound/NotFoundRedirectWithProviders'; +import { RootError } from '@/pages/Root/Root.error'; +import { rootLoader } from '@/pages/Root/Root.loader'; +import { Root } from '@/pages/Root/Root.page'; +import { SignIn } from '@/pages/SignIn/SignIn.page'; +import { Statistics } from '@/pages/Statistics/Statistics.page'; +import { TransactionMonitoring } from '@/pages/TransactionMonitoring/TransactionMonitoring'; +import { TransactionMonitoringAlerts } from '@/pages/TransactionMonitoringAlerts/TransactionMonitoringAlerts.page'; +import { TransactionMonitoringAlertsAnalysisPage } from '@/pages/TransactionMonitoringAlertsAnalysis/TransactionMonitoringAlertsAnalysis.page'; +import { Workflows } from '@/pages/Workflows/Workflows.page'; +import { FunctionComponent } from 'react'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; const router = createBrowserRouter([ { @@ -176,7 +176,7 @@ const router = createBrowserRouter([ ], }, { - element: , + element: , loader: authenticatedLayoutLoader, errorElement: , path: '/:locale/case-management/entities/:entityId/document/:documentId', diff --git a/apps/backoffice-v2/src/common/components/molecules/ImageEditor/ImageEditor.tsx b/apps/backoffice-v2/src/common/components/molecules/ImageEditor/ImageEditor.tsx index aaa90eabbd..d6e561b997 100644 --- a/apps/backoffice-v2/src/common/components/molecules/ImageEditor/ImageEditor.tsx +++ b/apps/backoffice-v2/src/common/components/molecules/ImageEditor/ImageEditor.tsx @@ -1,9 +1,9 @@ import { FunctionComponentWithChildren } from '@/common/types'; -import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; import { ctw } from '@/common/utils/ctw/ctw'; import { isPdf } from '@/common/utils/is-pdf/is-pdf'; import { ComponentProps } from 'react'; import ReactCrop, { Crop } from 'react-image-crop'; +import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; export interface IImageEditorProps { onTransformed: NonNullable['onTransformed']>; @@ -28,31 +28,39 @@ export const ImageEditor: FunctionComponentWithChildren = ({ return ( div]:d-full': isPdf(image), - 'rotate-90': imageRotation === 90, - 'rotate-180': imageRotation === 180, - 'rotate-[270deg]': imageRotation === 270, + className={ctw('h-full w-full overflow-hidden [&>div]:!w-full', { + 'flex flex-row [&>div]:min-h-[600px]': isPdf(image), })} > - {children} +
+ {children} +
diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/EditableDetailsV2.tsx b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/EditableDetailsV2.tsx new file mode 100644 index 0000000000..47709c7051 --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/EditableDetailsV2.tsx @@ -0,0 +1,120 @@ +import { Button, TextWithNAFallback } from '@ballerine/ui'; + +import { FormField } from '../Form/Form.Field'; +import { titleCase } from 'string-ts'; +import { Form } from '../Form/Form'; +import { FunctionComponent } from 'react'; +import { FormItem } from '../Form/Form.Item'; +import { FormLabel } from '../Form/Form.Label'; +import { FormMessage } from '../Form/Form.Message'; +import { useEditableDetailsV2Logic } from './hooks/useEditableDetailsV2Logic/useEditableDetailsV2Logic'; +import { EditableDetailsV2Options } from './components/EditableDetailsV2Options'; +import { EditableDetailV2 } from './components/EditableDetailV2/EditableDetailV2'; +import { IEditableDetailsV2Props } from './types'; + +export const EditableDetailsV2: FunctionComponent = ({ + title, + fields, + onSubmit, + onEnableIsEditable, + onCancel, + config, +}) => { + if (config.blacklist && config.whitelist) { + throw new Error('Cannot provide both blacklist and whitelist'); + } + + const { form, handleSubmit, handleCancel, filteredFields } = useEditableDetailsV2Logic({ + fields, + onSubmit, + onCancel, + config, + }); + + return ( +
+
+

{title}

+ +
+
+ +
+ {title} + {filteredFields.map(({ title, value, path, props }) => { + return ( + ( + + + {titleCase(title ?? '')} + + + + + )} + /> + ); + })} +
+
+ {!config.actions.editing.disabled && + filteredFields?.some(({ props }) => props.isEditable) && ( + + )} + {!config.actions.editing.disabled && + filteredFields?.some(({ props }) => props.isEditable) && ( + + )} +
+
+ +
+ ); +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailV2/EditableDetailV2.test.tsx b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailV2/EditableDetailV2.test.tsx new file mode 100644 index 0000000000..88ddbce22c --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailV2/EditableDetailV2.test.tsx @@ -0,0 +1,315 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { cleanup, render, screen } from '@testing-library/react'; +import { EditableDetailV2 } from './EditableDetailV2'; +import { Form } from '@/common/components/organisms/Form/Form'; +import { useForm } from 'react-hook-form'; +import { FormField } from '@/common/components/organisms/Form/Form.Field'; +import dayjs from 'dayjs'; +import { FormLabel } from '@/common/components/organisms/Form/Form.Label'; +import { FormItem } from '@/common/components/organisms/Form/Form.Item'; + +afterEach(() => { + cleanup(); +}); + +describe.skip('EditableDetailV2', () => { + describe('datetime', () => { + describe('when isEditable is false', () => { + it('renders ISO dates', () => { + // Arrange + const fieldName = 'isoDate'; + const fieldValue = '1864-01-12T12:34:56Z'; + const WithSetup = () => { + const form = useForm({ + defaultValues: { + [fieldName]: fieldValue, + }, + }); + + return ( +
+ ( + + {field.name} + + + )} + name={fieldName} + /> + + ); + }; + + render(); + + // Act + const element = screen.getByRole('textbox'); + + // Assert + expect(element).toHaveAttribute('aria-readonly', 'true'); + expect(element).toHaveTextContent(dayjs(fieldValue).local().format('DD/MM/YYYY HH:mm')); + }); + + it('renders a format of YYYY-MM-DD HH:mm:ss', () => { + // Arrange + const fieldName = 'customFormat'; + const fieldValue = '1864-01-12 12:34:56'; + const WithSetup = () => { + const form = useForm({ + defaultValues: { + [fieldName]: fieldValue, + }, + }); + + return ( +
+ ( + + {field.name} + + + )} + name={fieldName} + /> + + ); + }; + + render(); + + // Act + const element = screen.getByRole('textbox'); + + // Assert + expect(element).toHaveAttribute('aria-readonly', 'true'); + expect(element).toHaveTextContent(dayjs(fieldValue).local().format('DD/MM/YYYY HH:mm')); + }); + }); + + describe('when isEditable is true', () => { + it('renders ISO dates', () => { + // Arrange + const fieldName = 'isoDate'; + const fieldValue = '1864-01-12T12:34:56Z'; + const WithSetup = () => { + const form = useForm({ + defaultValues: { + [fieldName]: fieldValue, + }, + }); + + return ( +
+ ( + + {field.name} + + + )} + name={fieldName} + /> + + ); + }; + + render(); + + // Act + const element = screen.getByLabelText(fieldName); + + // Assert + expect(element).toHaveAttribute('type', 'datetime-local'); + expect(element).toHaveValue(dayjs(fieldValue).local().format('YYYY-MM-DDTHH:mm:ss.000')); + }); + + it('renders a format of YYYY-MM-DD HH:mm:ss', () => { + // Arrange + const fieldName = 'customFormat'; + const fieldValue = '1864-01-12 12:34:56'; + const WithSetup = () => { + const form = useForm({ + defaultValues: { + [fieldName]: fieldValue, + }, + }); + + return ( +
+ ( + + {field.name} + + + )} + name={fieldName} + /> + + ); + }; + + render(); + + // Act + const element = screen.getByLabelText(fieldName); + + // Assert + expect(element).toHaveAttribute('type', 'datetime-local'); + expect(element).toHaveValue(dayjs(fieldValue).local().format('YYYY-MM-DDTHH:mm:ss.000')); + }); + }); + }); + + describe('date', () => { + describe('when isEditable is false', () => { + it('renders YYYY-DD-MM dates', () => { + // Arrange + const fieldName = 'date'; + const fieldValue = '1864-01-12'; + const WithSetup = () => { + const form = useForm({ + defaultValues: { + [fieldName]: fieldValue, + }, + }); + + return ( +
+ ( + + {field.name} + + + )} + name={fieldName} + /> + + ); + }; + + render(); + + // Act + const element = screen.getByRole('textbox'); + + // Assert + expect(element).toHaveAttribute('aria-readonly', 'true'); + expect(element).toHaveTextContent(dayjs(fieldValue).local().format('DD/MM/YYYY')); + }); + }); + + describe('when isEditable is true', () => { + it('renders YYYY-DD-MM dates', () => { + // Arrange + const fieldName = 'date'; + const fieldValue = '1864-01-12'; + const WithSetup = () => { + const form = useForm({ + defaultValues: { + [fieldName]: fieldValue, + }, + }); + + return ( +
+ ( + + {field.name} + + + )} + name={fieldName} + /> + + ); + }; + + render(); + + // Act + const element = screen.getByLabelText(fieldName); + + // Assert + expect(element).toHaveAttribute('type', 'date'); + expect(element).toHaveValue(dayjs(fieldValue).local().format('YYYY-MM-DD')); + }); + }); + }); +}); diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailV2/EditableDetailV2.tsx b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailV2/EditableDetailV2.tsx new file mode 100644 index 0000000000..516a88032c --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailV2/EditableDetailV2.tsx @@ -0,0 +1,207 @@ +import { ChangeEvent, useCallback } from 'react'; +import { checkIsFormattedDatetime } from '@/common/utils/check-is-formatted-datetime'; +import { FileJson2 } from 'lucide-react'; +import { BallerineLink, ctw, Input, JsonDialog } from '@ballerine/ui'; +import { checkIsUrl, isNullish, isObject } from '@ballerine/common'; +import { Select } from '../../../../atoms/Select/Select'; +import { SelectTrigger } from '../../../../atoms/Select/Select.Trigger'; +import { SelectValue } from '../../../../atoms/Select/Select.Value'; +import { SelectContent } from '../../../../atoms/Select/Select.Content'; +import { SelectItem } from '../../../../atoms/Select/Select.Item'; +import { keyFactory } from '@/common/utils/key-factory/key-factory'; +import { Checkbox_ } from '../../../../atoms/Checkbox_/Checkbox_'; +import dayjs from 'dayjs'; +import { ReadOnlyDetailV2 } from '../ReadOnlyDetailV2'; +import { getDisplayValue } from '../../utils/get-display-value'; +import { FormControl } from '../../../Form/Form.Control'; +import { getInputType } from '../../utils/get-input-type'; +import { checkIsDate } from '@/common/components/organisms/EditableDetailsV2/utils/check-is-date'; +import { checkIsDatetime } from '@/common/components/organisms/EditableDetailsV2/utils/check-is-datetime'; + +export const EditableDetailV2 = ({ + isEditable, + className, + options, + formValue, + onInputChange, + onOptionChange, + name, + value, + valueAlias, + type, + format, + minimum, + maximum, + pattern, + inputType, + parse, +}: { + isEditable: boolean; + className?: string; + options?: Array<{ + label: string; + value: string; + }>; + name: string; + value: any; + onInputChange: (name: string, value: unknown) => void; + onOptionChange: (...event: any[]) => void; + valueAlias?: string; + formValue: any; + type: string | undefined; + format: string | undefined; + minimum?: number; + maximum?: number; + pattern?: string; + inputType?: string; + parse?: { + date?: boolean; + isoDate?: boolean; + datetime?: boolean; + boolean?: boolean; + url?: boolean; + nullish?: boolean; + }; +}) => { + const displayValue = getDisplayValue({ value, formValue, isEditable }); + const handleInputChange = useCallback( + (event: ChangeEvent) => { + const getValue = () => { + if (event.target.value === 'N/A') { + return ''; + } + + const isValidDatetime = dayjs( + event.target.value, + ['YYYY-MM-DDTHH:mm', 'YYYY-MM-DDTHH:mm:ss'], + true, + ).isValid(); + + if (isValidDatetime) { + return dayjs(event.target.value).toISOString(); + } + + return event.target.value; + }; + const value = getValue(); + + onInputChange(name, value); + }, + [name, onInputChange], + ); + const isValidDatetime = [ + checkIsDatetime(value), + checkIsFormattedDatetime(value), + type === 'date-time', + ].some(Boolean); + const isValidIsoDate = checkIsDatetime(value); + + if (Array.isArray(value) || isObject(value)) { + return ( +
+ } + dialogButtonText={`View Information`} + json={JSON.stringify(value)} + /> +
+ ); + } + + if (isEditable && options) { + return ( + + ); + } + + if ( + parse?.boolean && + (typeof value === 'boolean' || type === 'boolean' || inputType === 'checkbox') + ) { + return ( + + + + ); + } + + if (isEditable) { + const computedInputType = inputType ?? getInputType({ format, type, value }); + + return ( + + + + ); + } + + if (typeof value === 'boolean' || type === 'boolean') { + return {`${value}`}; + } + + if (parse?.url && checkIsUrl(value)) { + return ( + + {valueAlias ?? value} + + ); + } + + if ((parse?.datetime && isValidDatetime) || (parse?.isoDate && isValidIsoDate)) { + return ( + + {dayjs(value).local().format('DD/MM/YYYY HH:mm')} + + ); + } + + if (parse?.date && (checkIsDate(value) || type === 'date')) { + return ( + {dayjs(value).format('DD/MM/YYYY')} + ); + } + + if (parse?.nullish && isNullish(value)) { + return {value}; + } + + if (isNullish(value)) { + return {`${value}`}; + } + + return {value}; +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailsV2Options.tsx b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailsV2Options.tsx new file mode 100644 index 0000000000..990cc260d9 --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailsV2Options.tsx @@ -0,0 +1,47 @@ +import { + DropdownMenuContent, + Button, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuItem, +} from '@ballerine/ui'; +import { Edit } from 'lucide-react'; +import { FunctionComponent } from 'react'; + +export const EditableDetailsV2Options: FunctionComponent<{ + actions: { + options: { + disabled: boolean; + }; + enableEditing: { + disabled: boolean; + }; + }; + onEnableIsEditable: () => void; +}> = ({ actions, onEnableIsEditable }) => { + return ( + + + + + + + + + + + ); +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/ReadOnlyDetailV2.tsx b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/ReadOnlyDetailV2.tsx new file mode 100644 index 0000000000..001c54efeb --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/components/ReadOnlyDetailV2.tsx @@ -0,0 +1,24 @@ +import { TextWithNAFallback, ctw } from '@ballerine/ui'; +import { FunctionComponent, ComponentProps } from 'react'; + +export const ReadOnlyDetailV2: FunctionComponent> = ({ + children, + className, + ...props +}) => { + return ( + + {children} + + ); +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/constants.ts b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/constants.ts new file mode 100644 index 0000000000..5c1dcc1062 --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/constants.ts @@ -0,0 +1 @@ +export const __ROOT__ = '__ROOT__'; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/hooks/useEditableDetailsV2Logic/useEditableDetailsV2Logic.tsx b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/hooks/useEditableDetailsV2Logic/useEditableDetailsV2Logic.tsx new file mode 100644 index 0000000000..e9d99a686e --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/hooks/useEditableDetailsV2Logic/useEditableDetailsV2Logic.tsx @@ -0,0 +1,151 @@ +import { ComponentProps, useCallback, useMemo } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { EditableDetailsV2 } from '../../EditableDetailsV2'; +import { isPathMatch } from '../../utils/is-path-match'; +import { isObject } from '@ballerine/common'; +import { get, set } from 'lodash-es'; +import { sortData } from '@/lib/blocks/utils/sort-data'; + +export const useEditableDetailsV2Logic = ({ + fields, + onSubmit, + onCancel, + config, +}: Pick< + ComponentProps, + 'fields' | 'onSubmit' | 'onCancel' | 'config' +>) => { + const sortedFields = useMemo( + () => + sortData({ + data: fields, + direction: config?.sort?.direction, + predefinedOrder: config?.sort?.predefinedOrder, + }), + [fields, config?.sort?.direction, config?.sort?.predefinedOrder], + ); + // Should support multiple levels of nesting, arrays, objects, and multiple path syntaxes + const filterValue = useCallback( + ({ path, root }: { path: string; root: string }) => + (value: any): any => { + if (!config.blacklist && !config.whitelist) { + return value; + } + + if (isObject(value)) { + return Object.entries(value).reduce((acc, [key, value]) => { + const fullPath = `${path}.${key}`; + const isBlacklisted = config.blacklist?.some(pattern => + isPathMatch({ + pattern, + path: fullPath, + root, + }), + ); + const isWhitelisted = + !config.whitelist || + config.whitelist?.some(pattern => + isPathMatch({ + pattern, + path: fullPath, + root, + }), + ); + + if (isBlacklisted) { + return acc; + } + + if (isWhitelisted) { + acc[key] = filterValue({ path: fullPath, root })(value); + } + + return acc; + }, {} as Record); + } + + if (Array.isArray(value)) { + return value.map((item, index) => filterValue({ path: `${path}.${index}`, root })(item)); + } + + return value; + }, + [config.blacklist, config.whitelist], + ); + + const filteredFields = useMemo(() => { + return sortedFields.filter(field => { + if (config.blacklist) { + return !config.blacklist.some(pattern => + isPathMatch({ + pattern, + path: field.path, + root: field.root, + }), + ); + } + + if (config.whitelist) { + return config.whitelist.some(pattern => + isPathMatch({ + pattern, + path: field.path, + root: field.root, + }), + ); + } + + return true; + }); + }, [sortedFields, config.blacklist, config.whitelist]); + const defaultValues = useMemo( + () => + filteredFields.reduce((acc, curr) => { + set(acc, curr.path, curr.value); + + return acc; + }, {} as Record), + [filteredFields], + ); + const form = useForm({ + defaultValues, + }); + + const handleSubmit: SubmitHandler> = useCallback( + values => { + const updatedData = fields.reduce((acc, curr) => { + const value = get(values, curr.path); + const defaultValue = get(defaultValues, curr.path); + + if (value === defaultValue) { + return acc; + } + + if (curr.id) { + const pathToObject = curr.path.split('.').slice(0, -1).join('.'); + + set(acc, `${pathToObject}.id`, curr.id); + } + + set(acc, curr.path, value); + + return acc; + }, {} as Record); + + onSubmit(updatedData); + }, + [fields, defaultValues, onSubmit], + ); + + const handleCancel = useCallback(() => { + form.reset(defaultValues); + onCancel(); + }, [defaultValues, form.reset, onCancel]); + + return { + form, + handleSubmit, + handleCancel, + filteredFields, + }; +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/types.ts b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/types.ts new file mode 100644 index 0000000000..ef03c12655 --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/types.ts @@ -0,0 +1,76 @@ +import { SortDirection } from '@ballerine/common'; + +export interface IBaseEditableDetailsV2Config { + parse?: { + date?: boolean; + isoDate?: boolean; + datetime?: boolean; + boolean?: boolean; + url?: boolean; + nullish?: boolean; + }; + sort?: { + direction?: SortDirection; + predefinedOrder?: string[]; + }; + actions: { + editing: { + disabled: boolean; + }; + options: { + disabled: boolean; + }; + enableEditing: { + disabled: boolean; + }; + cancel: { + disabled: boolean; + }; + save: { + disabled: boolean; + }; + }; + inputTypes?: Record; +} + +export interface IEditableDetailsV2ConfigWithBlacklist extends IBaseEditableDetailsV2Config { + blacklist: string[]; + whitelist?: never; +} + +export interface IEditableDetailsV2ConfigWithWhitelist extends IBaseEditableDetailsV2Config { + blacklist?: never; + whitelist: string[]; +} + +export type TEditableDetailsV2Config = + | IEditableDetailsV2ConfigWithBlacklist + | IEditableDetailsV2ConfigWithWhitelist; + +export interface IEditableDetailsV2Props { + title: string; + fields: Array<{ + id?: string; + title: string; + value: any; + props: { + valueAlias?: string; + type: string | undefined; + format: string | undefined; + isEditable: boolean; + pattern?: string; + minimum?: number; + maximum?: number; + options?: Array<{ + label: string; + value: string; + }>; + }; + path: string; + root: string; + }>; + onSubmit: (values: Record) => void; + onEnableIsEditable: () => void; + onCancel: () => void; + config: TEditableDetailsV2Config; +} diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/check-is-date.ts b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/check-is-date.ts new file mode 100644 index 0000000000..8a27888cfd --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/check-is-date.ts @@ -0,0 +1,6 @@ +import { isType } from '@ballerine/common'; +import { z } from 'zod'; + +export const checkIsDate = (value: unknown): value is string => { + return isType(z.string().date())(value); +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/check-is-datetime.ts b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/check-is-datetime.ts new file mode 100644 index 0000000000..c7fb0c4795 --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/check-is-datetime.ts @@ -0,0 +1,6 @@ +import { isType } from '@ballerine/common'; +import { z } from 'zod'; + +export const checkIsDatetime = (value: unknown): value is string => { + return isType(z.string().datetime())(value); +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/generate-editable-details-v2-fields.ts b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/generate-editable-details-v2-fields.ts new file mode 100644 index 0000000000..c99185d233 --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/generate-editable-details-v2-fields.ts @@ -0,0 +1,35 @@ +import { __ROOT__ } from '../constants'; +import { get } from 'lodash-es'; +import { getPropertyPath } from './get-property-path'; + +export const generateEditableDetailsV2Fields = + (obj: Record) => + ({ path, id }: { path: string; id?: string }) => { + const isWildcardPath = path === '*'; + const objectAtPath = isWildcardPath ? obj : get(obj, path); + const fields = Object.keys(objectAtPath).map(key => { + const pathToValue = isWildcardPath ? key : `${path}.${key}`; + const propertyPath = getPropertyPath({ + obj, + accessor: proxy => get(proxy, pathToValue), + propertyId: id, + }); + const root = isWildcardPath ? __ROOT__ : path; + + if (!root) { + throw new Error('Root is undefined'); + } + + return { + ...propertyPath, + root, + props: { + type: undefined, + format: undefined, + isEditable: true, + }, + }; + }); + + return fields; + }; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/get-display-value.ts b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/get-display-value.ts new file mode 100644 index 0000000000..98c1d756da --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/get-display-value.ts @@ -0,0 +1,30 @@ +import { isNullish } from '@ballerine/common'; +import { checkIsDatetime } from '@/common/components/organisms/EditableDetailsV2/utils/check-is-datetime'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(utc); + +export const getDisplayValue = ({ + value, + formValue, + isEditable, +}: { + value: TValue; + formValue: TFormValue; + isEditable: boolean; +}) => { + if (isEditable && checkIsDatetime(formValue)) { + return dayjs(formValue).local().format('YYYY-MM-DDTHH:mm:ss'); + } + + if (isEditable) { + return formValue; + } + + if (isNullish(value) || value === '') { + return 'N/A'; + } + + return value; +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/get-input-type.ts b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/get-input-type.ts new file mode 100644 index 0000000000..ef3f171037 --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/get-input-type.ts @@ -0,0 +1,39 @@ +import { checkIsFormattedDatetime } from '@/common/utils/check-is-formatted-datetime'; +import { checkIsDate } from '@/common/components/organisms/EditableDetailsV2/utils/check-is-date'; +import { checkIsDatetime } from '@/common/components/organisms/EditableDetailsV2/utils/check-is-datetime'; + +export const getInputType = ({ + format, + type, + value, +}: { + format: string | undefined; + type: string | undefined; + value: unknown; +}) => { + if (format === 'date-time' || checkIsDatetime(value) || checkIsFormattedDatetime(value)) { + return 'datetime-local'; + } + + if (format) { + return format; + } + + if (type === 'string') { + return 'text'; + } + + if (type === 'number' || (typeof value === 'number' && Number.isFinite(value))) { + return 'number'; + } + + if (checkIsDate(value) || type === 'date') { + return 'date'; + } + + if (!type) { + return 'text'; + } + + return type; +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/get-property-path.ts b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/get-property-path.ts new file mode 100644 index 0000000000..6b400e1fca --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/get-property-path.ts @@ -0,0 +1,38 @@ +import { get } from 'lodash-es'; + +export const getPropertyPath = >({ + obj, + accessor, + propertyId, +}: { + obj: TObj; + accessor: (proxy: TObj) => any; + propertyId?: string; +}) => { + const path: string[] = []; + + const proxy = new Proxy(obj, { + get(target: TObj, prop: PropertyKey) { + path.push(String(prop)); + + return new Proxy({}, this); + }, + }); + + // Invoke the accessor function to trigger the proxy + accessor(proxy); + + const fullPath = path.join('.'); + const prop = path.at(-1); + + if (!prop) { + throw new Error('Property path is empty'); + } + + return { + id: propertyId, + title: prop, + value: get(obj, path.join('.')), + path: fullPath, + }; +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/is-path-match.ts b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/is-path-match.ts new file mode 100644 index 0000000000..f9edbb51fc --- /dev/null +++ b/apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/utils/is-path-match.ts @@ -0,0 +1,47 @@ +import { __ROOT__ } from '../constants'; + +export const isPathMatch = ({ + pattern, + path, + root, +}: { + pattern: string; + path: string; + root: string; +}) => { + const patternParts = pattern.split('.'); + + // Exact matches, no wildcards. + if (!pattern.includes('*') && patternParts.length > 1) { + return pattern === path; + } + + /** + * @example pattern: 'id', path: 'entity.id' where root is 'entity' + * */ + if (patternParts.length === 1 && path === `${root}.${pattern}`) { + return true; + } + + // Match any path not at the root level. + if (pattern.startsWith('*.')) { + const parts = path.split('.'); + const suffix = pattern.slice(2); + + // parts.length > 2 ensures we have at least one level between root and the target field + return (parts.length > 2 || root === __ROOT__) && path.endsWith(suffix); + } + + const regexPattern = + pattern + // Escape dots for the regex + .replace(/\./g, '\\.') + // Replace * with regex pattern that matches any characters except dots + .replace(/\*/g, '[^.]+') + + // Make the pattern match both exact and partial paths + '(?:\\.[^.]+)*'; + + const regex = new RegExp(`^${regexPattern}$`); + + return regex.test(path); +}; diff --git a/apps/backoffice-v2/src/common/components/organisms/ImageViewer/ImageViewer.SelectedImage.tsx b/apps/backoffice-v2/src/common/components/organisms/ImageViewer/ImageViewer.SelectedImage.tsx index 04b570d6a5..9533453638 100644 --- a/apps/backoffice-v2/src/common/components/organisms/ImageViewer/ImageViewer.SelectedImage.tsx +++ b/apps/backoffice-v2/src/common/components/organisms/ImageViewer/ImageViewer.SelectedImage.tsx @@ -1,9 +1,9 @@ -import { BallerineImage } from '../../atoms/BallerineImage'; import { forwardRef, useCallback, useEffect, useState } from 'react'; import { ctw } from '../../../utils/ctw/ctw'; +import { isPdf } from '../../../utils/is-pdf/is-pdf'; +import { BallerineImage } from '../../atoms/BallerineImage'; import { useSelectedImage } from './hooks/useSelectedImage/useSelectedImage'; import { TSelectedImageProps } from './interfaces'; -import { isPdf } from '../../../utils/is-pdf/is-pdf'; /** * @description To be used by {@link ImageViewer}. Uses {@link BallerineImage} to display the currently selected image with default styling. @@ -34,10 +34,10 @@ export const SelectedImage = forwardRef @@ -50,7 +50,7 @@ export const SelectedImage = forwardRef error: , warning: , }} + closeButton toastOptions={{ ...toastOptions, classNames: { toast: 'group toast group-[.toaster]:shadow-lg font-inter', actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground', cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground', + closeButton: 'group-[.toast]:opacity-70 group-[.toast]:hover:opacity-100', }, }} {...props} diff --git a/apps/backoffice-v2/src/common/env/schema.ts b/apps/backoffice-v2/src/common/env/schema.ts index ab346db9aa..5be4b76602 100644 --- a/apps/backoffice-v2/src/common/env/schema.ts +++ b/apps/backoffice-v2/src/common/env/schema.ts @@ -39,4 +39,5 @@ export const EnvSchema = z.object({ return new RegExp(value); }, z.custom(value => value instanceof RegExp).optional()), VITE_SAOLA_API_KEY: z.string().optional(), + VITE_BOTPRESS_CLIENT_ID: z.string().default('8f29c89d-ec0e-494d-b18d-6c3590b28be6'), }); diff --git a/apps/backoffice-v2/src/common/utils/check-is-formatted-datetime/check-is-formatted-datetime.ts b/apps/backoffice-v2/src/common/utils/check-is-formatted-datetime/check-is-formatted-datetime.ts new file mode 100644 index 0000000000..b352cd4d8d --- /dev/null +++ b/apps/backoffice-v2/src/common/utils/check-is-formatted-datetime/check-is-formatted-datetime.ts @@ -0,0 +1,9 @@ +/** + * @description Checks if a passed value is a string that match the format YYYY-MM-DD HH:MM:SS. + * @param value + */ +export const checkIsFormattedDatetime = (value: unknown): value is string => { + if (typeof value !== 'string') return false; + + return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value); +}; diff --git a/apps/backoffice-v2/src/common/utils/check-is-formatted-datetime/index.ts b/apps/backoffice-v2/src/common/utils/check-is-formatted-datetime/index.ts new file mode 100644 index 0000000000..162a192dae --- /dev/null +++ b/apps/backoffice-v2/src/common/utils/check-is-formatted-datetime/index.ts @@ -0,0 +1 @@ +export { checkIsFormattedDatetime } from './check-is-formatted-datetime'; diff --git a/apps/backoffice-v2/src/common/utils/fetcher/fetcher.ts b/apps/backoffice-v2/src/common/utils/fetcher/fetcher.ts index 7c95e6f656..2e57879c56 100644 --- a/apps/backoffice-v2/src/common/utils/fetcher/fetcher.ts +++ b/apps/backoffice-v2/src/common/utils/fetcher/fetcher.ts @@ -69,7 +69,7 @@ export const fetcher: IFetcher = async ({ return await handlePromise(res.blob()); } - if (!res.headers.get('content-length') || Number(res.headers.get('content-length')) > 0) { + if (!res.headers.get('content-length') || Number(res.headers.get('content-length') || 0) > 0) { // TODO: make sure its json by checking the content-type in order to safe access to json method return await handlePromise(res.json()); } diff --git a/apps/backoffice-v2/src/domains/chat/chatbot-opengpt.tsx b/apps/backoffice-v2/src/domains/chat/chatbot-opengpt.tsx index bc0462d6db..c10b74cad2 100644 --- a/apps/backoffice-v2/src/domains/chat/chatbot-opengpt.tsx +++ b/apps/backoffice-v2/src/domains/chat/chatbot-opengpt.tsx @@ -1,7 +1,9 @@ -import { getClient, Webchat, WebchatProvider } from '@botpress/webchat'; +import { getClient, Webchat, WebchatProvider, WebchatClient } from '@botpress/webchat'; import { buildTheme } from '@botpress/webchat-generator'; -import { useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useAuthenticatedUserQuery } from '../../domains/auth/hooks/queries/useAuthenticatedUserQuery/useAuthenticatedUserQuery'; +import { useCurrentCaseQuery } from '../../pages/Entity/hooks/useCurrentCaseQuery/useCurrentCaseQuery'; +import { useParams } from 'react-router-dom'; // declare const themeNames: readonly ["prism", "galaxy", "dusk", "eggplant", "dawn", "midnight"]; const { theme, style } = buildTheme({ @@ -9,30 +11,70 @@ const { theme, style } = buildTheme({ themeColor: 'blue', }); -const clientId = '8f29c89d-ec0e-494d-b18d-6c3590b28be6'; - const Chatbot = ({ isWebchatOpen, toggleIsWebchatOpen, + botpressClientId, }: { isWebchatOpen: boolean; toggleIsWebchatOpen: () => void; + botpressClientId: string; }) => { - const client = getClient({ clientId }); + const [client, setClient] = useState(null); const { data: session } = useAuthenticatedUserQuery(); + const { data: currentCase } = useCurrentCaseQuery(); + const { entityId: caseId } = useParams(); + + const sendCurrentCaseData = useCallback( + async (botpressClient: WebchatClient | null = client) => { + if (!currentCase || !botpressClient) { + return; + } + + try { + await botpressClient.sendEvent({ + type: 'case-data', + data: currentCase.context, + }); + } catch (error) { + console.error('Failed to send case data:', error); + } + }, + [currentCase, client], + ); useEffect(() => { - if (session?.user) { - const { firstName, lastName, email } = session.user; - void client.updateUser({ + if (client || !botpressClientId || !session?.user) { + return; + } + + const { firstName, lastName, email } = session.user; + const botpressClientInstance = getClient({ clientId: botpressClientId }); + setClient(botpressClientInstance); + + botpressClientInstance.on('conversation', (ev: any) => { + void botpressClientInstance.updateUser({ data: { firstName, lastName, email, }, }); + setTimeout(() => { + void sendCurrentCaseData(botpressClientInstance); + }, 0); + }); + }, [session, client, sendCurrentCaseData, botpressClientId]); + + useEffect(() => { + if (caseId) { + void sendCurrentCaseData(); } - }, [session, client]); + }, [caseId, sendCurrentCaseData]); + + if (!client) { + return null; + } return (
diff --git a/apps/backoffice-v2/src/domains/customer/fetchers.ts b/apps/backoffice-v2/src/domains/customer/fetchers.ts index 94587404af..dbe623de31 100644 --- a/apps/backoffice-v2/src/domains/customer/fetchers.ts +++ b/apps/backoffice-v2/src/domains/customer/fetchers.ts @@ -21,6 +21,9 @@ const CustomerSchema = z.object({ language: z.union([z.string(), z.null()]).optional(), features: z .object({ + chatbot: z + .object({ enabled: z.boolean().default(false), clientId: z.string().optional() }) + .optional(), createBusinessReport: z .object({ enabled: z.boolean().default(false), options: createBusinessReportOptions }) .optional(), @@ -35,7 +38,6 @@ const CustomerSchema = z.object({ isMerchantMonitoringEnabled: z.boolean().default(false), isExample: z.boolean().default(false), isDemo: z.boolean().default(false), - isChatbotEnabled: z.boolean().default(false), }) .nullable() .default({ diff --git a/apps/backoffice-v2/src/domains/notes/Notes.tsx b/apps/backoffice-v2/src/domains/notes/Notes.tsx index 951d57c617..68822d8384 100644 --- a/apps/backoffice-v2/src/domains/notes/Notes.tsx +++ b/apps/backoffice-v2/src/domains/notes/Notes.tsx @@ -88,7 +88,7 @@ export const Notes = ({ size={`sm`} aria-disabled={isLoading} className={ - 'mt-3 h-5 self-end p-4 text-sm font-medium aria-disabled:pointer-events-none aria-disabled:opacity-50' + 'mt-3 h-5 self-end p-4 text-sm font-medium enabled:bg-primary enabled:hover:bg-primary/90 aria-disabled:pointer-events-none aria-disabled:opacity-50' } > diff --git a/apps/backoffice-v2/src/domains/transactions/fetchers.ts b/apps/backoffice-v2/src/domains/transactions/fetchers.ts index 4a2c7e13be..ae7c46a971 100644 --- a/apps/backoffice-v2/src/domains/transactions/fetchers.ts +++ b/apps/backoffice-v2/src/domains/transactions/fetchers.ts @@ -178,7 +178,7 @@ export const TransactionsListSchema = z.array( export type TTransactionsList = z.output; export const fetchTransactions = async (params: { - counterpartyId: string; + counterpartyId?: string; page: { number: number; size: number; diff --git a/apps/backoffice-v2/src/domains/transactions/hooks/queries/useTransactionsQuery/useTransactionsQuery.tsx b/apps/backoffice-v2/src/domains/transactions/hooks/queries/useTransactionsQuery/useTransactionsQuery.tsx index 1f43e0d085..04e7b57478 100644 --- a/apps/backoffice-v2/src/domains/transactions/hooks/queries/useTransactionsQuery/useTransactionsQuery.tsx +++ b/apps/backoffice-v2/src/domains/transactions/hooks/queries/useTransactionsQuery/useTransactionsQuery.tsx @@ -9,7 +9,7 @@ export const useTransactionsQuery = ({ pageSize, }: { alertId: string; - counterpartyId: string; + counterpartyId?: string; page: number; pageSize: number; }) => { @@ -22,7 +22,7 @@ export const useTransactionsQuery = ({ page, pageSize, }), - enabled: isAuthenticated && !!counterpartyId, + enabled: isAuthenticated, staleTime: 100_000, }); }; diff --git a/apps/backoffice-v2/src/domains/transactions/query-keys.ts b/apps/backoffice-v2/src/domains/transactions/query-keys.ts index 158d98ec23..f806c25f79 100644 --- a/apps/backoffice-v2/src/domains/transactions/query-keys.ts +++ b/apps/backoffice-v2/src/domains/transactions/query-keys.ts @@ -8,7 +8,7 @@ export const transactionsQueryKeys = createQueryKeys('transactions', { ...params }: { alertId: string; - counterpartyId: string; + counterpartyId?: string; page: number; pageSize: number; }) => { diff --git a/apps/backoffice-v2/src/domains/workflow-definitions/fetchers.ts b/apps/backoffice-v2/src/domains/workflow-definitions/fetchers.ts index e353beead5..16cb3de6f1 100644 --- a/apps/backoffice-v2/src/domains/workflow-definitions/fetchers.ts +++ b/apps/backoffice-v2/src/domains/workflow-definitions/fetchers.ts @@ -47,6 +47,15 @@ export const WorkflowDefinitionConfigSchema = z .optional(), }) .optional(), + editableContext: z + .object({ + kyc: z + .object({ + entity: z.boolean().optional(), + }) + .optional(), + }) + .optional(), }) .passthrough() .nullable(); diff --git a/apps/backoffice-v2/src/domains/workflows/fetchers.ts b/apps/backoffice-v2/src/domains/workflows/fetchers.ts index ac1f9ab036..af75288bb8 100644 --- a/apps/backoffice-v2/src/domains/workflows/fetchers.ts +++ b/apps/backoffice-v2/src/domains/workflows/fetchers.ts @@ -13,6 +13,23 @@ import { z } from 'zod'; import { IWorkflowId } from './interfaces'; import { zPropertyKey } from '@/lib/zod/utils/z-property-key/z-property-key'; +export const updateContextAndSyncEntity = async ({ + workflowId, + data, +}: { + workflowId: string; + data: Partial; +}) => { + const [workflow, error] = await apiClient({ + endpoint: `../external/workflows/${workflowId}/sync-entity`, + method: Method.PATCH, + body: data, + schema: z.undefined(), + }); + + return handleZodError(error, workflow); +}; + export const fetchWorkflows = async (params: { filterId: string; orderBy: string; diff --git a/apps/backoffice-v2/src/domains/workflows/hooks/mutations/useUpdateContextAndSyncEntity/useUpdateContextAndSyncEntity.tsx b/apps/backoffice-v2/src/domains/workflows/hooks/mutations/useUpdateContextAndSyncEntity/useUpdateContextAndSyncEntity.tsx new file mode 100644 index 0000000000..a0de2d8918 --- /dev/null +++ b/apps/backoffice-v2/src/domains/workflows/hooks/mutations/useUpdateContextAndSyncEntity/useUpdateContextAndSyncEntity.tsx @@ -0,0 +1,34 @@ +import { TWorkflowById, updateContextAndSyncEntity } from '@/domains/workflows/fetchers'; + +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; +import { t } from 'i18next'; +import { workflowsQueryKeys } from '../../../query-keys'; + +export const useUpdateContextAndSyncEntityMutation = ({ + workflowId, + onSuccess, +}: { + workflowId: string; + onSuccess: (data: null, variables: Partial, context: unknown) => void; +}) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: Partial) => + await updateContextAndSyncEntity({ + workflowId, + data, + }), + onSuccess: (...args) => { + void queryClient.invalidateQueries(workflowsQueryKeys._def); + + toast.success(t('toast:update_details.success')); + + onSuccess(...args); + }, + onError: () => { + toast.error(t('toast:update_details.error')); + }, + }); +}; diff --git a/apps/backoffice-v2/src/index.css b/apps/backoffice-v2/src/index.css index 4965a08679..1e85734aa2 100644 --- a/apps/backoffice-v2/src/index.css +++ b/apps/backoffice-v2/src/index.css @@ -318,10 +318,7 @@ } } -a.bpComposerPoweredBy { +a.bpComposerPoweredBy, +div.bpHeaderExpandedContentDescriptionItemsPoweredBy { display: none !important; } - -button.bpModalDialogNewConversationButton { - background-color: var(--bpPrimary-500) !important; -} diff --git a/apps/backoffice-v2/src/lib/blocks/components/BlockCell/BlockCell.tsx b/apps/backoffice-v2/src/lib/blocks/components/BlockCell/BlockCell.tsx index ab5298c6b7..ae31ba0fc7 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/BlockCell/BlockCell.tsx +++ b/apps/backoffice-v2/src/lib/blocks/components/BlockCell/BlockCell.tsx @@ -1,14 +1,15 @@ -import { ctw } from '@/common/utils/ctw/ctw'; -import { CardContent } from '@/common/components/atoms/Card/Card.Content'; import { Card } from '@/common/components/atoms/Card/Card'; -import { FunctionComponent } from 'react'; -import { Block } from '@ballerine/blocks'; +import { CardContent } from '@/common/components/atoms/Card/Card.Content'; +import { ctw } from '@/common/utils/ctw/ctw'; import { cells } from '@/lib/blocks/create-blocks-typed/create-blocks-typed'; +import { Block } from '@ballerine/blocks'; +import { FunctionComponent } from 'react'; interface IBlockCellProps { value: Block; props?: { className?: string; + contentClassName?: string; }; } @@ -20,9 +21,13 @@ export const BlockCell: FunctionComponent = ({ value, props }) return ( cell?.type === 'multiDocuments'), - })} + className={ctw( + 'grid gap-2', + { + 'grid-cols-2': value?.some(cell => cell?.type === 'multiDocuments'), + }, + props?.contentClassName, + )} > {value?.map((cell, index) => { const Cell = cells[cell?.type]; diff --git a/apps/backoffice-v2/src/lib/blocks/components/CallToActionLegacy/CallToActionLegacy.tsx b/apps/backoffice-v2/src/lib/blocks/components/CallToActionLegacy/CallToActionLegacy.tsx index c62acd5d0a..cd4da51747 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/CallToActionLegacy/CallToActionLegacy.tsx +++ b/apps/backoffice-v2/src/lib/blocks/components/CallToActionLegacy/CallToActionLegacy.tsx @@ -189,7 +189,10 @@ export const CallToActionLegacy: FunctionComponent = ( size="wide" variant="warning" disabled={disabled} - className={ctw({ 'flex gap-2': isReuploadResetable })} + className={ctw( + { 'flex gap-2': isReuploadResetable }, + 'enabled:bg-warning enabled:hover:bg-warning/90', + )} > {value.text} {isReuploadResetable && ( @@ -253,9 +256,11 @@ export const CallToActionLegacy: FunctionComponent = ( } close={ @@ -57,9 +61,11 @@ export const DefaultActions = () => {

Create Merchant Check

-

Registered Company Name

diff --git a/apps/backoffice-v2/src/pages/Root/Root.page.tsx b/apps/backoffice-v2/src/pages/Root/Root.page.tsx index 4655a1ab77..f2fa055f7d 100644 --- a/apps/backoffice-v2/src/pages/Root/Root.page.tsx +++ b/apps/backoffice-v2/src/pages/Root/Root.page.tsx @@ -1,10 +1,11 @@ -import React, { FunctionComponent, lazy, useState } from 'react'; -import { Outlet } from 'react-router-dom'; +import { FunctionComponent, lazy, useState } from 'react'; import { Providers } from '../../common/components/templates/Providers/Providers'; -import { ServerDownLayout } from './ServerDown.layout'; import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; import { FullScreenLoader } from '@/common/components/molecules/FullScreenLoader/FullScreenLoader'; import Chatbot from '@/domains/chat/chatbot-opengpt'; +import { env } from '@/common/env/env'; +import { Outlet } from 'react-router-dom'; +import { ServerDownLayout } from './ServerDown.layout'; const ReactQueryDevtools = lazy(() => process.env.NODE_ENV !== 'production' @@ -25,11 +26,19 @@ const ChatbotLayout: FunctionComponent = () => { return ; } - if (!customer?.config?.isChatbotEnabled) { + if (!customer?.features?.chatbot?.enabled) { return null; } - return ; + const botpressClientId = customer?.features?.chatbot?.clientId || env.VITE_BOTPRESS_CLIENT_ID; + + return ( + + ); }; export const Root: FunctionComponent = () => { diff --git a/apps/backoffice-v2/src/pages/SignIn/SignIn.page.tsx b/apps/backoffice-v2/src/pages/SignIn/SignIn.page.tsx index 2390fd8e0b..9a3c420949 100644 --- a/apps/backoffice-v2/src/pages/SignIn/SignIn.page.tsx +++ b/apps/backoffice-v2/src/pages/SignIn/SignIn.page.tsx @@ -101,7 +101,10 @@ export const SignIn: FunctionComponent = () => { )} />
-
diff --git a/apps/backoffice-v2/src/pages/TransactionMonitoringAlertsAnalysis/hooks/useTransactionMonitoringAlertsAnalysisPageLogic/useTransactionMonitoringAlertsAnalysisPageLogic.tsx b/apps/backoffice-v2/src/pages/TransactionMonitoringAlertsAnalysis/hooks/useTransactionMonitoringAlertsAnalysisPageLogic/useTransactionMonitoringAlertsAnalysisPageLogic.tsx index 9e4be9d012..ee98601bfa 100644 --- a/apps/backoffice-v2/src/pages/TransactionMonitoringAlertsAnalysis/hooks/useTransactionMonitoringAlertsAnalysisPageLogic/useTransactionMonitoringAlertsAnalysisPageLogic.tsx +++ b/apps/backoffice-v2/src/pages/TransactionMonitoringAlertsAnalysis/hooks/useTransactionMonitoringAlertsAnalysisPageLogic/useTransactionMonitoringAlertsAnalysisPageLogic.tsx @@ -5,15 +5,14 @@ import { useTransactionsQuery } from '@/domains/transactions/hooks/queries/useTr import { useCallback } from 'react'; export const useTransactionMonitoringAlertsAnalysisPageLogic = () => { - const [{ businessId, counterpartyId }] = useSerializedSearchParams(); + const [{ counterpartyId }] = useSerializedSearchParams(); const { alertId } = useParams(); const { data: alertDefinition, isLoading: isLoadingAlertDefinition } = useAlertDefinitionByAlertIdQuery({ alertId: alertId ?? '', }); const { data: transactions } = useTransactionsQuery({ - alertId: alertId ?? '', - businessId: businessId ?? '', + alertId: alertId?.toString() ?? '', // @TODO: Remove counterpartyId: counterpartyId ?? '', page: 1, diff --git a/apps/backoffice-v2/src/tests-setup.ts b/apps/backoffice-v2/src/tests-setup.ts index 86779502e1..bb02c60cd0 100644 --- a/apps/backoffice-v2/src/tests-setup.ts +++ b/apps/backoffice-v2/src/tests-setup.ts @@ -1,4 +1 @@ -import { expect } from 'vitest'; -import matchers from '@testing-library/jest-dom/matchers'; - -expect.extend(matchers); +import '@testing-library/jest-dom/vitest'; diff --git a/apps/kyb-app/CHANGELOG.md b/apps/kyb-app/CHANGELOG.md index 02dbc591e4..84b3716849 100644 --- a/apps/kyb-app/CHANGELOG.md +++ b/apps/kyb-app/CHANGELOG.md @@ -1,11 +1,72 @@ # kyb-app +## 0.3.93 + +### Patch Changes + +- Bump +- Updated dependencies + - @ballerine/blocks@0.2.29 + - @ballerine/common@0.9.58 + - @ballerine/ui@0.5.50 + - @ballerine/workflow-browser-sdk@0.6.77 + +## 0.3.92 + +### Patch Changes + +- @ballerine/workflow-browser-sdk@0.6.76 + +## 0.3.91 + +### Patch Changes + +- Updated dependencies + - @ballerine/common@0.9.57 + - @ballerine/workflow-browser-sdk@0.6.75 + +## 0.3.90 + +### Patch Changes + +- Updated dependencies + - @ballerine/common@0.9.56 + - @ballerine/workflow-browser-sdk@0.6.74 + +## 0.3.89 + +### Patch Changes + +- Updated dependencies + - @ballerine/ui@0.5.49 + +## 0.3.88 + +### Patch Changes + +- bump +- Updated dependencies + - @ballerine/blocks@0.2.28 + - @ballerine/common@0.9.55 + - @ballerine/ui@0.5.48 + - @ballerine/workflow-browser-sdk@0.6.73 + +## 0.3.87 + +### Patch Changes + +- Updated dependencies + - @ballerine/workflow-browser-sdk@0.6.72 + ## 0.3.86 +## 0.3.84 + ### Patch Changes - Updated dependencies - @ballerine/common@0.9.54 + - @ballerine/workflow-browser-sdk@0.6.69 - @ballerine/workflow-browser-sdk@0.6.71 ## 0.3.85 @@ -90,7 +151,10 @@ - bump - Updated dependencies + - @ballerine/blocks@0.2.24 + - @ballerine/common@0.9.44 - @ballerine/ui@0.5.43 + - @ballerine/workflow-browser-sdk@0.6.56 ## 0.3.75 diff --git a/apps/kyb-app/package.json b/apps/kyb-app/package.json index fdc39269ec..1ad628ec17 100644 --- a/apps/kyb-app/package.json +++ b/apps/kyb-app/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/kyb-app", "private": true, - "version": "0.3.86", + "version": "0.3.93", "type": "module", "scripts": { "dev": "vite", @@ -15,10 +15,10 @@ "test:dev": "vitest" }, "dependencies": { - "@ballerine/blocks": "0.2.27", - "@ballerine/common": "^0.9.54", - "@ballerine/ui": "0.5.47", - "@ballerine/workflow-browser-sdk": "0.6.71", + "@ballerine/blocks": "0.2.29", + "@ballerine/common": "^0.9.58", + "@ballerine/ui": "0.5.50", + "@ballerine/workflow-browser-sdk": "0.6.77", "@lukemorales/query-key-factory": "^1.0.3", "@radix-ui/react-icons": "^1.3.0", "@rjsf/core": "^5.9.0", @@ -64,8 +64,8 @@ "zod": "^3.23.4" }, "devDependencies": { - "@ballerine/config": "^1.1.25", - "@ballerine/eslint-config-react": "^2.0.25", + "@ballerine/config": "^1.1.27", + "@ballerine/eslint-config-react": "^2.0.27", "@jest/globals": "^29.7.0", "@sentry/vite-plugin": "^2.9.0", "@testing-library/jest-dom": "^6.1.4", diff --git a/apps/kyb-app/src/common/components/atoms/StepperProgress/StepperProgress.tsx b/apps/kyb-app/src/common/components/atoms/StepperProgress/StepperProgress.tsx index 0ac3eb9318..736fa0b9d4 100644 --- a/apps/kyb-app/src/common/components/atoms/StepperProgress/StepperProgress.tsx +++ b/apps/kyb-app/src/common/components/atoms/StepperProgress/StepperProgress.tsx @@ -8,5 +8,16 @@ interface Props { export const StepperProgress = ({ currentStep, totalSteps }: Props) => { const { t } = useTranslation(); - return {`${t('step')} ${currentStep} / ${totalSteps}`}; + return ( +
+
+ {t('step')} +
+ {currentStep} + / + {totalSteps} +
+
+
+ ); }; diff --git a/apps/kyb-app/src/common/types/settings.ts b/apps/kyb-app/src/common/types/settings.ts index d5310f4fb8..b386bd9897 100644 --- a/apps/kyb-app/src/common/types/settings.ts +++ b/apps/kyb-app/src/common/types/settings.ts @@ -8,6 +8,7 @@ export interface ITheme { poweredBy?: boolean; }; signup?: { + showJobTitle?: boolean; companyLogo: { imageSrc?: string; styles?: CSSProperties; diff --git a/apps/kyb-app/src/components/layouts/AppShell/AppShell.tsx b/apps/kyb-app/src/components/layouts/AppShell/AppShell.tsx index cf60de6b5b..bac48a36cf 100644 --- a/apps/kyb-app/src/components/layouts/AppShell/AppShell.tsx +++ b/apps/kyb-app/src/components/layouts/AppShell/AppShell.tsx @@ -10,7 +10,7 @@ interface Props { } export const AppShell = ({ children }: Props) => { - return
{children}
; + return
{children}
; }; AppShell.FormContainer = FormContainer; diff --git a/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx b/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx index 42ddce49b3..485c134df4 100644 --- a/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx +++ b/apps/kyb-app/src/components/layouts/AppShell/FormContainer.tsx @@ -1,4 +1,6 @@ +import { usePageResolverContext } from '@/components/organisms/DynamicUI/PageResolver/hooks/usePageResolverContext'; import { AnyChildren, ScrollArea } from '@ballerine/ui'; +import { useEffect, useRef } from 'react'; interface Props { children: AnyChildren; @@ -6,8 +8,25 @@ interface Props { } export const FormContainer = ({ children, header }: Props) => { + const scrollAreaRef = useRef(null); + + const { currentPage } = usePageResolverContext(); + + // Scrolls to top of the page when page changes + useEffect(() => { + if (scrollAreaRef.current) { + setTimeout(() => { + scrollAreaRef.current!.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }, 100); + } + }, [currentPage]); + return ( - + //@ts-ignore +
{header ?
{header}
: null}
{children}
diff --git a/apps/kyb-app/src/components/layouts/AppShell/Sidebar.tsx b/apps/kyb-app/src/components/layouts/AppShell/Sidebar.tsx index 48baa1ea1c..182f66871b 100644 --- a/apps/kyb-app/src/components/layouts/AppShell/Sidebar.tsx +++ b/apps/kyb-app/src/components/layouts/AppShell/Sidebar.tsx @@ -7,7 +7,7 @@ interface Props { export const Sidebar = ({ children }: Props) => { return (