From 056b66ba3b60ac0be298947500caeb8a66b3daf6 Mon Sep 17 00:00:00 2001 From: Venceslas Burongu Date: Fri, 14 Nov 2025 12:14:44 +0200 Subject: [PATCH 1/4] allow the review page (panviewer) to render a pdf --- packages/commons/src/documents.ts | 2 +- .../DocumentViewer/components/PanViewer.tsx | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/commons/src/documents.ts b/packages/commons/src/documents.ts index 9e59a701fe1..a6052ba541d 100644 --- a/packages/commons/src/documents.ts +++ b/packages/commons/src/documents.ts @@ -14,7 +14,7 @@ import { extendZodWithOpenApi } from 'zod-openapi' extendZodWithOpenApi(z) export const MINIO_REGEX = - /^https?:\/\/[^\/]+(.*)?\/[^\/?]+\.(jpg|png|jpeg|svg)(\?.*)?$/i + /^https?:\/\/[^\/]+(.*)?\/[^\/?]+\.(jpg|png|jpeg|pdf|svg)(\?.*)?$/i export function isBase64FileString(str: string) { if (str === '' || str.trim() === '') { diff --git a/packages/components/src/DocumentViewer/components/PanViewer.tsx b/packages/components/src/DocumentViewer/components/PanViewer.tsx index 815281362a0..235c93c3d6b 100644 --- a/packages/components/src/DocumentViewer/components/PanViewer.tsx +++ b/packages/components/src/DocumentViewer/components/PanViewer.tsx @@ -42,6 +42,19 @@ const PanViewer: React.FC = ({ image, zoom, rotation }) => { const [dx] = useState(0) const [dy] = useState(0) + const isPdf = image.toLowerCase().endsWith('.pdf') + + React.useEffect(() => { + if (isPdf) { + fetch(image) + .then((res) => res.blob()) + .then((blob) => { + const blobUrl = URL.createObjectURL(blob) + window.open(blobUrl, '_blank') + }) + } + }, [image, isPdf]) + return ( = ({ image, zoom, rotation }) => { rotation={rotation} key={dx} > - Supporting Document + {!isPdf ? ( + Supporting Document + ) : ( +
+ PDF opened in a new tab +
+ )}
) From 8dcb39693a05d1b22cf34ee5554295a6326d7dc6 Mon Sep 17 00:00:00 2001 From: Venceslas Burongu Date: Fri, 14 Nov 2025 12:20:11 +0200 Subject: [PATCH 2/4] update the changelog --- CHANGELOG.md | 116 ++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6732e68003..c63ef16add0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ field(.query-params).get('data.code') - **Removed support for following scopes** + - `NATLSYSADMIN` - `DECLARE` - `VALIDATE` @@ -29,11 +30,13 @@ - `RECORD_REGISTRATION_VERIFY_CERTIFIED_COPIES` - `PROFILE_UPDATE` + - Review page now supports PDF rendering when a PDF link is clicked allowing to verify the uploaded PDF + ## [1.9.0](https://github.com/opencrvs/opencrvs-core/compare/v1.8.1...v1.9.0) ### Breaking changes -* Dashboard configuration through **Metabase** has been fully migrated to **countryconfig**, and the standalone dashboard package has been removed. +- Dashboard configuration through **Metabase** has been fully migrated to **countryconfig**, and the standalone dashboard package has been removed. For details on configuring dashboards and information about the latest updates, refer to the [ANALYTICS.md](https://github.com/opencrvs/opencrvs-countryconfig/blob/v1.9.0/ANALYTICS.md) documentation. ### New features @@ -63,7 +66,6 @@ These actions must be executed in a defined order and cannot be skipped. Each action must be accepted by **countryconfig** before the next one can be performed. - ###### Rejecting and Archiving After declaration, instead of proceeding with registration, an event may be either **rejected** or **archived**. @@ -71,18 +73,16 @@ After declaration, instead of proceeding with registration, an event may be eith If **deduplication** is enabled for an action, performing that action may trigger a **DUPLICATE_DETECTED** action if duplicates are found. When this occurs, two additional actions become available: -* **MARK_AS_DUPLICATE** – archives the event. -* **MARK_AS_NOT_DUPLICATE** – resumes the normal action flow. +- **MARK_AS_DUPLICATE** – archives the event. +- **MARK_AS_NOT_DUPLICATE** – resumes the normal action flow. If an event is rejected by a user, the preceding action must be repeated before continuing. - ###### Actions Before Declaration 1. **NOTIFY** – a partial version of the `DECLARE` action. 2. **DELETE** – an event can be deleted only if no declaration action has yet been performed. - ###### Actions After Registration Once an event has been registered, a certificate may be printed. @@ -93,7 +93,6 @@ If a correction is required due to an error in the registered declaration, a cor 3. **REJECT_CORRECTION** 4. **APPROVE_CORRECTION** - ###### General / Meta Actions 1. **READ** – appended to the action trail whenever a complete event record is retrieved. @@ -104,8 +103,8 @@ If a correction is required due to an error in the registered declaration, a cor Event data is collected through **Forms**, which come in two types: -* **Declaration Form** – collects data about the event itself -* **Action Form** – collects data specific to a particular action, also known as annotation data in the system +- **Declaration Form** – collects data about the event itself +- **Action Form** – collects data specific to a particular action, also known as annotation data in the system Forms are composed of **Pages**, and pages are composed of **Fields**. Fields can be shown, hidden, or enabled dynamically based on the values of other fields, allowing for a responsive and intuitive user experience. @@ -129,12 +128,12 @@ The `field` function (exported from `@opencrvs/toolkit`) includes a set of helpe ##### Available helpers include: -* **Boolean connectors**: `and`, `or`, `not` -* **Basic conditions**: `alwaysTrue`, `never` -* **Comparisons**: `isAfter`, `isBefore`, `isGreaterThan`, `isLessThan`, `isBetween`, `isEqualTo` -* **Field state checks**: `isFalsy`, `isUndefined`, `inArray`, `matches` (regex patterns) -* **Age-specific helpers**: `asAge`, `asDob` (to compare age or date of birth) -* **Nested fields**: +- **Boolean connectors**: `and`, `or`, `not` +- **Basic conditions**: `alwaysTrue`, `never` +- **Comparisons**: `isAfter`, `isBefore`, `isGreaterThan`, `isLessThan`, `isBetween`, `isEqualTo` +- **Field state checks**: `isFalsy`, `isUndefined`, `inArray`, `matches` (regex patterns) +- **Age-specific helpers**: `asAge`, `asDob` (to compare age or date of birth) +- **Nested fields**: ```ts field('parent.field.name').get('nested.field').isTruthy() @@ -150,9 +149,9 @@ user.isOnline() These conditions can control: -* `SHOW` – whether a component is visible -* `ENABLE` – whether a component is interactive -* `DISPLAY_ON_REVIEW` – whether a field appears on review pages +- `SHOW` – whether a component is visible +- `ENABLE` – whether a component is interactive +- `DISPLAY_ON_REVIEW` – whether a field appears on review pages They can also be used to validate form data dynamically based on the current form state or user context. @@ -167,14 +166,14 @@ Advanced search is now configurable through the `EventConfig.advancedSearch` pro You can search across: -* **Declaration Fields** – using the same `field` function from declaration forms with helpers such as `range`, `exact`, `fuzzy`, and `within` -* **Event Metadata** – using the `event` function to search against metadata such as: +- **Declaration Fields** – using the same `field` function from declaration forms with helpers such as `range`, `exact`, `fuzzy`, and `within` +- **Event Metadata** – using the `event` function to search against metadata such as: - * `trackingId` - * `status` - * `legalStatuses.REGISTERED.acceptedAt` - * `legalStatuses.REGISTERED.createdAtLocation` - * `updatedAt` + - `trackingId` + - `status` + - `legalStatuses.REGISTERED.acceptedAt` + - `legalStatuses.REGISTERED.createdAtLocation` + - `updatedAt` More details about the metadata fields are available in `packages/commons/src/events/EventMetadata.ts`. @@ -202,27 +201,25 @@ Countryconfig has full control over how the action is processed and may choose t By hooking into these action trigger routes, countryconfig can also: -* Send customized **Notifications** -* Access the full event data at the time an action is performed +- Send customized **Notifications** +- Access the full event data at the time an action is performed #### Configurable Workqueues Workqueues can now be configured from countryconfig using the `defineWorkqueues` function from `@opencrvs/toolkit/events`. This enables the creation of role- or workflow-specific queues without requiring code changes in core. -* The **`actions`** property is used to define the default actions displayed for records within a workqueue. -* The **`query`** property is used to determine which events are included in the workqueue. -* The **`workqueue[id=workqueue-one|workqueue-two]`** scope is used to control the visibility of workqueues for particular roles. +- The **`actions`** property is used to define the default actions displayed for records within a workqueue. +- The **`query`** property is used to determine which events are included in the workqueue. +- The **`workqueue[id=workqueue-one|workqueue-two]`** scope is used to control the visibility of workqueues for particular roles. Details on the available configuration options can be found in the `WorkqueueConfig.ts` file. - #### Event Overview -The configuration of the event overview page (formerly known as *Record Audit*) has been made customizable through the `EventConfig.summary` property. +The configuration of the event overview page (formerly known as _Record Audit_) has been made customizable through the `EventConfig.summary` property. The record details displayed on this page can be referenced directly from the declaration form or defined as custom fields that combine multiple form values. If some value contains PII data, they can optionally be hidden via the `secured` flag so that the data will only be visible once the record is assigned to the user. - #### Quick Search The dropdown previously available in the search bar has been removed. @@ -232,50 +229,58 @@ Any search performed through the **Quick Search** bar is now executed against co The following variables are available for use within certificate templates: -* **`$declaration`** – Contains the latest raw declaration form data. Typically used with the `$lookup` Handlebars helper to resolve values into human-readable text. -* **`$metadata`** – Contains the `EventMetadata` object. Commonly used with the `$lookup` helper for resolving metadata fields into readable values. -* **`$review`** – A boolean flag indicating whether the certificate is being rendered in review mode. -* **`$references`** – Contains reference data for locations and users, accessible via `{{ $references.locations }}` and `{{ $references.users }}`. +- **`$declaration`** – Contains the latest raw declaration form data. Typically used with the `$lookup` Handlebars helper to resolve values into human-readable text. +- **`$metadata`** – Contains the `EventMetadata` object. Commonly used with the `$lookup` helper for resolving metadata fields into readable values. +- **`$review`** – A boolean flag indicating whether the certificate is being rendered in review mode. +- **`$references`** – Contains reference data for locations and users, accessible via `{{ $references.locations }}` and `{{ $references.users }}`. This is useful when manually resolving values from `$declaration`, `$metadata` or `action`. ##### Handlebars Helpers The following helpers are supported within certificate templates: -* **`$lookup`** – Resolves values from `$declaration`, `$metadata`, or `action` data into a human-readable format. -* **`$intl`** – Dynamically constructs a translation key by joining multiple string parts. +- **`$lookup`** – Resolves values from `$declaration`, `$metadata`, or `action` data into a human-readable format. +- **`$intl`** – Dynamically constructs a translation key by joining multiple string parts. Example: ```hbs - {{ $intl 'constants.greeting' (lookup $declaration "child.name") }} + {{$intl 'constants.greeting' (lookup $declaration 'child.name')}} ``` -* **`$intlWithParams`** – Enables dynamic translations with parameters. + +- **`$intlWithParams`** – Enables dynamic translations with parameters. Takes a translation ID as the first argument, followed by parameter name–value pairs. Example: ```hbs - {{ $intlWithParams 'constants.greeting' 'name' (lookup $declaration "child.name") }} + {{$intlWithParams + 'constants.greeting' + 'name' + (lookup $declaration 'child.name') + }} ``` -* **`$actions`** – Resolves all actions for a specified action type. + +- **`$actions`** – Resolves all actions for a specified action type. Example: ```hbs - {{ $actions "PRINT_CERTIFICATE" }} + {{$actions 'PRINT_CERTIFICATE'}} ``` -* **`$action`** – Retrieves the latest action data for a specific action type. + +- **`$action`** – Retrieves the latest action data for a specific action type. Example: ```hbs - {{ $action "PRINT_CERTIFICATE" }} + {{$action 'PRINT_CERTIFICATE'}} ``` -* **`ifCond`** – Compares two values (`v1` and `v2`) using the specified operator and conditionally renders a block based on the result. + +- **`ifCond`** – Compares two values (`v1` and `v2`) using the specified operator and conditionally renders a block based on the result. **Supported operators:** - * `'==='` – strict equality - * `'!=='` – strict inequality - * `'<'`, `'<='`, `'>'`, `'>='` – numeric or string comparisons - * `'&&'` – both values must be truthy - * `'||'` – at least one value must be truthy + - `'==='` – strict equality + - `'!=='` – strict inequality + - `'<'`, `'<='`, `'>'`, `'>='` – numeric or string comparisons + - `'&&'` – both values must be truthy + - `'||'` – at least one value must be truthy **Usage example:** @@ -284,8 +289,9 @@ The following helpers are supported within certificate templates: ... {{/ifCond}} ``` -* **`$or`** – Returns the first truthy value among the provided arguments. -* **`$json`** – Converts any value to its JSON string representation (useful for debugging). + +- **`$or`** – Returns the first truthy value among the provided arguments. +- **`$json`** – Converts any value to its JSON string representation (useful for debugging). Besides the ones introduced above, all built-in [Handlebars helpers](https://handlebarsjs.com/guide/builtin-helpers.html) are available. @@ -297,8 +303,6 @@ To see Events V2 in action, check out the example configurations in the **countr --- - - - **Redis password support with authorization and authentication** [#9338](https://github.com/opencrvs/opencrvs-core/pull/9338). By default password is disabled for local development environment and enabled on server environments. - **Switch back to default redis image** [#10173](https://github.com/opencrvs/opencrvs-core/issues/10173) - **Certificate Template Conditionals**: Certificate template conditionals allow dynamic template selection based on print history using the template conditional helpers.. [#7585](https://github.com/opencrvs/opencrvs-core/issues/7585) From c6d8b0e11ca7a4b2d501f1432e98c1df62187a81 Mon Sep 17 00:00:00 2001 From: Venceslas Burongu Date: Fri, 14 Nov 2025 22:54:26 +0200 Subject: [PATCH 3/4] edit pan viewer logic --- .../DocumentViewer/components/PanViewer.tsx | 36 ++++++++++++------- packages/components/src/Icon/all-icons.ts | 1 + 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/components/src/DocumentViewer/components/PanViewer.tsx b/packages/components/src/DocumentViewer/components/PanViewer.tsx index 235c93c3d6b..7dbc01cc5e0 100644 --- a/packages/components/src/DocumentViewer/components/PanViewer.tsx +++ b/packages/components/src/DocumentViewer/components/PanViewer.tsx @@ -11,6 +11,8 @@ import * as React from 'react' import { useState } from 'react' import ReactPanZoom from './PanDraggable' +import { Button } from '../../Button' +import { Icon } from '../../Icon' import styled from 'styled-components' const StyledReactPanZoom = styled(ReactPanZoom)<{ @@ -42,18 +44,18 @@ const PanViewer: React.FC = ({ image, zoom, rotation }) => { const [dx] = useState(0) const [dy] = useState(0) - const isPdf = image.toLowerCase().endsWith('.pdf') + const isPdf = image.endsWith('.pdf') - React.useEffect(() => { - if (isPdf) { - fetch(image) - .then((res) => res.blob()) - .then((blob) => { - const blobUrl = URL.createObjectURL(blob) - window.open(blobUrl, '_blank') - }) + const handleOpenPdf = async () => { + try { + const res = await fetch(image, { cache: 'default' }) + const blob = await res.blob() + const blobUrl = URL.createObjectURL(blob) + window.open(blobUrl, '_blank') + } catch (err) { + console.error('Failed to open PDF', err) } - }, [image, isPdf]) + } return ( @@ -71,9 +73,17 @@ const PanViewer: React.FC = ({ image, zoom, rotation }) => { style={{ transform: `rotate(${rotation}deg)` }} /> ) : ( -
- PDF opened in a new tab -
+ )}
diff --git a/packages/components/src/Icon/all-icons.ts b/packages/components/src/Icon/all-icons.ts index 4f8dd67eb5e..dbfe317dfd7 100644 --- a/packages/components/src/Icon/all-icons.ts +++ b/packages/components/src/Icon/all-icons.ts @@ -16,6 +16,7 @@ export { ArchiveTray, ArrowCircleDown, ArrowCounterClockwise, + ArrowSquareOut, ArrowLeft, ArrowRight, Buildings, From 500f9362b481c99857ea4bb3cdb3e5a70e061a31 Mon Sep 17 00:00:00 2001 From: Venceslas Burongu Date: Mon, 17 Nov 2025 12:28:13 +0200 Subject: [PATCH 4/4] add translation for the oprn pdf button --- packages/client/src/tests/languages.json | 1 + .../src/DocumentViewer/components/PanViewer.tsx | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/client/src/tests/languages.json b/packages/client/src/tests/languages.json index 3a64083932d..8d4b95fe019 100644 --- a/packages/client/src/tests/languages.json +++ b/packages/client/src/tests/languages.json @@ -1017,6 +1017,7 @@ "review.modal.desc.submitConfirmation": "{completeDeclaration, select, true {This declaration will be sent to the Registrar for them to review.} false {This declaration will be sent to the Registrar for completion. Please inform the Informant that they will need to visit the office with the missing information and supporting documents.}}", "review.modal.title.registerConfirmation": "Register this {event}?", "review.modal.title.submitConfirmation": "{completeDeclaration, select, true {Send declaration for review?} false {Send incomplete declaration?}}", + "review.panViewer.openPDF": "Open PDF in new tab", "review.rejection.form": "Rejection form", "review.rejection.form.commentInstruction": "Please provide specific instructions of what needs to be updated by the health worker to correctly update the declaration", "review.rejection.form.commentLabel": "Comments or instructions for health worker to rectify declaration", diff --git a/packages/components/src/DocumentViewer/components/PanViewer.tsx b/packages/components/src/DocumentViewer/components/PanViewer.tsx index 7dbc01cc5e0..f1f1870c000 100644 --- a/packages/components/src/DocumentViewer/components/PanViewer.tsx +++ b/packages/components/src/DocumentViewer/components/PanViewer.tsx @@ -9,6 +9,7 @@ * Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS. */ import * as React from 'react' +import { useIntl } from 'react-intl' import { useState } from 'react' import ReactPanZoom from './PanDraggable' import { Button } from '../../Button' @@ -41,6 +42,7 @@ interface IProps { } const PanViewer: React.FC = ({ image, zoom, rotation }) => { + const intl = useIntl() const [dx] = useState(0) const [dy] = useState(0) @@ -81,7 +83,11 @@ const PanViewer: React.FC = ({ image, zoom, rotation }) => { type="positive" onClick={handleOpenPdf} > - Open PDF in a new tab + {intl.formatMessage({ + id: 'review.panViewer.openPDF', + defaultMessage: 'Open PDF in a new tab', + description: 'Label for open PDF button' + })} )}