, 'ref'> & TableDataAdditionalProps;
function TableData(props: Props) {
const {
className,
children,
+ expandedContentCell,
+ withoutBorder,
...otherProps
} = props;
return (
|
diff --git a/packages/ui/src/components/Table/TableData/styles.module.css b/packages/ui/src/components/Table/TableData/styles.module.css
index 7e8d347ff..fcd5d8138 100644
--- a/packages/ui/src/components/Table/TableData/styles.module.css
+++ b/packages/ui/src/components/Table/TableData/styles.module.css
@@ -1,3 +1,15 @@
-.td {
- border-bottom: var(--go-ui-width-separator-sm) solid var(--go-ui-color-separator);
+.table-data {
+ --cell-border-color: var(--go-ui-color-separator);
+ --cell-background-color: transparent;
+
+ border-bottom: var(--go-ui-width-separator-sm) solid var(--cell-border-color);
+ background-color: var(--cell-background-color);
+ padding: var(--go-ui-spacing-sm);
+ overflow: hidden;
+ overflow-wrap: break-word;
+
+ &.expanded-content-cell {
+ --cell-border-color: var(--go-ui-color-separator-light);
+ --cell-background-color: var(--go-ui-color-background);
+ }
}
diff --git a/packages/ui/src/components/Table/types.ts b/packages/ui/src/components/Table/types.ts
index b33da8217..213653c30 100644
--- a/packages/ui/src/components/Table/types.ts
+++ b/packages/ui/src/components/Table/types.ts
@@ -1,3 +1,5 @@
+import { type TableDataAdditionalProps } from './TableData';
+
export type SortDirection = 'asc' | 'dsc';
export interface BaseHeader {
@@ -34,6 +36,12 @@ export interface Column {
cellRendererClassName?: string;
cellContainerClassName?: string;
+ cellContainerRendererParams?: (
+ key: KEY,
+ datum: DATA,
+ index: number,
+ data: DATA[],
+ ) => TableDataAdditionalProps;
}
export type VerifyColumn = unknown extends (
From 56e1dcad831fcbf4673bd3bf21821a1de6b0b923 Mon Sep 17 00:00:00 2001
From: frozenhelium
Date: Tue, 25 Nov 2025 11:48:00 +0545
Subject: [PATCH 18/19] feat(eap): add PrintableDescription component
- With support for the diff view
---
app/package.json | 1 +
app/src/components/PerExportModal/index.tsx | 12 +-
.../domain/DrefExportModal/index.tsx | 8 +-
.../domain/EapExportModal/index.tsx | 9 +-
.../printable/PrintableDescription/index.tsx | 88 +++++++++++
.../PrintableDescription/styles.module.css | 18 +++
app/src/utils/constants.ts | 1 -
.../EapTableActions/i18n.json | 2 -
app/src/views/AccountMyFormsEap/i18n.json | 3 +-
app/src/views/AccountMyFormsEap/index.tsx | 12 +-
app/src/views/SimplifiedEapExport/i18n.json | 3 +-
app/src/views/SimplifiedEapExport/index.tsx | 144 ++++++++++++------
.../ApproachesInput/i18n.json | 1 -
.../SimplifiedEapForm/RiskAnalysis/i18n.json | 1 -
.../ExpansionIndicator/styles.module.css | 4 +-
pnpm-lock.yaml | 15 +-
16 files changed, 251 insertions(+), 71 deletions(-)
create mode 100644 app/src/components/printable/PrintableDescription/index.tsx
create mode 100644 app/src/components/printable/PrintableDescription/styles.module.css
diff --git a/app/package.json b/app/package.json
index ea3ee7d2a..fd752598d 100644
--- a/app/package.json
+++ b/app/package.json
@@ -48,6 +48,7 @@
"@togglecorp/toggle-request": "^1.0.0-beta.3",
"@turf/bbox": "^6.5.0",
"@turf/buffer": "^6.5.0",
+ "diff": "^8.0.2",
"exceljs": "^4.3.0",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
diff --git a/app/src/components/PerExportModal/index.tsx b/app/src/components/PerExportModal/index.tsx
index 732fbdb2b..0602d3938 100644
--- a/app/src/components/PerExportModal/index.tsx
+++ b/app/src/components/PerExportModal/index.tsx
@@ -14,11 +14,15 @@ import {
import Link from '#components/Link';
import { type components } from '#generated/types';
-import { useRequest } from '#utils/restRequest';
+import {
+ type GoApiBody,
+ useRequest,
+} from '#utils/restRequest';
import i18n from './i18n.json';
type ExportStatusEnum = components<'read'>['schemas']['ExportStatusEnum'];
+type ExportBody = GoApiBody<'/api/v2/pdf-export/', 'POST'>;
const EXPORT_STATUS_PENDING = 0 satisfies ExportStatusEnum;
const EXPORT_STATUS_COMPLETED = 1 satisfies ExportStatusEnum;
@@ -45,8 +49,10 @@ function PerExportModal(props: Props) {
export_id: Number(perId),
export_type: 'per' as const,
per_country: Number(countryId),
- is_pga: false,
- }),
+ is_pga: undefined,
+ version: undefined,
+ diff: undefined,
+ } satisfies ExportBody),
[perId, countryId],
);
diff --git a/app/src/components/domain/DrefExportModal/index.tsx b/app/src/components/domain/DrefExportModal/index.tsx
index 0bcebcca0..837b67168 100644
--- a/app/src/components/domain/DrefExportModal/index.tsx
+++ b/app/src/components/domain/DrefExportModal/index.tsx
@@ -24,6 +24,7 @@ import {
type TypeOfDrefEnum,
} from '#utils/constants';
import {
+ type GoApiBody,
useLazyRequest,
useRequest,
} from '#utils/restRequest';
@@ -33,6 +34,7 @@ import styles from './styles.module.css';
type ExportTypeEnum = components<'read'>['schemas']['ExportTypeEnum'];
type ExportStatusEnum = components<'read'>['schemas']['ExportStatusEnum'];
+type ExportBody = GoApiBody<'/api/v2/pdf-export/', 'POST'>;
const EXPORT_STATUS_PENDING = 0 satisfies ExportStatusEnum;
const EXPORT_STATUS_COMPLETED = 1 satisfies ExportStatusEnum;
@@ -83,9 +85,11 @@ function DrefExportModal(props: Props) {
export_id: id,
export_type: type,
is_pga: includePga,
- selector: '#pdf-preview-ready',
+ // selector: '#pdf-preview-ready',
per_country: undefined,
- };
+ version: undefined,
+ diff: undefined,
+ } satisfies ExportBody;
},
[
id,
diff --git a/app/src/components/domain/EapExportModal/index.tsx b/app/src/components/domain/EapExportModal/index.tsx
index 7ebe2d17d..669d7a6b6 100644
--- a/app/src/components/domain/EapExportModal/index.tsx
+++ b/app/src/components/domain/EapExportModal/index.tsx
@@ -41,6 +41,7 @@ interface Props {
eapType: EapType;
version?: number;
onClose: () => void;
+ diff?: boolean;
}
function EapExportModal(props: Props) {
@@ -49,6 +50,7 @@ function EapExportModal(props: Props) {
eapType,
onClose,
version,
+ diff,
} = props;
const strings = useTranslation(i18n);
@@ -59,13 +61,14 @@ function EapExportModal(props: Props) {
const exportTriggerBody = useMemo(
() => ({
export_id: eapId,
- export_type: eapType === EAP_TYPE_SIMPLIFIED ? 'simplified-eap' : 'full-eap',
+ export_type: eapType === EAP_TYPE_SIMPLIFIED ? 'simplified' : 'full',
selector: '#pdf-preview-ready',
- is_pga: false,
+ is_pga: undefined,
per_country: undefined,
version,
+ diff,
}),
- [eapId, eapType, version],
+ [eapId, eapType, version, diff],
);
const {
diff --git a/app/src/components/printable/PrintableDescription/index.tsx b/app/src/components/printable/PrintableDescription/index.tsx
new file mode 100644
index 000000000..1843a9402
--- /dev/null
+++ b/app/src/components/printable/PrintableDescription/index.tsx
@@ -0,0 +1,88 @@
+import {
+ Fragment,
+ useMemo,
+} from 'react';
+import {
+ _cs,
+ isNotDefined,
+} from '@togglecorp/fujs';
+import { diffSentences } from 'diff';
+
+import styles from './styles.module.css';
+
+interface Props {
+ value?: string | null;
+ className?: string;
+ prevValue?: string | null;
+}
+
+function PrintableDescription(props: Props) {
+ const {
+ className,
+ value,
+ prevValue,
+ } = props;
+
+ const diff = useMemo(() => {
+ if (isNotDefined(value) || isNotDefined(prevValue)) {
+ return undefined;
+ }
+
+ return diffSentences(prevValue, value);
+ }, [value, prevValue]);
+
+ if (isNotDefined(diff)) {
+ return (
+
+ {value}
+
+ );
+ }
+
+ return (
+
+ {diff.map((part, index) => {
+ const { added, removed, value: partValue } = part;
+
+ if (added) {
+ return (
+
+ {partValue}
+
+ );
+ }
+
+ if (removed) {
+ return (
+
+ {partValue}
+
+ );
+ }
+
+ return (
+ // eslint-disable-next-line react/no-array-index-key
+
+ {partValue}
+
+ );
+ })}
+
+ );
+}
+
+export default PrintableDescription;
diff --git a/app/src/components/printable/PrintableDescription/styles.module.css b/app/src/components/printable/PrintableDescription/styles.module.css
new file mode 100644
index 000000000..a52f405f0
--- /dev/null
+++ b/app/src/components/printable/PrintableDescription/styles.module.css
@@ -0,0 +1,18 @@
+.printable-description {
+ text-align: justify;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+
+ &.with-diff-view {
+ .added {
+ background-color: color-mix(in srgb, var(--go-ui-color-green) 10%, transparent);
+ color: var(--go-ui-color-green);
+ }
+
+ .removed {
+ background-color: color-mix(in srgb, var(--go-ui-color-red) 10%, transparent);
+ text-decoration: line-through;
+ color: var(--go-ui-color-red);
+ }
+ }
+}
diff --git a/app/src/utils/constants.ts b/app/src/utils/constants.ts
index a441b4ad4..08630617d 100644
--- a/app/src/utils/constants.ts
+++ b/app/src/utils/constants.ts
@@ -222,4 +222,3 @@ export const EAP_STATUS_TECHNICALLY_VALIDATED = 40 satisfies EapStatus;
export const EAP_STATUS_APPROVED = 50 satisfies EapStatus;
export const EAP_STATUS_PFA_SIGNED = 60 satisfies EapStatus;
export const EAP_STATUS_ACTIVATED = 70 satisfies EapStatus;
-
diff --git a/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json b/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json
index e4d9e1211..1404c3e35 100644
--- a/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json
+++ b/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json
@@ -1,8 +1,6 @@
{
"namespace":"accountMyFormsEap",
"strings":{
- "eapViewLabel": "View",
- "eapEditLabel": "Edit",
"eapStartFullLink": "Start Full EAP",
"eapStartSimplifiedLink": "Start sEAP",
"eapEditFullLink": "Edit Full EAP",
diff --git a/app/src/views/AccountMyFormsEap/i18n.json b/app/src/views/AccountMyFormsEap/i18n.json
index aa1d449b3..4fcb4dcfd 100644
--- a/app/src/views/AccountMyFormsEap/i18n.json
+++ b/app/src/views/AccountMyFormsEap/i18n.json
@@ -6,7 +6,6 @@
"eapLastUpdated": "Last Updated",
"eapName": "Name/Phase",
"eapType": "EAP Type",
- "eapStatus": "Status",
- "eapRegistration": "EAP Registration"
+ "eapStatus": "Status"
}
}
diff --git a/app/src/views/AccountMyFormsEap/index.tsx b/app/src/views/AccountMyFormsEap/index.tsx
index c049f13ef..ac6b69c4f 100644
--- a/app/src/views/AccountMyFormsEap/index.tsx
+++ b/app/src/views/AccountMyFormsEap/index.tsx
@@ -251,23 +251,23 @@ export function Component() {
const detailColumns = useMemo(
() => ([
- createExpansionIndicatorColumn(
+ createExpansionIndicatorColumn(
true,
(row) => !!row.disabled,
),
- createDateColumn(
+ createDateColumn(
'created_at',
strings.eapLastUpdated,
(row) => row.lastUpdated,
),
- createStringColumn(
+ createStringColumn(
'title',
'',
(row) => row.label,
{ withLightText: (item) => !!item.disabled },
),
- createEmptyColumn(),
- createElementColumn(
+ createEmptyColumn(),
+ createElementColumn(
'actions',
'',
EapTableActions,
@@ -275,7 +275,7 @@ export function Component() {
expandedListItem: row,
}),
),
- createEmptyColumn(),
+ createEmptyColumn(),
]),
[strings.eapLastUpdated],
);
diff --git a/app/src/views/SimplifiedEapExport/i18n.json b/app/src/views/SimplifiedEapExport/i18n.json
index 92ce6b17f..ab310c14a 100644
--- a/app/src/views/SimplifiedEapExport/i18n.json
+++ b/app/src/views/SimplifiedEapExport/i18n.json
@@ -1,6 +1,8 @@
{
"namespace": "simplifiedEapExport",
"strings": {
+ "pageTitleSimplifiedText": "Simplified",
+ "pageTitleEapText": "Early Action Protocol",
"sEapNoLabel": "sEAP No",
"totalBudgetLabel": "Total Budget",
"readinessLabel": "Readiness",
@@ -33,7 +35,6 @@
"conditionsToDeliverHeading": "Conditions to deliver the Early Action",
"earlyActionCapacityHeading": "Experience and/or capacity to implement the early actions",
"rcrcMovementInvolvementHeading": "RCRC Movement partners, Governmental/other agencies consulted/involved",
- "budgetHeading": "Budget",
"contactInformationHeading": "Contact information",
"contactInformationDescription": "For further information, specifically related to this simplified EAP please contact:"
}
diff --git a/app/src/views/SimplifiedEapExport/index.tsx b/app/src/views/SimplifiedEapExport/index.tsx
index 9b41ab752..a8cd4afb2 100644
--- a/app/src/views/SimplifiedEapExport/index.tsx
+++ b/app/src/views/SimplifiedEapExport/index.tsx
@@ -4,10 +4,7 @@ import {
} from 'react-router-dom';
import { Label } from '@ifrc-go/ui';
import { useTranslation } from '@ifrc-go/ui/hooks';
-import {
- DescriptionText,
- Image,
-} from '@ifrc-go/ui/printable';
+import { Image } from '@ifrc-go/ui/printable';
import {
isDefined,
isFalsyString,
@@ -18,6 +15,7 @@ import {
import PrintableContainer from '#components/printable/PrintableContainer';
import PrintableDataDisplay from '#components/printable/PrintableDataDisplay';
+import PrintableDescription from '#components/printable/PrintableDescription';
import PrintablePage from '#components/printable/PrintablePage';
import useGlobalEnums from '#hooks/domain/useGlobalEnums';
import { useRequest } from '#utils/restRequest';
@@ -30,7 +28,9 @@ import styles from './styles.module.css';
export function Component() {
const { eapId } = useParams<{ eapId: string }>();
const [searchParams] = useSearchParams();
+
const version = searchParams.get('version') ?? undefined;
+ const showDiff = searchParams.get('diff') ?? undefined;
const strings = useTranslation(i18n);
@@ -45,18 +45,45 @@ export function Component() {
} : undefined,
});
- const latestSimplifiedEap = eapRegistrationResponse?.simplified_eap_details?.find(
+ const selectedSimplifiedEap = eapRegistrationResponse?.simplified_eap_details?.find(
(simplifiedEap) => String(simplifiedEap.version) === String(version),
);
+ const latestSimplifiedEapVersion = eapRegistrationResponse?.latest_simplified_eap;
+ const latestSimplifiedEap = eapRegistrationResponse?.simplified_eap_details?.find(
+ (simplifiedEap) => simplifiedEap.version === latestSimplifiedEapVersion,
+ );
+
+ const currentSimplifiedEap = selectedSimplifiedEap ?? latestSimplifiedEap;
+ const currentSimplifiedEapId = currentSimplifiedEap?.id;
+
+ const prevSimplifiedEapVersion = isDefined(currentSimplifiedEap?.version)
+ && currentSimplifiedEap.version > 1
+ ? currentSimplifiedEap.version - 1
+ : undefined;
+ const prevSimplifiedEap = eapRegistrationResponse?.simplified_eap_details.find(
+ (simplifiedEap) => simplifiedEap.version === prevSimplifiedEapVersion,
+ );
+
const {
pending: simplifiedEapPending,
response: simplifiedEapResponse,
} = useRequest({
- skip: isNotDefined(latestSimplifiedEap?.id),
+ skip: isNotDefined(currentSimplifiedEapId),
url: '/api/v2/simplified-eap/{id}/',
- pathVariables: isDefined(latestSimplifiedEap?.id) ? {
- id: Number(latestSimplifiedEap.id),
+ pathVariables: isDefined(currentSimplifiedEapId) ? {
+ id: Number(currentSimplifiedEapId),
+ } : undefined,
+ });
+
+ const {
+ pending: prevSimplifiedEapPending,
+ response: prevSimplifiedEapResponse,
+ } = useRequest({
+ skip: isNotDefined(prevSimplifiedEap) || showDiff?.toLowerCase() !== 'true',
+ url: '/api/v2/simplified-eap/{id}/',
+ pathVariables: isDefined(prevSimplifiedEap) ? {
+ id: Number(prevSimplifiedEap.id),
} : undefined,
});
@@ -113,21 +140,36 @@ export function Component() {
national_society_contact_phone_number,
} = simplifiedEapResponse ?? {};
+ const {
+ prioritized_hazard_and_impact: prev_prioritized_hazard_and_impact,
+ risks_selected_protocols: prev_risks_selected_protocols,
+ overall_objective_intervention: prev_overall_objective_intervention,
+ potential_geographical_high_risk_areas: prev_potential_geographical_high_risk_areas,
+ assisted_through_operation: prev_assisted_through_operation,
+ trigger_statement: prev_trigger_statement,
+ trigger_threshold_justification: prev_trigger_threshold_justification,
+ next_step_towards_full_eap: prev_next_step_towards_full_eap,
+ early_action_capability: prev_early_action_capability,
+ rcrc_movement_involvement: prev_rcrc_movement_involvement,
+ } = prevSimplifiedEapResponse ?? {};
+
const eapTitle = [
country_details?.name,
admin2_details?.map(({ name }) => name).join(', '),
disaster_type_details?.name,
].filter(isTruthyString).join(' | ');
- const previewReady = !eapRegistrationPending && !simplifiedEapPending;
+ const previewReady = !eapRegistrationPending
+ && !simplifiedEapPending
+ && !prevSimplifiedEapPending;
return (
- Simplified
+ {strings.pageTitleSimplifiedText}
- Early Action Protocol
+ {strings.pageTitleEapText}
>
)}
description={eapTitle ?? '--'}
@@ -235,17 +277,19 @@ export function Component() {
heading={strings.prioritizedHazardAndImpactHeading}
headingLevel={3}
>
-
- {prioritized_hazard_and_impact}
-
+
-
- {risks_selected_protocols}
-
+
-
- {overall_objective_intervention}
-
+
-
- {potential_geographical_high_risk_areas}
-
+
-
- {assisted_through_operation}
-
+
-
- {trigger_statement}
-
+
-
- {trigger_threshold_justification}
-
+
-
- {next_step_towards_full_eap}
-
+
-
- {early_action_capability}
-
+
-
- {rcrc_movement_involvement}
-
+
+ {/*
-
- Budget details
-
+
+ */}
|