[Customer Portal][FE][Web] Enhance Markdown Rendering and Case Activity Timeline with Better Error Handling - #519
Conversation
Introduce throwIfApiNotOk(response, fallbackMessage) to throw an ApiError for non-OK fetch responses while preserving any backend-provided message. The helper clones and attempts to parse the JSON body to extract a string `message`, falls back to a composed message when absent, and returns silently for OK responses.
Add remark-gfm@^4.0.1 to apps/customer-portal/webapp/package.json to enable GitHub Flavored Markdown (GFM) support for react-markdown in the customer portal webapp.
Short-circuits SupportPage rendering on API errors by aggregating hook errors (stats/cases/conversations) and rendering ApiErrorState with a fallback message. Refactors CaseDetails.conversation from unknown to IdLabelRef|null and exposes error or conversationId props across several support component prop types (CaseDetailsDetailsPanelProps, CallsPanelProps, CaseDetailsContentProps, CaseDetailsTabPanelsProps, CaseDetailsActivityPanelProps). Also adds optional markdown props to ChatMessageCardProps (markdownContent, renderAsMarkdown) to support rendering message content. These changes improve error handling and type-safety for support UI components.
Introduce ApiErrorState component (apps/customer-portal/webapp/src/components/error/ApiErrorState.tsx) to provide a consistent API error UI for support pages and panels. It dispatches to Error401Page and Error403Page for unauthorized/forbidden errors using helpers from @utils/ApiError, and falls back to a 500-style view with a customizable message and container styling. Includes license header and typed props for error, fallbackMessage, and containerSx.
Merge conversation messages into case activity timeline and improve rendering and error handling. Added useGetConversationMessages and merged its comments with case comments (sorted by createdOn/id). Updated loading/error logic to consider conversation fetches and surface API errors using ApiErrorState. ChatMessageCard now supports rendering markdown (ReactMarkdown + remark-gfm) with safe link handling and table styling; CommentBubble selects bot/‘novera’ comments to render as markdown. Attachments and calls panels now display ApiErrorState on errors; CallsErrorState was refactored to accept an error prop. Miscellaneous styling tweaks for message content.
Replace inline error UIs with ApiErrorState across support components and pages: ConversationKnowledgeRecommendations and ConversationDetailsPage now import and render ApiErrorState, and data-fetch hooks have their `error` returned and forwarded where needed (also passed into CaseDetailsPage child). Add GitHub Flavored Markdown table support in ChatMessageBubble by importing remark-gfm, registering it with ReactMarkdown, and adding styled table/thead/tbody/tr/th/td renderers with responsive scrolling.
Replace custom error UI (Error500Page and inline markup) with ApiErrorState across case details components. Add an early return in CaseDetailsContent to render ApiErrorState when isError and error are present, and thread the error prop through CaseDetailsTabPanels to CaseDetailsDetailsPanel, CaseDetailsActivityPanel (also passing conversationId), and CallsPanel so they can render consistent API error states.
Update pnpm-lock.yaml to include remark-gfm and its associated GitHub-Flavored-Markdown packages and lock metadata. Changes add remark-gfm to importers and add related packages (remark-stringify, mdast-util-gfm and helpers, micromark GFM extensions, markdown-table, escape-string-regexp, etc.) along with their snapshots and dependency entries so the lockfile reflects the new GFM processing dependencies. No source files were modified.
Delete the throwIfApiNotOk helper from ApiError.ts. This removes the function that parsed Response bodies and threw an ApiError for non-OK fetch responses; callers should rely on existing error utilities (e.g. getApiErrorMessage) or implement alternative handling.
📝 WalkthroughWalkthroughThis PR introduces Markdown/GFM rendering support and unified error handling across the support portal. A new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/customer-portal/webapp/src/features/support/pages/CaseDetailsPage.tsx (1)
71-134:⚠️ Potential issue | 🟡 MinorPossible duplicate error surfacing.
erroris now passed intoCaseDetailsContent, which (per the snippet atCaseDetailsContent.tsx:210-221) will renderApiErrorStatein-page onisError && error. Meanwhile the existinguseEffectat lines 71–80 still callsshowError("Could not load case details."), so users see both the full-page 401/403/500 error state and a toast/banner for the same failure. Consider dropping the banner (or suppressing it whenApiErrorStatewill render) to avoid the duplicate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/features/support/pages/CaseDetailsPage.tsx` around lines 71 - 134, The page currently shows both an in-page ApiErrorState and a toast/banners by calling showError from the useEffect; update the useEffect that checks isError to only call showError when the in-page error component (CaseDetailsContent -> ApiErrorState) will NOT render (i.e., suppress the banner when error is present). Concretely, modify the useEffect around useEffect(() => { ... }, [isError, showError]) to check error (the same error prop passed into CaseDetailsContent) and only call showError("Could not load case details.") when isError is true and error is falsy (or when the conditions for ApiErrorState are false), so you no longer surface duplicate error UI between useEffect/showError and CaseDetailsContent -> ApiErrorState.apps/customer-portal/webapp/src/features/support/components/case-details/details-tab/CaseDetailsContent.tsx (1)
210-235:⚠️ Potential issue | 🔴 CriticalMove the new error return below the remaining hook.
Line 210 now returns before the
useEffecton Line 223. IfisError && errorchanges between renders, this component calls fewer hooks and can crash with React’s hook-order error. Keep all hooks before conditional returns.🐛 Proposed fix
- if (isError && error) { - return ( - <ApiErrorState - error={error} - fallbackMessage={ - isServiceRequest - ? "Failed to load service request details." - : "Failed to load case details." - } - /> - ); - } - useEffect(() => { if (!suppressFloatingNoveraOnActivityTab) { return; } @@ setHideForDetailsActivityTab, ]); + + if (isError && error) { + return ( + <ApiErrorState + error={error} + fallbackMessage={ + isServiceRequest + ? "Failed to load service request details." + : "Failed to load case details." + } + /> + ); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/features/support/components/case-details/details-tab/CaseDetailsContent.tsx` around lines 210 - 235, The early return that renders ApiErrorState when isError && error is currently placed before the useEffect hook, which can change hook order across renders; move the conditional block that returns the ApiErrorState (the check using isError and error and the ApiErrorState component) so that all hooks (including the useEffect that references suppressFloatingNoveraOnActivityTab, resolvedPanelIndex, and setHideForDetailsActivityTab) are declared first, and only after those hooks run perform the conditional return; ensure references to ApiErrorState and the existing fallbackMessage logic remain unchanged.apps/customer-portal/webapp/src/features/support/components/case-details/activity-tab/ChatMessageCard.tsx (1)
119-140:⚠️ Potential issue | 🟡 MinorMake markdown-rendered image previews keyboard-accessible.
Markdown can now render images into the same preview flow, but preview activation is still click-only and images are not focusable. Add delegated
keydownhandling for Enter/Space plustabIndex,role, and an accessible label on rendered images. Based on learnings, the ChatMessageCard image fullscreen preview is pointer-only and should add delegatedhandleKeyDown,tabIndex=0,role="button",aria-label, and cleanup.Also applies to: 221-229, 260-263
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/features/support/components/case-details/activity-tab/ChatMessageCard.tsx` around lines 119 - 140, The click-only image preview needs keyboard accessibility: add a delegated handleKeyDown alongside handleClick that listens on contentRef for keydown events and, when event.target is an HTMLImageElement and key is Enter or Space, prevents default and calls onImageClick with the image src; also ensure rendered images get tabIndex=0, role="button" and an aria-label (e.g., "Open image preview") at render time (update the markdown image renderer or any render path that outputs <img>), and register/unregister the keydown listener in the same useEffect cleanup as handleClick (update references: handleClick, handleKeyDown, contentRef, onImageClick). Apply the same delegated keydown + attribute changes to other image-rendering handlers mentioned (the other markdown/render paths).
🧹 Nitpick comments (2)
apps/customer-portal/webapp/src/features/support/pages/ConversationDetailsPage.tsx (1)
281-286: Nit: redundant wrapper aroundApiErrorState.
ApiErrorStatealready applies centered layout withpy: 4internally; the surrounding<Box sx={{ textAlign: "center", py: 6 }}>double-pads the error and may visually conflict with the 401/403 full-page variants thatApiErrorStatecan render. Consider dropping the wrapper (or moving tocontainerSxonApiErrorState).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/features/support/pages/ConversationDetailsPage.tsx` around lines 281 - 286, Remove the redundant Box wrapper around ApiErrorState in ConversationDetailsPage: ApiErrorState already centers and applies internal padding, so delete the surrounding <Box sx={{ textAlign: "center", py: 6 }}> (or instead pass any needed styling via ApiErrorState's containerSx prop) to avoid double padding and preserve correct full-page 401/403 variants.apps/customer-portal/webapp/src/features/support/components/case-details/activity-tab/ChatMessageCard.tsx (1)
265-265: Add JSDoc documentation tohtmlContentparameter requiring pre-sanitization.The component accepts
htmlContentwithout sanitizing it internally. While the only current call site (CommentBubble) sanitizes before passing, document the pre-sanitization requirement to prevent future unsafe usage.Add a note to the JSDoc for
ChatMessageCardPropsclarifying thathtmlContentmust be sanitized by the caller before passing to ChatMessageCard (e.g., with DOMPurify).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/customer-portal/webapp/src/features/support/components/case-details/activity-tab/ChatMessageCard.tsx` at line 265, Add JSDoc to the ChatMessageCardProps interface documenting that the htmlContent prop passed into ChatMessageCard must be sanitized by the caller (e.g., with DOMPurify) before use; update the JSDoc to mention that ChatMessageCard uses dangerouslySetInnerHTML and does not perform sanitization itself, and reference typical caller CommentBubble as an example of where sanitization should occur so future callers know the precondition.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/customer-portal/webapp/src/components/error/ApiErrorState.tsx`:
- Around line 45-58: ApiErrorState is inconsistenly applying containerSx: the
401/403 branches render Error401Page/Error403Page without applying containerSx
and the 500 branch uses object spread which can drop SxProps like
functions/arrays. Update ApiErrorState so all error branches pass the merged sx
using MUI's array form (e.g., sx={[ baseSx, containerSx ]}) instead of
{...containerSx}; specifically, when rendering Error401Page and Error403Page add
an sx prop that merges the same base Stack styles (py: 4 and any other container
base styles) with containerSx using an array, and replace the object spread in
the main Stack return with the same array-based sx merging.
In `@apps/customer-portal/webapp/src/features/support/pages/SupportPage.tsx`:
- Around line 133-141: The current early-return uses supportPageError =
statsError ?? casesError ?? chatError which hides healthy sections; revert to
per-card error handling by removing the early return around ApiErrorState so
CasesOverviewStatCard and SupportOverviewCard continue to receive their isError
/ isCasesError / isChatError props and render section-level errors;
alternatively, if you prefer a short-circuit, change the logic to only treat the
page as failed when all sub-queries fail (supportPageError = statsError &&
casesError && chatError) and only promote to ApiErrorState for auth failures
coming from the primary stats query (check statsError.isUnauthorizedError /
isForbiddenError before returning ApiErrorState).
---
Outside diff comments:
In
`@apps/customer-portal/webapp/src/features/support/components/case-details/activity-tab/ChatMessageCard.tsx`:
- Around line 119-140: The click-only image preview needs keyboard
accessibility: add a delegated handleKeyDown alongside handleClick that listens
on contentRef for keydown events and, when event.target is an HTMLImageElement
and key is Enter or Space, prevents default and calls onImageClick with the
image src; also ensure rendered images get tabIndex=0, role="button" and an
aria-label (e.g., "Open image preview") at render time (update the markdown
image renderer or any render path that outputs <img>), and register/unregister
the keydown listener in the same useEffect cleanup as handleClick (update
references: handleClick, handleKeyDown, contentRef, onImageClick). Apply the
same delegated keydown + attribute changes to other image-rendering handlers
mentioned (the other markdown/render paths).
In
`@apps/customer-portal/webapp/src/features/support/components/case-details/details-tab/CaseDetailsContent.tsx`:
- Around line 210-235: The early return that renders ApiErrorState when isError
&& error is currently placed before the useEffect hook, which can change hook
order across renders; move the conditional block that returns the ApiErrorState
(the check using isError and error and the ApiErrorState component) so that all
hooks (including the useEffect that references
suppressFloatingNoveraOnActivityTab, resolvedPanelIndex, and
setHideForDetailsActivityTab) are declared first, and only after those hooks run
perform the conditional return; ensure references to ApiErrorState and the
existing fallbackMessage logic remain unchanged.
In `@apps/customer-portal/webapp/src/features/support/pages/CaseDetailsPage.tsx`:
- Around line 71-134: The page currently shows both an in-page ApiErrorState and
a toast/banners by calling showError from the useEffect; update the useEffect
that checks isError to only call showError when the in-page error component
(CaseDetailsContent -> ApiErrorState) will NOT render (i.e., suppress the banner
when error is present). Concretely, modify the useEffect around useEffect(() =>
{ ... }, [isError, showError]) to check error (the same error prop passed into
CaseDetailsContent) and only call showError("Could not load case details.") when
isError is true and error is falsy (or when the conditions for ApiErrorState are
false), so you no longer surface duplicate error UI between useEffect/showError
and CaseDetailsContent -> ApiErrorState.
---
Nitpick comments:
In
`@apps/customer-portal/webapp/src/features/support/components/case-details/activity-tab/ChatMessageCard.tsx`:
- Line 265: Add JSDoc to the ChatMessageCardProps interface documenting that the
htmlContent prop passed into ChatMessageCard must be sanitized by the caller
(e.g., with DOMPurify) before use; update the JSDoc to mention that
ChatMessageCard uses dangerouslySetInnerHTML and does not perform sanitization
itself, and reference typical caller CommentBubble as an example of where
sanitization should occur so future callers know the precondition.
In
`@apps/customer-portal/webapp/src/features/support/pages/ConversationDetailsPage.tsx`:
- Around line 281-286: Remove the redundant Box wrapper around ApiErrorState in
ConversationDetailsPage: ApiErrorState already centers and applies internal
padding, so delete the surrounding <Box sx={{ textAlign: "center", py: 6 }}> (or
instead pass any needed styling via ApiErrorState's containerSx prop) to avoid
double padding and preserve correct full-page 401/403 variants.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e3d740d4-371d-4bec-8434-acd870846c30
⛔ Files ignored due to path filters (1)
apps/customer-portal/webapp/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (18)
apps/customer-portal/webapp/package.jsonapps/customer-portal/webapp/src/components/error/ApiErrorState.tsxapps/customer-portal/webapp/src/features/support/components/case-details/activity-tab/CaseDetailsActivityPanel.tsxapps/customer-portal/webapp/src/features/support/components/case-details/activity-tab/ChatMessageCard.tsxapps/customer-portal/webapp/src/features/support/components/case-details/activity-tab/CommentBubble.tsxapps/customer-portal/webapp/src/features/support/components/case-details/attachments-tab/CaseDetailsAttachmentsPanel.tsxapps/customer-portal/webapp/src/features/support/components/case-details/calls-tab/CallsErrorState.tsxapps/customer-portal/webapp/src/features/support/components/case-details/calls-tab/CallsPanel.tsxapps/customer-portal/webapp/src/features/support/components/case-details/details-tab/CaseDetailsContent.tsxapps/customer-portal/webapp/src/features/support/components/case-details/details-tab/CaseDetailsDetailsPanel.tsxapps/customer-portal/webapp/src/features/support/components/case-details/header/CaseDetailsTabPanels.tsxapps/customer-portal/webapp/src/features/support/components/knowledge-base/ConversationKnowledgeRecommendations.tsxapps/customer-portal/webapp/src/features/support/components/novera-ai-assistant/novera-chat-page/ChatMessageBubble.tsxapps/customer-portal/webapp/src/features/support/pages/CaseDetailsPage.tsxapps/customer-portal/webapp/src/features/support/pages/ConversationDetailsPage.tsxapps/customer-portal/webapp/src/features/support/pages/SupportPage.tsxapps/customer-portal/webapp/src/features/support/types/cases.tsapps/customer-portal/webapp/src/features/support/types/supportComponents.ts
Refactor error presentation and enhance image accessibility across support pages. - ApiErrorState: normalize container styling by merging base SxProps and wrap 401/403/error pages in a Stack for consistent layout. - ChatMessageCard: add markdown img renderer and set keyboard/ARIA attributes (tabindex, role, aria-label) for images; add keydown handler to open image preview on Enter/Space and a helper to set attributes for existing images. - CaseDetailsContent: relocate ApiErrorState return to avoid interfering with existing effects/run order. - CaseDetailsPage: avoid showing a duplicate banner when an in-page ApiErrorState is present by short-circuiting error notification and adding `error` to effect deps. - ConversationDetailsPage: remove redundant centering wrapper around ApiErrorState. - SupportPage: remove the global early ApiErrorState fallback to rely on component-level error handling. These changes improve a11y for inline images and unify how API errors are displayed to reduce duplicate notifications and layout inconsistencies.
a8e6da5
into
wso2-open-operations:dev-app-customer-portal
Description
This pull request introduces support for GitHub Flavored Markdown (GFM) rendering in the customer portal webapp and improves error handling in the support case activity panel. The main changes include adding the
remark-gfmpackage and its dependencies, and refactoring theCaseDetailsActivityPanelto merge conversation messages with case comments and handle API errors more consistently.Markdown rendering enhancements:
remark-gfmpackage topackage.jsonand updatedpnpm-lock.yamlto includeremark-gfmand all required dependencies for GitHub Flavored Markdown support. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13]Support case activity panel improvements:
CaseDetailsActivityPanelto merge both case comments and conversation messages into a single, chronologically ordered timeline, improving the completeness of the activity feed. [1] [2]CaseDetailsActivityPanelto show a consistent error UI using the newApiErrorStatecomponent. [1] [2] [3]Error handling:
ApiErrorStatecomponent to standardize API error display across the app, with support for 401, 403, and 500 errors.These changes lay the groundwork for improved Markdown rendering in the UI and provide a more robust and user-friendly experience when loading support case activities.
Summary by CodeRabbit
New Features
Chores