From 956bf8d320c7aa45a77ae2b1f6511b2d536759d4 Mon Sep 17 00:00:00 2001 From: dbarber-cps Date: Thu, 25 Jun 2026 15:23:07 +0100 Subject: [PATCH 01/10] bugfix temp removal of e2e tests (#411) --- materials_devops_pipelines/Materials-PR.yml | 105 ++++++++++---------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/materials_devops_pipelines/Materials-PR.yml b/materials_devops_pipelines/Materials-PR.yml index 06367779..52753b40 100644 --- a/materials_devops_pipelines/Materials-PR.yml +++ b/materials_devops_pipelines/Materials-PR.yml @@ -211,59 +211,60 @@ stages: # ---------- E2E TESTS (Playwright) ---------- - - script: | - npx playwright install - displayName: 'Install Playwright browsers' - condition: ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true') - workingDirectory: '$(workingDir)' - env: - PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' +#temp removal of E2E tests until pipeline issues resolved + # - script: | + # npx playwright install + # displayName: 'Install Playwright browsers' + # condition: ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true') + # workingDirectory: '$(workingDir)' + # env: + # PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' - - script: | - npx playwright install-deps - displayName: 'Install Playwright OS dependencies' - condition: and(eq(variables['Agent.OS'], 'Linux'), ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true')) - workingDirectory: '$(workingDir)' - env: - PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' + # - script: | + # npx playwright install-deps + # displayName: 'Install Playwright OS dependencies' + # condition: and(eq(variables['Agent.OS'], 'Linux'), ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true')) + # workingDirectory: '$(workingDir)' + # env: + # PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' - - task: Npm@1 - displayName: 'Run E2E Tests with Coverage' - inputs: - command: 'custom' - customCommand: 'run e2e' - workingDir: '$(workingDir)' - env: - PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' - E2E_TEST_MS_USERNAME: $(e2e_username) - E2E_TEST_MS_PASSWORD: $(e2e_password) - E2E_CIN3_USERNAME: $(e2e_cin3_username) - E2E_CIN3_PASSWORD: $(e2e_cin3_password) - E2E_CMS_COOKIE_URL: $(e2e_cms_cookie_url) - E2E_URN: $(e2e_urn) - E2E_CASE: $(e2e_case) - VITE_MSAL_CLIENT_ID: $(MSAL_CLIENT_ID) - VITE_MSAL_TENANT_ID: $(MSAL_TENANT_ID) - VITE_MSAL_REDIRECT_URI: $(MSAL_REDIRECT_URI_LOCAL) - VITE_POLARIS_GATEWAY_URL: $(POLARIS_GATEWAY_URL) - VITE_POLARIS_GATEWAY_SCOPE: $(POLARIS_GATEWAY_SCOPE) - VITE_GLOBAL_SCRIPT_URL: $(GLOBAL_SCRIPT_URL) - VITE_BASE_URL: $(BASE_URL) - VITE_E2E: True + # - task: Npm@1 + # displayName: 'Run E2E Tests with Coverage' + # inputs: + # command: 'custom' + # customCommand: 'run e2e' + # workingDir: '$(workingDir)' + # env: + # PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' + # E2E_TEST_MS_USERNAME: $(e2e_username) + # E2E_TEST_MS_PASSWORD: $(e2e_password) + # E2E_CIN3_USERNAME: $(e2e_cin3_username) + # E2E_CIN3_PASSWORD: $(e2e_cin3_password) + # E2E_CMS_COOKIE_URL: $(e2e_cms_cookie_url) + # E2E_URN: $(e2e_urn) + # E2E_CASE: $(e2e_case) + # VITE_MSAL_CLIENT_ID: $(MSAL_CLIENT_ID) + # VITE_MSAL_TENANT_ID: $(MSAL_TENANT_ID) + # VITE_MSAL_REDIRECT_URI: $(MSAL_REDIRECT_URI_LOCAL) + # VITE_POLARIS_GATEWAY_URL: $(POLARIS_GATEWAY_URL) + # VITE_POLARIS_GATEWAY_SCOPE: $(POLARIS_GATEWAY_SCOPE) + # VITE_GLOBAL_SCRIPT_URL: $(GLOBAL_SCRIPT_URL) + # VITE_BASE_URL: $(BASE_URL) + # VITE_E2E: True - - task: PublishPipelineArtifact@1 - displayName: 'Publish E2E Artifact' - inputs: - targetPath: '$(workingDir)/tests/playwright-report' - artifact: 'playwright-report' - publishLocation: 'pipeline' - condition: succeededOrFailed() + # - task: PublishPipelineArtifact@1 + # displayName: 'Publish E2E Artifact' + # inputs: + # targetPath: '$(workingDir)/tests/playwright-report' + # artifact: 'playwright-report' + # publishLocation: 'pipeline' + # condition: succeededOrFailed() - - task: PublishTestResults@2 - displayName: 'Publish E2E Test Results (JUnit)' - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '$(workingDir)/tests/e2e-test-results.xml' - testRunTitle: 'E2E Tests' - publishRunAttachments: false - condition: always() \ No newline at end of file + # - task: PublishTestResults@2 + # displayName: 'Publish E2E Test Results (JUnit)' + # inputs: + # testResultsFormat: 'JUnit' + # testResultsFiles: '$(workingDir)/tests/e2e-test-results.xml' + # testRunTitle: 'E2E Tests' + # publishRunAttachments: false + # condition: always() \ No newline at end of file From 15c8315df64a53b09f3066961699c37a1827b8f6 Mon Sep 17 00:00:00 2001 From: kramduny-cps Date: Thu, 25 Jun 2026 15:26:14 +0100 Subject: [PATCH 02/10] Sync Dev branch with Main (#409) * FCT2-19669 move doc buttons (#407) * move doc buttons * fix doc refetch on exiting full width mode * updating branch triggers for new strategy (#405) * updating branch triggers for new strategy * updated stage Determine_Changes to support multiple branches * adding checkout self * removed main from PR trigger --------- Co-authored-by: benrome-cps Co-authored-by: dbarber-cps --- materials_devops_pipelines/Materials-PR.yml | 30 ++++++- .../Materials-UI-Build-Prod.yml | 2 +- .../Materials-UI-Build-Staging.yml | 2 +- .../Materials-UI-Release-Prod.yml | 2 +- .../Materials-UI-Release-Staging.yml | 2 +- .../dropDownButton/DropdownButton.tsx | 1 - .../src/caseWorkApp/components/tabs/Tabs.tsx | 42 ++++++--- .../ReviewAndRedactPage.tsx | 42 ++++++--- .../DocumentTabPanel/DocumentTabPanel.tsx | 12 +-- .../DocumentActionsDropdown.tsx | 89 +++++++++++++++++++ .../documenViewportArea/index.tsx | 78 +--------------- 11 files changed, 186 insertions(+), 116 deletions(-) create mode 100644 materials_ui/src/materials_components/documenViewportArea/DocumentActionsDropdown.tsx diff --git a/materials_devops_pipelines/Materials-PR.yml b/materials_devops_pipelines/Materials-PR.yml index 52753b40..59bf53dc 100644 --- a/materials_devops_pipelines/Materials-PR.yml +++ b/materials_devops_pipelines/Materials-PR.yml @@ -6,7 +6,8 @@ trigger: none pr: branches: include: - - main + - development + paths: include: - terraform/* @@ -59,13 +60,34 @@ stages: pool: name: $(dev-build-agent) steps: + - checkout: self + fetchDepth: 0 + - task: PowerShell@2 inputs: targetType: 'inline' script: | - git fetch origin main - $files = $(git diff --name-only origin/main) - $temp=$files -split ' ' + $targetBranch = $env:SYSTEM_PULLREQUEST_TARGETBRANCH + $targetBranchOverride = $env:TARGETBRANCHOVERRIDE + + # Optional override to support manual runs. + if ([string]::IsNullOrWhiteSpace($targetBranch) -and -not [string]::IsNullOrWhiteSpace($targetBranchOverride)) + { + $targetBranch = $targetBranchOverride + } + + if ([string]::IsNullOrWhiteSpace($targetBranch)) + { + # Default for non-PR runs when no override is provided. + $targetBranch = "refs/heads/development" + } + + $targetBranchName = $targetBranch -replace '^refs/heads/', '' + echo "Comparing changes against origin/$targetBranchName" + + git fetch origin "+refs/heads/${targetBranchName}:refs/remotes/origin/${targetBranchName}" --depth=200 + + $temp = @(git diff --name-only "origin/$targetBranchName...HEAD" | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) $count=$temp.Length echo "Total changed $count files" diff --git a/materials_devops_pipelines/Materials-UI-Build-Prod.yml b/materials_devops_pipelines/Materials-UI-Build-Prod.yml index d02b74e6..305e6f19 100644 --- a/materials_devops_pipelines/Materials-UI-Build-Prod.yml +++ b/materials_devops_pipelines/Materials-UI-Build-Prod.yml @@ -5,7 +5,7 @@ trigger: batch: true branches: include: - - development ## TBC once branching confirmed + - main paths: include: - materials_ui diff --git a/materials_devops_pipelines/Materials-UI-Build-Staging.yml b/materials_devops_pipelines/Materials-UI-Build-Staging.yml index 8621c25c..f4a63349 100644 --- a/materials_devops_pipelines/Materials-UI-Build-Staging.yml +++ b/materials_devops_pipelines/Materials-UI-Build-Staging.yml @@ -5,7 +5,7 @@ trigger: batch: true branches: include: - - development ## TBC once branching confirmed + - main paths: include: - materials_ui diff --git a/materials_devops_pipelines/Materials-UI-Release-Prod.yml b/materials_devops_pipelines/Materials-UI-Release-Prod.yml index 3709b443..65f9760f 100644 --- a/materials_devops_pipelines/Materials-UI-Release-Prod.yml +++ b/materials_devops_pipelines/Materials-UI-Release-Prod.yml @@ -9,7 +9,7 @@ resources: trigger: branches: include: - - refs/heads/development + - refs/heads/main stages: - Publish_Artifacts diff --git a/materials_devops_pipelines/Materials-UI-Release-Staging.yml b/materials_devops_pipelines/Materials-UI-Release-Staging.yml index 4aeedabb..fcfd7b33 100644 --- a/materials_devops_pipelines/Materials-UI-Release-Staging.yml +++ b/materials_devops_pipelines/Materials-UI-Release-Staging.yml @@ -9,7 +9,7 @@ resources: trigger: branches: include: - - refs/heads/development + - refs/heads/main stages: - Publish_Artifacts diff --git a/materials_ui/src/caseWorkApp/components/dropDownButton/DropdownButton.tsx b/materials_ui/src/caseWorkApp/components/dropDownButton/DropdownButton.tsx index 7caa5796..dc3b86e7 100644 --- a/materials_ui/src/caseWorkApp/components/dropDownButton/DropdownButton.tsx +++ b/materials_ui/src/caseWorkApp/components/dropDownButton/DropdownButton.tsx @@ -84,7 +84,6 @@ export const DropdownButton2 = (p: { + + + )} - )} + {documentActions} +
= ({ alignItems: 'start' }} > -
- -
-
({ diff --git a/materials_ui/src/caseWorkApp/pages/ReviewAndRedactPage/ReviewAndRedactPage.tsx b/materials_ui/src/caseWorkApp/pages/ReviewAndRedactPage/ReviewAndRedactPage.tsx index 930da37a..a13b814c 100644 --- a/materials_ui/src/caseWorkApp/pages/ReviewAndRedactPage/ReviewAndRedactPage.tsx +++ b/materials_ui/src/caseWorkApp/pages/ReviewAndRedactPage/ReviewAndRedactPage.tsx @@ -21,6 +21,7 @@ import { DocSearchContext, DocumentTabPanel } from '../../../materials_components/DocumentTabPanel/DocumentTabPanel'; +import { DocumentActionsDropdown } from '../../../materials_components/documenViewportArea/DocumentActionsDropdown'; import { TRedaction } from '../../../materials_components/PdfRedactor/utils/coordUtils'; import { isRedactionEnabledMode, @@ -87,6 +88,9 @@ export const ReviewAndRedactPage = () => { const [modeByParentId, setModeByParentId] = useState>( {} ); + const [numOfPagesByParentId, setNumOfPagesByParentId] = useState< + Record + >({}); const [searchModalOpen, setSearchModalOpen] = useState(false); @@ -229,15 +233,12 @@ export const ReviewAndRedactPage = () => { reloadSidebarTrigger.fire(); }} initRedactions={redactionsIndexedOnParentId[doc.parentId]} - onViewInNewWindowClick={() => { - if (!urn || !caseId) return; - navigateToViewDocumentPageInNewTab({ - urn, - caseId, - materialId: doc.parentId - }); - }} - onRedactionLogClick={() => setShowRedactionLogModal(true)} + onNumOfPagesChange={(numOfPages) => + setNumOfPagesByParentId((prev) => ({ + ...prev, + [doc.parentId]: numOfPages + })) + } searchContext={searchContextByParentId[doc.parentId]} onFocusedSearchIndexChange={(index) => setFocusedSearchIndex(doc.parentId, index) @@ -290,6 +291,7 @@ export const ReviewAndRedactPage = () => { setOpenParentIds([]); setSearchContextByParentId({}); setRedactionsIndexedOnParentId({}); + setNumOfPagesByParentId({}); const sidebarFocusTarget = document.querySelector( @@ -475,7 +477,9 @@ export const ReviewAndRedactPage = () => { onSetDocumentOpenIds={setOpenParentIds} onDocumentClick={requestActiveTabChange} reloadTriggerData={reloadSidebarTrigger.data} - onDocumentsChange={setDocuments} + onDocumentsChange={(docs) => { + if (docs !== undefined) setDocuments(docs); + }} /> ) : undefined @@ -491,6 +495,24 @@ export const ReviewAndRedactPage = () => { noMargin onShowHideCategoriesClick={() => setIsSidebarVisible((v) => !v)} isShowCategories={isSidebarVisible} + documentActions={ + + handleModeChange(activeTabId, newMode) + } + onRedactionLogClick={() => setShowRedactionLogModal(true)} + onViewInNewWindowClick={() => { + if (!urn || !caseId) return; + navigateToViewDocumentPageInNewTab({ + urn, + caseId, + materialId: activeTabId + }); + }} + numOfDocumentPages={numOfPagesByParentId[activeTabId] ?? 0} + /> + } /> )} diff --git a/materials_ui/src/materials_components/DocumentTabPanel/DocumentTabPanel.tsx b/materials_ui/src/materials_components/DocumentTabPanel/DocumentTabPanel.tsx index fc58950c..dad3c6f1 100644 --- a/materials_ui/src/materials_components/DocumentTabPanel/DocumentTabPanel.tsx +++ b/materials_ui/src/materials_components/DocumentTabPanel/DocumentTabPanel.tsx @@ -42,9 +42,8 @@ export type DocumentTabPanelProps = { onModeChange: (mode: TMode) => void; onRedactionsChange: (redactions: TRedaction[]) => void; onModification: (x: TDocument) => void; - onViewInNewWindowClick: () => void; initRedactions?: TRedaction[]; - onRedactionLogClick: () => void; + onNumOfPagesChange: (numOfPages: number) => void; searchContext?: DocSearchContext; onFocusedSearchIndexChange?: (index: number) => void; onBackToSearchResults?: () => void; @@ -62,8 +61,7 @@ export const DocumentTabPanel = ({ onRedactionsChange, onModification, initRedactions, - onViewInNewWindowClick, - onRedactionLogClick, + onNumOfPagesChange, searchContext, onFocusedSearchIndexChange, onBackToSearchResults, @@ -121,7 +119,6 @@ export const DocumentTabPanel = ({ } }; }, [parentId, childId, urn, caseId]); - const [numOfDocumentPages, setNumOfDocumentPages] = useState(0); useEffect(() => { const loadLookups = async () => { @@ -178,9 +175,6 @@ export const DocumentTabPanel = ({ documentName={document.presentationTitle} mode={mode} onModeChange={onModeChange} - onViewInNewWindowButtonClick={onViewInNewWindowClick} - onRedactionLogClick={onRedactionLogClick} - numOfDocumentPages={numOfDocumentPages} searchMode={ searchContext ? { @@ -220,7 +214,7 @@ export const DocumentTabPanel = ({ setShowRedactionLogModal(true); }} onRedactionSaveStatusChange={setRedactionSaveStatus} - onNumOfPagesDocumentChange={(x) => setNumOfDocumentPages(x)} + onNumOfPagesDocumentChange={onNumOfPagesChange} searchHighlights={searchContext?.highlights} focusedSearchIndex={searchContext?.focusedIndex} checkInDocumentTriggerData={checkInDocumentTriggerData} diff --git a/materials_ui/src/materials_components/documenViewportArea/DocumentActionsDropdown.tsx b/materials_ui/src/materials_components/documenViewportArea/DocumentActionsDropdown.tsx new file mode 100644 index 00000000..798ab197 --- /dev/null +++ b/materials_ui/src/materials_components/documenViewportArea/DocumentActionsDropdown.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react'; +import { + DropdownButton2, + DropdownListItem +} from '../../caseWorkApp/components/dropDownButton/DropdownButton'; +import { TMode } from '../PdfRedactor/utils/modeUtils'; + +const DROPDOWN_ACTIONS = { + LOG_REDACTION: 'log-redaction', + ROTATE: 'rotate', + DELETE: 'delete', + VIEW_NEW_WINDOW: 'view-new-window' +} as const; + +type DocumentActionsDropdownProps = { + mode: TMode; + onModeChange: (mode: TMode) => void; + onRedactionLogClick: () => void; + onViewInNewWindowClick: () => void; + numOfDocumentPages: number; +}; + +export const DocumentActionsDropdown = ({ + mode, + onModeChange, + onRedactionLogClick, + onViewInNewWindowClick, + numOfDocumentPages +}: DocumentActionsDropdownProps) => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + return ( + Document actions} + isOpen={isDropdownOpen} + setIsOpen={(x) => setIsDropdownOpen(x)} + > +
+ { + onRedactionLogClick(); + setIsDropdownOpen(false); + }} + borderBottom + > + Log an Under/Over redaction + + { + onModeChange(mode === 'rotation' ? 'disabled' : 'rotation'); + setIsDropdownOpen(false); + }} + > + {mode === 'rotation' + ? 'Hide rotate document pages' + : 'Rotate document pages'} + + {numOfDocumentPages > 1 && ( + { + onModeChange(mode === 'deletion' ? 'disabled' : 'deletion'); + setIsDropdownOpen(false); + }} + > + {mode === 'deletion' + ? 'Hide delete page options' + : 'Show delete page options'} + + )} + { + onViewInNewWindowClick(); + setIsDropdownOpen(false); + }} + > + View in new window + +
+
+ ); +}; diff --git a/materials_ui/src/materials_components/documenViewportArea/index.tsx b/materials_ui/src/materials_components/documenViewportArea/index.tsx index ad4c4492..8bdcfd9e 100644 --- a/materials_ui/src/materials_components/documenViewportArea/index.tsx +++ b/materials_ui/src/materials_components/documenViewportArea/index.tsx @@ -1,17 +1,6 @@ -import { CSSProperties, useState } from 'react'; -import { - DropdownButton2, - DropdownListItem -} from '../../caseWorkApp/components/dropDownButton/DropdownButton'; +import { CSSProperties } from 'react'; import { isRedactionEnabledMode, TMode } from '../PdfRedactor/utils/modeUtils'; -const DROPDOWN_ACTIONS = { - LOG_REDACTION: 'log-redaction', - ROTATE: 'rotate', - DELETE: 'delete', - VIEW_NEW_WINDOW: 'view-new-window' -} as const; - const linkButtonStyle: CSSProperties = { margin: '0.125rem', display: 'inline', @@ -38,9 +27,6 @@ type DocumentViewportAreaProps = { documentName: string; mode: TMode; onModeChange: (mode: TMode) => void; - onViewInNewWindowButtonClick: () => void; - onRedactionLogClick: () => void; - numOfDocumentPages: number; searchMode?: SearchModeProps; }; @@ -48,12 +34,8 @@ export const DocumentViewportArea = ({ documentName, mode, onModeChange, - onViewInNewWindowButtonClick, - onRedactionLogClick, - numOfDocumentPages, searchMode }: DocumentViewportAreaProps) => { - const [isDropdownOpen, setIsDropdownOpen] = useState(false); return (
{searchMode ? ( @@ -159,61 +142,6 @@ export const DocumentViewportArea = ({ ? 'Stop redacting' : 'Start redacting'} - Document Actions} - isOpen={isDropdownOpen} - setIsOpen={(x) => setIsDropdownOpen(x)} - > -
- { - onRedactionLogClick(); - setIsDropdownOpen(false); - }} - borderBottom - > - Log an Under/Over redaction - - { - onModeChange(mode === 'rotation' ? 'disabled' : 'rotation'); - setIsDropdownOpen(false); - }} - > - {mode === 'rotation' - ? 'Hide rotate document pages' - : 'Rotate document pages'} - - {numOfDocumentPages > 1 && ( - { - onModeChange(mode === 'deletion' ? 'disabled' : 'deletion'); - setIsDropdownOpen(false); - }} - > - {mode === 'deletion' - ? 'Hide delete page options' - : 'Show delete page options'} - - )} - { - onViewInNewWindowButtonClick(); - setIsDropdownOpen(false); - }} - > - View in new window - -
-
From 814e0efece77105af59b012ee92d3d4329624175 Mon Sep 17 00:00:00 2001 From: afeescps Date: Thu, 25 Jun 2026 15:47:26 +0100 Subject: [PATCH 03/10] fix(materials): only show update option for exhibits and statements (#408) --- materials_ui/src/pages/Materials.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/materials_ui/src/pages/Materials.tsx b/materials_ui/src/pages/Materials.tsx index 1ee62e82..b8612c0e 100644 --- a/materials_ui/src/pages/Materials.tsx +++ b/materials_ui/src/pages/Materials.tsx @@ -138,11 +138,11 @@ export const MaterialsPage = () => { onClick: () => handleEditClick(row as CaseMaterialsType, getRoute('MATERIALS')), hide: (() => { - const itemCommsCategory = selectedItems.communications[0]?.category; - if (!itemCommsCategory) return; + const itemMaterialsCategory = selectedItems.materials[0]?.category; + if (!itemMaterialsCategory) return; return ( - selectedItems.communications.length > 1 || - !['Exhibit', 'Statement'].includes(itemCommsCategory) + selectedItems.materials.length > 1 || + !['Exhibit', 'Statement'].includes(itemMaterialsCategory) ); })() }, From c1c11e47ee9282108f76817619df012a5683fd9c Mon Sep 17 00:00:00 2001 From: benrome-cps Date: Fri, 26 Jun 2026 13:42:17 +0100 Subject: [PATCH 04/10] FCT2-19973 implement polling for bulk search (#412) --- .../hooks/useBulkSearch.ts | 68 ++++++++++++++----- .../utils/bulkSearchDocumentUtils.ts | 17 +++-- 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/materials_ui/src/materials_components/CaseworkPdfRedactorWrapper/hooks/useBulkSearch.ts b/materials_ui/src/materials_components/CaseworkPdfRedactorWrapper/hooks/useBulkSearch.ts index b6c9f2bb..fa8ff839 100644 --- a/materials_ui/src/materials_components/CaseworkPdfRedactorWrapper/hooks/useBulkSearch.ts +++ b/materials_ui/src/materials_components/CaseworkPdfRedactorWrapper/hooks/useBulkSearch.ts @@ -10,6 +10,22 @@ export type TBulkSearchInternalState = | { status: 'done'; candidates: TRedaction[]; focusedIndex: number } | { status: 'error' }; +const POLL_INTERVAL_MS = 3000; +const MAX_SEARCH_ATTEMPTS = 5; + +const wait = (ms: number, signal: AbortSignal) => + new Promise((resolve) => { + const timeoutId = setTimeout(resolve, ms); + signal.addEventListener( + 'abort', + () => { + clearTimeout(timeoutId); + resolve(); + }, + { once: true } + ); + }); + export const useBulkSearch = (p: { axiosInstance: AxiosInstance; urn: string; @@ -34,24 +50,44 @@ export const useBulkSearch = (p: { const controller = new AbortController(); abortRef.current = controller; setState({ status: 'loading' }); + try { - const resp = await bulkSearchDocument({ - axiosInstance: p.axiosInstance, - urn: p.urn, - caseId: p.caseId, - versionId: p.versionId, - documentId: p.documentId, - searchText, - signal: controller.signal - }); - if (controller.signal.aborted) return undefined; - if (resp.isNotFound || resp.failedReason) { - setState({ status: 'error' }); - return undefined; + for (let attempt = 1; attempt <= MAX_SEARCH_ATTEMPTS; attempt++) { + const { status, data } = await bulkSearchDocument({ + axiosInstance: p.axiosInstance, + urn: p.urn, + caseId: p.caseId, + versionId: p.versionId, + documentId: p.documentId, + searchText, + signal: controller.signal + }); + if (controller.signal.aborted) return undefined; + + if (status === 200) { + if (!data || data.isNotFound || data.failedReason) { + setState({ status: 'error' }); + return undefined; + } + const candidates = convertSearchResponseToRedactions(data); + setState({ status: 'done', candidates, focusedIndex: 0 }); + return candidates; + } + + const stillProcessing = status === 202 || status === 423; + if (!stillProcessing) { + setState({ status: 'error' }); + return undefined; + } + + if (attempt < MAX_SEARCH_ATTEMPTS) { + await wait(POLL_INTERVAL_MS, controller.signal); + if (controller.signal.aborted) return undefined; + } } - const candidates = convertSearchResponseToRedactions(resp); - setState({ status: 'done', candidates, focusedIndex: 0 }); - return candidates; + + setState({ status: 'error' }); + return undefined; } catch (err) { if (axios.isCancel(err)) return undefined; setState({ status: 'error' }); diff --git a/materials_ui/src/materials_components/CaseworkPdfRedactorWrapper/utils/bulkSearchDocumentUtils.ts b/materials_ui/src/materials_components/CaseworkPdfRedactorWrapper/utils/bulkSearchDocumentUtils.ts index 6383b60f..94d1faf3 100644 --- a/materials_ui/src/materials_components/CaseworkPdfRedactorWrapper/utils/bulkSearchDocumentUtils.ts +++ b/materials_ui/src/materials_components/CaseworkPdfRedactorWrapper/utils/bulkSearchDocumentUtils.ts @@ -19,6 +19,11 @@ export type TBulkSearchResponse = { isNotFound: boolean; }; +export type TBulkSearchResult = { + status: number; + data: TBulkSearchResponse | null; +}; + export const bulkSearchDocument = async (p: { axiosInstance: AxiosInstance; urn: string; @@ -27,10 +32,14 @@ export const bulkSearchDocument = async (p: { documentId: string; searchText: string; signal?: AbortSignal; -}): Promise => { - const response = await p.axiosInstance.get( +}): Promise => { + const response = await p.axiosInstance.get( `/api/urns/${p.urn}/cases/${p.caseId}/documents/${p.documentId}/versions/${p.versionId}/search`, - { params: { SearchText: p.searchText }, signal: p.signal } + { + params: { SearchText: p.searchText }, + signal: p.signal, + validateStatus: () => true + } ); - return response.data; + return { status: response.status, data: response.data ?? null }; }; From dc78d06f8586daabe0add86ab3e3a25ca3642592 Mon Sep 17 00:00:00 2001 From: TahmeedAhmed-NTT-CPS Date: Mon, 29 Jun 2026 08:34:45 +0000 Subject: [PATCH 05/10] display dac link for single defendant (#399) --- materials_ui/src/components/CaseInfo/CaseInfo.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/materials_ui/src/components/CaseInfo/CaseInfo.tsx b/materials_ui/src/components/CaseInfo/CaseInfo.tsx index e9f4bbe7..1578360b 100644 --- a/materials_ui/src/components/CaseInfo/CaseInfo.tsx +++ b/materials_ui/src/components/CaseInfo/CaseInfo.tsx @@ -53,10 +53,12 @@ export const CaseInfo = ({ caseInfo }: Props) => { {caseInfoName}

{caseInfo?.urn}

- {caseInfo.numberOfDefendants > 1 && ( + {caseInfo.numberOfDefendants > 0 && (

- View {caseInfo.numberOfDefendants} defendants and charges + {caseInfo.numberOfDefendants === 1 + ? 'View defendant and charges' + : `View ${caseInfo.numberOfDefendants} defendants and charges`}

)} From 9db3f92b268ee580cf7e99c7c69ccc3e65bfd833 Mon Sep 17 00:00:00 2001 From: abdul-cps Date: Wed, 1 Jul 2026 13:53:24 +0100 Subject: [PATCH 06/10] e2e tests debug (#414) * e2e tests debug * change redirect url * change wait for ms auth page * increase timeout * change to baseurl * test qa proxy url * add screenshot after getting cooki * increase timeout * removing VITE_BASE_URL from pipeline as this is not needed * Fct2 e2e pipeline fix db (#420) * e2e changes for testing * further e2e testing * updating to use Playwright-managed Chromium in CI. * reverting previous change * updating pipeline to install msedge --------- Co-authored-by: abdul-cps Co-authored-by: Daniel Barber --- materials_devops_pipelines/Materials-PR.yml | 113 +++++++++--------- materials_ui/playwright.config.ts | 13 +- .../tests/global-setup/global.setup.ts | 16 ++- .../tests/tests-e2e/materials.spec.ts | 31 +---- 4 files changed, 85 insertions(+), 88 deletions(-) diff --git a/materials_devops_pipelines/Materials-PR.yml b/materials_devops_pipelines/Materials-PR.yml index 59bf53dc..5b2a5072 100644 --- a/materials_devops_pipelines/Materials-PR.yml +++ b/materials_devops_pipelines/Materials-PR.yml @@ -226,67 +226,72 @@ stages: VITE_POLARIS_GATEWAY_URL: $(POLARIS_GATEWAY_URL) VITE_POLARIS_GATEWAY_SCOPE: $(POLARIS_GATEWAY_SCOPE) VITE_GLOBAL_SCRIPT_URL: $(GLOBAL_SCRIPT_URL) - VITE_BASE_URL: $(BASE_URL) VITE_E2E: True VITE_REDACTION_LOG_URL: $(VITE_REDACTION_LOG_URL) VITE_REDACTION_LOG_SCOPE: $(VITE_REDACTION_LOG_SCOPE) # ---------- E2E TESTS (Playwright) ---------- -#temp removal of E2E tests until pipeline issues resolved - # - script: | - # npx playwright install - # displayName: 'Install Playwright browsers' - # condition: ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true') - # workingDirectory: '$(workingDir)' - # env: - # PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' + - script: | + npx playwright install + displayName: 'Install Playwright browsers' + condition: ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true') + workingDirectory: '$(workingDir)' + env: + PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' - # - script: | - # npx playwright install-deps - # displayName: 'Install Playwright OS dependencies' - # condition: and(eq(variables['Agent.OS'], 'Linux'), ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true')) - # workingDirectory: '$(workingDir)' - # env: - # PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' + - script: | + npx playwright install-deps + displayName: 'Install Playwright OS dependencies' + condition: and(eq(variables['Agent.OS'], 'Linux'), ne(variables['PLAYWRIGHT_CACHE_RESTORED'], 'true')) + workingDirectory: '$(workingDir)' + env: + PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' - # - task: Npm@1 - # displayName: 'Run E2E Tests with Coverage' - # inputs: - # command: 'custom' - # customCommand: 'run e2e' - # workingDir: '$(workingDir)' - # env: - # PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' - # E2E_TEST_MS_USERNAME: $(e2e_username) - # E2E_TEST_MS_PASSWORD: $(e2e_password) - # E2E_CIN3_USERNAME: $(e2e_cin3_username) - # E2E_CIN3_PASSWORD: $(e2e_cin3_password) - # E2E_CMS_COOKIE_URL: $(e2e_cms_cookie_url) - # E2E_URN: $(e2e_urn) - # E2E_CASE: $(e2e_case) - # VITE_MSAL_CLIENT_ID: $(MSAL_CLIENT_ID) - # VITE_MSAL_TENANT_ID: $(MSAL_TENANT_ID) - # VITE_MSAL_REDIRECT_URI: $(MSAL_REDIRECT_URI_LOCAL) - # VITE_POLARIS_GATEWAY_URL: $(POLARIS_GATEWAY_URL) - # VITE_POLARIS_GATEWAY_SCOPE: $(POLARIS_GATEWAY_SCOPE) - # VITE_GLOBAL_SCRIPT_URL: $(GLOBAL_SCRIPT_URL) - # VITE_BASE_URL: $(BASE_URL) - # VITE_E2E: True + - script: | + npx playwright install msedge + displayName: 'Install Microsoft Edge (Playwright channel browser)' + condition: eq(variables['Agent.OS'], 'Linux') + workingDirectory: '$(workingDir)' + env: + PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' - # - task: PublishPipelineArtifact@1 - # displayName: 'Publish E2E Artifact' - # inputs: - # targetPath: '$(workingDir)/tests/playwright-report' - # artifact: 'playwright-report' - # publishLocation: 'pipeline' - # condition: succeededOrFailed() + - task: Npm@1 + displayName: 'Run E2E Tests with Coverage' + inputs: + command: 'custom' + customCommand: 'run e2e' + workingDir: '$(workingDir)' + env: + PLAYWRIGHT_BROWSERS_PATH: '$(Pipeline.Workspace)/.playwright' + E2E_TEST_MS_USERNAME: $(e2e_username) + E2E_TEST_MS_PASSWORD: $(e2e_password) + E2E_CIN3_USERNAME: $(e2e_cin3_username) + E2E_CIN3_PASSWORD: $(e2e_cin3_password) + E2E_CMS_COOKIE_URL: $(e2e_cms_cookie_url) + E2E_URN: $(e2e_urn) + E2E_CASE: $(e2e_case) + VITE_MSAL_CLIENT_ID: $(MSAL_CLIENT_ID) + VITE_MSAL_TENANT_ID: $(MSAL_TENANT_ID) + VITE_MSAL_REDIRECT_URI: $(MSAL_REDIRECT_URI_LOCAL) + VITE_POLARIS_GATEWAY_URL: $(POLARIS_GATEWAY_URL) + VITE_POLARIS_GATEWAY_SCOPE: $(POLARIS_GATEWAY_SCOPE) + VITE_GLOBAL_SCRIPT_URL: $(GLOBAL_SCRIPT_URL) + VITE_E2E: True - # - task: PublishTestResults@2 - # displayName: 'Publish E2E Test Results (JUnit)' - # inputs: - # testResultsFormat: 'JUnit' - # testResultsFiles: '$(workingDir)/tests/e2e-test-results.xml' - # testRunTitle: 'E2E Tests' - # publishRunAttachments: false - # condition: always() \ No newline at end of file + - task: PublishPipelineArtifact@1 + displayName: 'Publish E2E Artifact' + inputs: + targetPath: '$(workingDir)/tests/playwright-report' + artifact: 'playwright-report' + publishLocation: 'pipeline' + condition: succeededOrFailed() + + - task: PublishTestResults@2 + displayName: 'Publish E2E Test Results (JUnit)' + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '$(workingDir)/tests/e2e-test-results.xml' + testRunTitle: 'E2E Tests' + publishRunAttachments: false + condition: always() \ No newline at end of file diff --git a/materials_ui/playwright.config.ts b/materials_ui/playwright.config.ts index e0eaf25e..79275299 100644 --- a/materials_ui/playwright.config.ts +++ b/materials_ui/playwright.config.ts @@ -1,5 +1,9 @@ import { defineConfig, devices } from '@playwright/test'; +const e2eUrn = process.env.E2E_URN; +const e2eCase = process.env.E2E_CASE; +const e2eRoute = e2eUrn && e2eCase ? `${e2eUrn}/${e2eCase}/` : ''; + /** * Read environment variables from file. * https://github.com/motdotla/dotenv @@ -29,7 +33,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('')`. */ - baseURL: `http://localhost:3000/materials-ui/${process.env.E2E_URN}/${process.env.E2E_CASE}/`, + baseURL: `http://localhost:3000/materials-ui/${e2eRoute}`, video: 'retain-on-failure', screenshot: 'only-on-failure', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ @@ -87,6 +91,11 @@ export default defineConfig({ command: 'npm run dev', url: 'http://localhost:3000/materials-ui', reuseExistingServer: !process.env.CI, - timeout: 120_000 + timeout: 120_000, + env: { + ...process.env, + // Keep E2E independent from external script availability in CI. + VITE_GLOBAL_SCRIPT_URL: '' + } } }); \ No newline at end of file diff --git a/materials_ui/tests/global-setup/global.setup.ts b/materials_ui/tests/global-setup/global.setup.ts index 23f11746..ff530952 100644 --- a/materials_ui/tests/global-setup/global.setup.ts +++ b/materials_ui/tests/global-setup/global.setup.ts @@ -2,6 +2,7 @@ import { test as setup } from '@playwright/test'; import { log } from 'console'; import * as dotenv from 'dotenv'; setup('setup cookie', async ({ browser }) => { + setup.setTimeout(130_000); dotenv.config(); const username = process.env.E2E_CIN3_USERNAME || ''; @@ -11,6 +12,7 @@ setup('setup cookie', async ({ browser }) => { const mslPassword = process.env.E2E_TEST_MS_PASSWORD || ''; const context = await browser.newContext(); + context.setDefaultNavigationTimeout(130_000); const page = await context.newPage(); //sign in to get cookie @@ -19,6 +21,7 @@ setup('setup cookie', async ({ browser }) => { await page.getByRole('textbox', { name: 'User name' }).fill(username); await page.getByRole('textbox', { name: 'Password' }).fill(password); await page.getByRole('button', { name: 'Log in' }).click(); + await page.screenshot(); const allCookies = await context.cookies(); const authCookie = allCookies.find( @@ -31,16 +34,17 @@ setup('setup cookie', async ({ browser }) => { log('Cms-Auth-Value cookie not found'); } - //MSAL will redirect to Microsoft login page. - await page.goto('/', { timeout: 30000 }); + //MSAL will redirect to Microsoft login page. + // Use a relative URL so Playwright keeps the configured baseURL path. + await page.goto('', { waitUntil: 'domcontentloaded' }); + await page.waitForURL(/login\.microsoftonline\.com|login\.live\.com/); + const signHeader = page.getByRole('heading', { name: 'Sign in' }); await signHeader.waitFor({ timeout: 10000 }); - await page - .locator('#i0116') - .fill(mslUsername); + await page.locator('#i0116').fill(mslUsername); await page.getByRole('button', { name: 'Next' }).click(); await page.locator('#i0118').fill(mslPassword); await page.getByRole('button', { name: 'Sign in' }).click(); await page.getByRole('button', { name: 'Yes' }).click(); await page.context().storageState({ path: 'tests/.auth/globalSetup.json' }); -}); \ No newline at end of file +}); diff --git a/materials_ui/tests/tests-e2e/materials.spec.ts b/materials_ui/tests/tests-e2e/materials.spec.ts index 9233c976..e6b581c3 100644 --- a/materials_ui/tests/tests-e2e/materials.spec.ts +++ b/materials_ui/tests/tests-e2e/materials.spec.ts @@ -80,28 +80,7 @@ test.describe('Materials page', () => { ).toBeVisible(); }); - test('T-005: user is able to filter by none status', async ({ page }) => { - mockRoute( - page, - '/case-materials', - mockCaseMaterials({ - subject: 'Case Action Plan 4', - type: 'Statement', - category: 'Other Material', - status: 'None' - }) - ); - await page.goto('./materials', { waitUntil: 'domcontentloaded' }); - - //None - await page.getByTestId('status-None').check(); - await page.getByTestId('applyFiltersButton').click(); - await expect( - page.getByText('Case Action Plan 4', { exact: true }) - ).toBeVisible(); - }); - - test('T-006: user is able to filter by statement category', async ({ + test('T-005: user is able to filter by statement category', async ({ page }) => { mockRoute( @@ -122,7 +101,7 @@ test.describe('Materials page', () => { ).toBeVisible(); }); - test('T-007: user is able to filter by exhibit', async ({ page }) => { + test('T-006: user is able to filter by exhibit', async ({ page }) => { mockRoute( page, '/case-materials', @@ -140,7 +119,7 @@ test.describe('Materials page', () => { ).toBeVisible(); }); - test('T-008 user is able to filter by category', async ({ page }) => { + test('T-007: user is able to filter by category', async ({ page }) => { mockRoute( page, '/case-materials', @@ -158,7 +137,7 @@ test.describe('Materials page', () => { ).toBeVisible(); }); - test('T-009: user is able to hide filter', async ({ page }) => { + test('T-008: user is able to hide filter', async ({ page }) => { mockRoute(page, '/case-materials', mockCaseMaterials()); await page.getByRole('button', { name: 'Hide filter' }).click(); await expect(page.getByText('FiltersClear filtersSearch')).toBeHidden(); @@ -167,7 +146,7 @@ test.describe('Materials page', () => { }); // search - test('T-010: user is able to search materials', async ({ page }) => { + test('T-009: user is able to search materials', async ({ page }) => { mockRoute( page, '/case-materials', From a639ed911f1c5453e959bd09f49e9eb178b66dc4 Mon Sep 17 00:00:00 2001 From: TahmeedAhmed-NTT-CPS Date: Wed, 1 Jul 2026 14:01:47 +0000 Subject: [PATCH 07/10] error message amended (#417) --- .../DocumentPreview/DocumentPreview.tsx | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/materials_ui/src/components/DocumentPreview/DocumentPreview.tsx b/materials_ui/src/components/DocumentPreview/DocumentPreview.tsx index 9f29aff0..3c4cf482 100644 --- a/materials_ui/src/components/DocumentPreview/DocumentPreview.tsx +++ b/materials_ui/src/components/DocumentPreview/DocumentPreview.tsx @@ -1,9 +1,9 @@ +import { Banner } from '../../components'; import { useDocumentPreview } from '../../hooks/'; import { CaseMaterialsType } from '../../schemas/caseMaterials'; import { ErrorSummary } from '../ErrorSummary/ErrorSummary'; import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner'; import { PdfViewer } from '../PdfViewer/PdfViewer'; -import { Banner } from '../../components'; type Props = { row: CaseMaterialsType }; @@ -14,36 +14,25 @@ export default function DocumentPreview({ row }: Props) { error: caseDocumentError } = useDocumentPreview({ materialId: row.materialId }); - const errorTitle = caseDocumentError?.toString().includes('403') ? - 'This document is password protected' : 'No preview available'; + const errorTitle = caseDocumentError?.toString().includes('403') + ? 'This document is password protected' + : 'There is a problem'; - const errorMessage = caseDocumentError?.toString().includes('403') ? - 'Ask the agency who supplied it to remove the password and resend the document.' : - 'The material you are trying to preview is not currently available. You can view it in CMS.'; + const errorMessage = caseDocumentError?.toString().includes('403') + ? 'Ask the agency who supplied it to remove the password and resend the document.' + : 'This document cannot be shown. You can still view it in CMS.'; let content = null; if (!caseDocumentLoading) { if (caseDocumentError) { content = caseDocumentError.toString().includes('403') ? ( - + ) : ( - + ); } else if (caseDocumentData) { - content = ( - - ); + content = ; } } From 7c025dc2831ad0be12389b49740fb3b692e6e4bf Mon Sep 17 00:00:00 2001 From: benrome-cps Date: Wed, 1 Jul 2026 15:30:14 +0100 Subject: [PATCH 08/10] FCT2-19974 improve highlight and redaction accuracy via bulk redaction (#416) * improve highlight accuracy * improve spacing + change units to points --- .../PdfRedactor/utils/bulkRedactionUtils.ts | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/materials_ui/src/materials_components/PdfRedactor/utils/bulkRedactionUtils.ts b/materials_ui/src/materials_components/PdfRedactor/utils/bulkRedactionUtils.ts index 966a1059..eb362fcf 100644 --- a/materials_ui/src/materials_components/PdfRedactor/utils/bulkRedactionUtils.ts +++ b/materials_ui/src/materials_components/PdfRedactor/utils/bulkRedactionUtils.ts @@ -2,23 +2,29 @@ import type { TBulkSearchResponse } from '../../CaseworkPdfRedactorWrapper/utils import type { TRedaction } from './coordUtils'; import type { TSearchHighlight } from './searchHighlightUtils'; -const PADDING_INCHES = 0.03; +const POINTS_PER_INCH = 72; + +const PADDING_X_POINTS = 3; +const PADDING_Y_POINTS = 1.1; export const convertSearchResponseToRedactions = ( resp: TBulkSearchResponse ): TRedaction[] => resp.redactionDefinitions .flatMap((def) => - def.redactionCoordinates.map((c) => ({ - id: crypto.randomUUID(), - pageNumber: def.pageIndex, - pageWidth: def.width, - pageHeight: def.height, - x1: c.x1, - y1: def.height - c.y1, - x2: c.x2, - y2: def.height - c.y2 - })) + def.redactionCoordinates.map((c) => { + const toPoints = (inches: number) => inches * POINTS_PER_INCH; + return { + id: crypto.randomUUID(), + pageNumber: def.pageIndex, + pageWidth: toPoints(def.width), + pageHeight: toPoints(def.height), + x1: toPoints(Math.min(c.x1, c.x2)), + y1: toPoints(def.height - Math.max(c.y1, c.y2)) - PADDING_Y_POINTS, + x2: toPoints(Math.max(c.x1, c.x2)) + PADDING_X_POINTS, + y2: toPoints(def.height - Math.min(c.y1, c.y2)) + PADDING_Y_POINTS + }; + }) ) .sort((a, b) => { if (a.pageNumber !== b.pageNumber) return a.pageNumber - b.pageNumber; @@ -33,8 +39,8 @@ export const convertCandidatesToSearchHighlights = ( pageNumber: c.pageNumber, pageHeight: c.pageHeight, pageWidth: c.pageWidth, - xLeft: Math.min(c.x1, c.x2) + PADDING_INCHES, - xRight: Math.max(c.x1, c.x2) + PADDING_INCHES, - yTop: c.pageHeight - Math.max(c.y1, c.y2) - PADDING_INCHES, - yBottom: c.pageHeight - Math.min(c.y1, c.y2) + PADDING_INCHES + xLeft: Math.min(c.x1, c.x2), + xRight: Math.max(c.x1, c.x2), + yTop: c.pageHeight - Math.max(c.y1, c.y2), + yBottom: c.pageHeight - Math.min(c.y1, c.y2) })); From 91fddaf1888d16e30fc1010bb15a4e272727f8ec Mon Sep 17 00:00:00 2001 From: robmolloy-cps Date: Thu, 2 Jul 2026 08:38:49 +0000 Subject: [PATCH 09/10] FCT2-18679-fe-pcd-review-page-needs-to-show-rich-text-formatting-2 (#419) * FCT2-18679-fe-pcd-review-page-needs-to-show-rich-text-formatting-2 * FCT2-18679-fe-pcd-review-page-needs-to-show-rich-text-formatting-2 --- materials_ui/src/pages/PcdReview.tsx | 39 ++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/materials_ui/src/pages/PcdReview.tsx b/materials_ui/src/pages/PcdReview.tsx index ebf5201c..6300c66e 100644 --- a/materials_ui/src/pages/PcdReview.tsx +++ b/materials_ui/src/pages/PcdReview.tsx @@ -22,6 +22,31 @@ import type { PCDReviewDetailsResponseType } from '../schemas/pcdReview'; +/** + * compactWhitespace + * - One or more spaces or tabs become a single space. + * - A single line break is preserved. + * - Two or more consecutive line breaks become exactly two line breaks. + * - If a run of whitespace contains both line breaks and spaces/tabs, + * a single trailing space is preserved after the normalized line break(s). + */ +const compactWhitespace = (text: string) => { + return text.replace(/[ \t\r\n]+/g, (match) => { + const newlineCount = (match.match(/\n/g) ?? []).length; + const hasSpaces = /[ \t]/.test(match); + + const lineBreaks = (() => { + if (newlineCount >= 2) return '\n\n'; + if (newlineCount === 1) return '\n'; + return ''; + })(); + + const spaces = hasSpaces ? ' ' : ''; + + return lineBreaks + spaces; + }); +}; + dayjs.extend(customParseFormat); const PCD_REVIEW_CORE_NAV_TYPE_ORDER: PcdReviewCoreType[] = [ @@ -121,11 +146,21 @@ const CaseHeadlineCodeTest = ({ analysis }: { analysis: AnalysisOutcome }) => { return ( <>

Case Headline / Code Test

-

{analysis.caseSummary}

+
{items.map((c) => (

{c.header}

-

{c.body}

+ {c.body && ( +
+ )}
))} From 5095ce674464435a40f2f69f57667a34d12aaf99 Mon Sep 17 00:00:00 2001 From: robmolloy-cps Date: Thu, 2 Jul 2026 09:02:49 +0000 Subject: [PATCH 10/10] FCT2-18679-fe-pcd-review-page-needs-to-show-rich-text-formatting-3 (#422) --- materials_ui/src/pages/PcdReview.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/materials_ui/src/pages/PcdReview.tsx b/materials_ui/src/pages/PcdReview.tsx index 6300c66e..106afa3a 100644 --- a/materials_ui/src/pages/PcdReview.tsx +++ b/materials_ui/src/pages/PcdReview.tsx @@ -1,5 +1,6 @@ import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat'; +import DomPurify from 'dompurify'; import { Navigate, useParams } from 'react-router-dom'; import { @@ -149,7 +150,7 @@ const CaseHeadlineCodeTest = ({ analysis }: { analysis: AnalysisOutcome }) => {
{items.map((c) => ( @@ -158,7 +159,9 @@ const CaseHeadlineCodeTest = ({ analysis }: { analysis: AnalysisOutcome }) => { {c.body && (
)}