Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export default function ListPageHeader({
variant="text"
sx={{
alignSelf: "flex-start",
px: 0,
px: 1,
ml: -1,
minWidth: 0,
mb: 0,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { type JSX } from "react";
*/
export default function CaseDetailsBackButton({
onClick,
sx = { mb: 1.5, ml: -0.5, alignSelf: "flex-start" },
sx = { mb: 1.5, ml: -1, px: 1, alignSelf: "flex-start" },
}: CaseDetailsBackButtonProps): JSX.Element {
return (
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import { Box } from "@wso2/oxygen-ui";
import DOMPurify from "dompurify";
import type { JSX } from "react";
import { stripLightModeInlineStyles } from "@utils/common";

// Matches actual HTML formatting tags — not XML/config tag-like strings e.g. <Product_Home>.
export const HTML_FORMAT_RE =
/<\/?(p|span|div|ul|ol|li|strong|em|b|i|br|h[1-6]|a[\s>]|table|tr|td|th|code|pre|blockquote)\b/i;

export function decodeEntities(s: string): string {
return s
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&nbsp;/g, " ");
}

export const HTML_CONTENT_SX = {
fontSize: "0.875rem",
lineHeight: 1.7,
color: "text.secondary",
"& p": { margin: "0 0 0.4em 0" },
"& p:last-child": { marginBottom: 0 },
"& a": { color: "primary.main", textDecoration: "underline" },
"& ul, & ol": { mt: 0, mb: 0.5, pl: 2.5 },
"& li": { mb: 0.25 },
"& strong, & b": { fontWeight: 600, color: "text.primary" },
};

/**
* Renders a string that may contain real HTML markup or plain text with HTML
* entities (e.g. &lt;Product_Home&gt;). Real HTML formatting tags are sanitized
* via DOMPurify with optional dark-mode inline-style stripping. Plain text has
* entities decoded and newlines converted to <br> so angle-bracket content
* renders correctly.
*/
export function HtmlOrText({
content,
isDark,
}: {
content: string;
isDark: boolean;
}): JSX.Element {
if (HTML_FORMAT_RE.test(content)) {
const stripped = isDark ? stripLightModeInlineStyles(content) : content;
return (
<Box
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(stripped) }}
sx={HTML_CONTENT_SX}
/>
);
}

const decoded = decodeEntities(content);
const safeHtml = decoded
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\n/g, "<br>");
return <Box dangerouslySetInnerHTML={{ __html: safeHtml }} sx={HTML_CONTENT_SX} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ import {
generateUpdateLevelsReportPdf,
isSafeHttpUrl,
parseBugFixes,
parseDescriptionSections,
} from "@features/updates/utils/updateLevelsReportPdf";
import type { UpdateLevelsReportModalProps, UpdateDescriptionLevel } from "@features/updates/types/updates";
import { useDarkMode } from "@utils/useDarkMode";
import { HtmlOrText } from "@features/updates/components/HtmlOrText";

function isInstructionsNonEmpty(text: string | null | undefined): boolean {
if (!text) return false;
Expand All @@ -48,17 +49,12 @@ function formatTimestamp(ts: number): string {
return `${months[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()}`;
}

function UpdateDescriptionItem({ item }: { item: UpdateDescriptionLevel }): JSX.Element {
const parsed = parseDescriptionSections(item.description);
const displayDesc = parsed.generalDescription || parsed.implementationDetails || parsed.impact;
function UpdateDescriptionItem({ item, isDark }: { item: UpdateDescriptionLevel; isDark: boolean }): JSX.Element {
const bugFixUrls = parseBugFixes(item.bugFixes);
const desc = item.description?.trim();
return (
<Box sx={{ pl: 2, py: 0.5, borderLeft: "2px solid", borderColor: "divider" }}>
{displayDesc && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
{displayDesc}
</Typography>
)}
{desc && <HtmlOrText content={desc} isDark={isDark} />}
{bugFixUrls.length > 0 && (
<Typography variant="caption" color="text.secondary">
Bug fixes:{" "}
Expand Down Expand Up @@ -90,6 +86,7 @@ export default function UpdateLevelsReportModal({
rawData,
}: UpdateLevelsReportModalProps): JSX.Element | null {
const [isDownloading, setIsDownloading] = useState(false);
const isDark = useDarkMode();

const handleDownloadPdf = useCallback(() => {
if (!reportData) return;
Expand Down Expand Up @@ -202,7 +199,7 @@ export default function UpdateLevelsReportModal({
</Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.75 }}>
{entry.updateDescriptionLevels.map((item, i) => (
<UpdateDescriptionItem key={i} item={item} />
<UpdateDescriptionItem key={i} item={item} isDark={isDark} />
))}
</Box>
</Box>
Expand Down Expand Up @@ -244,7 +241,7 @@ export default function UpdateLevelsReportModal({
</Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.75 }}>
{entry.updateDescriptionLevels.map((item, i) => (
<UpdateDescriptionItem key={i} item={item} />
<UpdateDescriptionItem key={i} item={item} isDark={isDark} />
))}
</Box>
</Box>
Expand Down Expand Up @@ -307,7 +304,6 @@ export default function UpdateLevelsReportModal({
Update Details
</Typography>
{reportData.allEntries.map((entry, i) => {
const parsedDesc = parseDescriptionSections(entry.description);
const bugFixUrls = parseBugFixes(entry.bugFixes);
return (
<Box key={i} sx={{ mb: 3 }}>
Expand All @@ -328,28 +324,12 @@ export default function UpdateLevelsReportModal({
{entry.productName}-{entry.productVersion} ({entry.channel})
</Typography>
</Box>
{parsedDesc.generalDescription && (
<Box sx={{ mb: 1, pl: 1 }}>
<Typography variant="caption" fontWeight={700} display="block">
General Description:
</Typography>
<Typography variant="body2">{parsedDesc.generalDescription}</Typography>
</Box>
)}
{parsedDesc.implementationDetails && (
{entry.description?.trim() && (
<Box sx={{ mb: 1, pl: 1 }}>
<Typography variant="caption" fontWeight={700} display="block">
Implementation Details:
Description:
</Typography>
<Typography variant="body2">{parsedDesc.implementationDetails}</Typography>
</Box>
)}
{parsedDesc.impact && (
<Box sx={{ mb: 1, pl: 1 }}>
<Typography variant="caption" fontWeight={700} display="block">
Impact:
</Typography>
<Typography variant="body2">{parsedDesc.impact}</Typography>
<HtmlOrText content={entry.description.trim()} isDark={isDark} />
</Box>
)}
{bugFixUrls.length > 0 && (
Expand All @@ -371,27 +351,11 @@ export default function UpdateLevelsReportModal({
</Box>
)}
{isInstructionsNonEmpty(entry.instructions) && (
<Box sx={{ mb: 1 }}>
<Typography variant="caption" fontWeight={700} display="block" sx={{ pl: 1, mb: 0.5 }}>
<Box sx={{ mb: 1, pl: 1 }}>
<Typography variant="caption" fontWeight={700} display="block" sx={{ mb: 0.5 }}>
Instructions:
</Typography>
<Box
component="pre"
sx={{
bgcolor: "grey.100",
border: "1px solid",
borderColor: "divider",
borderRadius: 1,
p: 1.5,
m: 0,
fontFamily: "monospace",
fontSize: "0.75rem",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{entry.instructions?.trim()}
</Box>
<HtmlOrText content={entry.instructions!.trim()} isDark={isDark} />
</Box>
)}
{(entry.securityAdvisories?.length ?? 0) > 0 && (
Expand All @@ -405,12 +369,13 @@ export default function UpdateLevelsReportModal({
>
Security Advisories:
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{entry.securityAdvisories.map((adv, j) => (
<Box component="li" key={j} sx={{ mb: 0.25 }}>
<Typography variant="caption">
<strong>{adv.id}</strong> ({adv.severity}): {adv.overview}
<Box key={j} sx={{ pl: 1 }}>
<Typography variant="caption" fontWeight={700}>
{adv.id} ({adv.severity})
</Typography>
{adv.overview && <HtmlOrText content={adv.overview} isDark={isDark} />}
</Box>
))}
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
} from "@wso2/oxygen-ui";
import { ArrowLeft, ExternalLink } from "@wso2/oxygen-ui-icons-react";
import { useState, useMemo, type JSX } from "react";
import { useDarkMode } from "@utils/useDarkMode";
import { HtmlOrText } from "@features/updates/components/HtmlOrText";
import { useNavigate, useParams, useSearchParams } from "react-router";
import { usePostUpdateLevelsSearch } from "@features/updates/api/usePostUpdateLevelsSearch";
import { getUpdateTypeChipColor } from "@features/updates/utils/updates";
Expand Down Expand Up @@ -96,37 +98,21 @@ function parseJsonStringArray(raw: string): string[] {
}
}

/**
* Renders a labelled section with pre-wrapped text, supporting `\n` as real line breaks.
*
* @param {{ title: string; content: string }} props - Section heading and raw content string.
* @returns {JSX.Element} The rendered section.
*/
function UpdateSection({
title,
content,
isDark,
}: {
title: string;
content: string;
isDark: boolean;
}): JSX.Element {
const lines = content.split("\n");
return (
<Box>
<Typography variant="subtitle2" fontWeight={700} sx={{ mb: 0.75 }}>
{title}
</Typography>
<Box>
{lines.map((line, i) => (
<Typography
key={i}
variant="body2"
color="text.secondary"
sx={{ lineHeight: 1.7 }}
>
{line || "\u00A0"}
</Typography>
))}
</Box>
<HtmlOrText content={content} isDark={isDark} />
</Box>
);
}
Expand All @@ -143,6 +129,7 @@ function UpdateDetailCard({
desc: UpdateDescriptionLevel;
}): JSX.Element {
const theme = useTheme();
const isDark = useDarkMode();
const chipColor = getUpdateTypeChipColor(desc.updateType);
const bugFixes = parseJsonStringArray(desc.bugFixes);
const filesModified = parseJsonStringArray(desc.filesModified);
Expand Down Expand Up @@ -203,12 +190,12 @@ function UpdateDetailCard({
<Box sx={{ display: "flex", flexDirection: "column", gap: 2.5 }}>
{/* Description */}
{desc.description && (
<UpdateSection title="Description" content={desc.description} />
<UpdateSection title="Description" content={desc.description} isDark={isDark} />
)}

{/* Instructions */}
{desc.instructions && desc.instructions !== "N/A" && (
<UpdateSection title="Instructions" content={desc.instructions} />
<UpdateSection title="Instructions" content={desc.instructions} isDark={isDark} />
)}

{/* Bug Fixes */}
Expand Down Expand Up @@ -385,22 +372,26 @@ function UpdateDetailCard({
}}
/>
</Box>
<Typography
variant="body2"
fontWeight={600}
sx={{ mb: 0.5 }}
>
{advisory.overview}
</Typography>
<UpdateSection
title="Description"
content={advisory.description}
/>
<Box sx={{ mt: 1 }}>
<UpdateSection
title="Overview"
content={advisory.overview}
isDark={isDark}
/>
</Box>
<Box sx={{ mt: 1.5 }}>
<UpdateSection
title="Description"
content={advisory.description}
isDark={isDark}
/>
</Box>
{advisory.impact && (
<Box sx={{ mt: 1.5 }}>
<UpdateSection
title="Impact"
content={advisory.impact}
isDark={isDark}
/>
</Box>
)}
Expand All @@ -409,19 +400,21 @@ function UpdateDetailCard({
<UpdateSection
title="Solution"
content={advisory.solution}
isDark={isDark}
/>
</Box>
)}
{advisory.notes && (
<Box sx={{ mt: 1.5 }}>
<UpdateSection title="Notes" content={advisory.notes} />
<UpdateSection title="Notes" content={advisory.notes} isDark={isDark} />
</Box>
)}
{advisory.credits && advisory.credits !== "-" && (
<Box sx={{ mt: 1.5 }}>
<UpdateSection
title="Credits"
content={advisory.credits}
isDark={isDark}
/>
</Box>
)}
Expand Down
Loading