Skip to content

[Customer Portal][FE][Web] fix: Auth UX – idle timeout, token refresh error handling, upload cleanup - #158

Merged
Rashmika998 merged 32 commits into
wso2-open-operations:customer-portal-milestone-1from
dileepapeiris:fix/m1-auth-issues
Feb 16, 2026
Merged

Rashmika998 merged 32 commits into
wso2-open-operations:customer-portal-milestone-1from
dileepapeiris:fix/m1-auth-issues

Conversation

@dileepapeiris

@dileepapeiris dileepapeiris commented Feb 16, 2026 •

Copy link
Copy Markdown
Contributor

Description

Addresses review feedback on auth-related behavior: idle timeout forced sign-out, token refresh error handling, and upload flow cleanup before navigation.

Changes

Auth & session

  • src/providers/IdleTimeoutProvider.tsx
    • Add onIdle handler: when the user ignores the "Are you still there?" prompt, sign out and redirect to /login when the idle timeout is reached.

API client

  • src/utils/apiClient.ts
    • In the 401 token-refresh flow: await signOut() in the catch block and rethrow the original error (or wrap with cause) instead of replacing it with "Token refresh failed".

Test setup

  • src/vitest.setup.ts
    • Mock useAuthApiClient with mockImplementation() so each call gets a new Response instance instead of a reused one (response bodies can only be consumed once).

UI flow

  • src/pages/CreateCasePage.tsx
    • Call setIsUploadingAttachments(false) in the finally block before navigate(), so state is reset while the component is still mounted and avoids setState on an unmounted component.

Files changed

File Change
src/providers/IdleTimeoutProvider.tsx Add onIdle handler for forced sign-out
src/utils/apiClient.ts Await signOut, preserve original error in refresh flow
src/vitest.setup.ts Fresh Response per mock call
src/pages/CreateCasePage.tsx Upload cleanup before navigation

Summary by CodeRabbit

Release Notes

  • New Features

    • Rich text editor for case descriptions with formatting, images, links, and syntax highlighting
    • Session idle timeout warning that prompts users before automatic logout
    • Enhanced attachment handling in case creation workflow with preview and removal options
  • Improvements

    • Improved case action availability based on case status
    • HTML sanitization in case titles and descriptions for better display
    • Better error handling and status indicators in case details
  • Style

    • Added light and dark mode favicon support

Add react-idle-timer@^5.7.2 to the customer-portal webapp to provide idle/user inactivity detection and include it in the project's dependencies.
Import vi from vitest and add a mock for @context/AuthApiContext in the test setup. The mock provides a stubbed useAuthApiClient that resolves to an empty 200 Response and an AuthApiProvider that simply returns children, preventing real authenticated network calls and simplifying testing of API hooks.
Replace BackgroundTokenRefresh with IdleTimeoutProvider in the customer portal AppLayout. The IdleTimeoutProvider import was added and the AppShell (including GlobalNotificationBanner and children) is now wrapped with it; the BackgroundTokenRefresh component and import were removed. This change enables idle timeout handling for the layout.
Add an explicit React import to the test file and replace the mocked BackgroundTokenRefresh with a passthrough mock for @providers/IdleTimeoutProvider (default export returning children). This avoids requiring auth/navigate or LoggerProvider during tests and updates the accompanying comment.
Introduce IdleTimeoutProvider that detects user idle time using react-idle-timer and displays a SessionWarningDialog before session timeout. The dialog allows users to continue (which resets the idle timer) or logout (which signs out via Asgardeo and navigates to /login). The provider respects mock mode (disabled when isMockEnabled) and uses auth constants (IDLE_TIMEOUT_MS, IDLE_PROMPT_BEFORE_MS, IDLE_THROTTLE_MS).
Delete apps/customer-portal/webapp/src/providers/BackgroundTokenRefresh.tsx which implemented an invisible component that periodically called getIdToken to keep the Asgardeo session alive and triggered sign-out after repeated failures. This removes the interval-based background token refresh logic and its related constants (TOKEN_REFRESH_INTERVAL_MS, FAILURE_THRESHOLD).
Adds apps/customer-portal/webapp/src/utils/apiClient.ts which exports authenticatedFetch and ApiClientCallbacks. The utility injects auth headers (via addApiHeaders), performs a fetch, and on 401 attempts a single token refresh (using the provided getToken), retries once with the new token, and calls signOut/throws on failure. Concurrent 401s are deduplicated by sharing a refresh promise so only one refresh runs at a time.
Add a new AuthApiContext.tsx that exports AuthApiProvider and useAuthApiClient. The provider wires Asgardeo's getIdToken and signOut into the project's authenticatedFetch via ApiClientCallbacks, exposing a typed authenticated fetch function (with 401 refresh/retry handling). Enforces usage inside AuthApiProvider (throws if used outside).
Introduce a new SessionWarningDialog React component (default export) that prompts the user with "Are you still there?" when idle. It accepts open, onContinue, and onLogout props and uses @wso2/oxygen-ui Dialog, Button, and Typography components. The dialog is accessible (aria-labelledby), fullWidth with maxWidth="sm", and includes styled dialog actions for Logout (outlined) and Continue (primary contained) buttons that invoke the provided callbacks.
Replace manual token/header handling with useAuthApiClient in useGetCaseAttachments and useGetCaseComments. Removed addApiHeaders and direct getIdToken usage; the hooks now call the fetch function from AuthApiContext to perform authenticated GET requests. Keeps existing request URLs and error handling while centralizing authentication logic.
Replace manual token/header handling with the centralized auth fetch client. Both useGetCaseDetails and useGetCasesFilters now import useAuthApiClient and call the returned fetchFn for GET requests instead of calling getIdToken and addApiHeaders. This simplifies the code by removing direct token retrieval and header assembly while keeping existing auth/loading checks and mock handling intact.
Replace direct token/header handling with the centralized auth API client in useGetChatHistory and useGetDeployments. Removed addApiHeaders and getIdToken usage, updated Asgardeo destructuring, and call useAuthApiClient() to perform authenticated fetches (fetchFn(requestUrl, { method: 'GET' })). This centralizes auth header management in AuthApiContext and simplifies these hooks.
Replace direct token/header management with a centralized auth-aware fetch function. Introduces a FetchFn type and switches fetchDeploymentProducts and useGetProductUpdateLevels to use useAuthApiClient() instead of getIdToken/addApiHeaders, simplifying request logic and keeping mock handling intact. Updated imports and function signatures accordingly across the two API hooks.
Replace manual token/header handling with useAuthApiClient. Import useAuthApiClient and create fetchFn to perform the request, remove getIdToken usage and addApiHeaders import, and simplify the fetch call to use fetchFn(requestUrl, { method: 'GET' }). This centralizes auth header/token logic in the auth API client and cleans up the hook implementation.
Replace manual token/header handling with the centralized auth API client. Updated useGetProductUpdatesStats and useGetProjectCases to import and use useAuthApiClient for network calls, removed addApiHeaders and getIdToken usage and corresponding header construction, and adjusted auth destructuring accordingly. This centralizes auth-aware fetch logic and simplifies the hooks' request code.
Replace direct idToken/header handling with a centralized auth fetch function in two hooks. Import useAuthApiClient and remove addApiHeaders/getIdToken usage in useGetProjectCasesStats and useGetProjectDeployments, switching fetch calls to use the returned fetchFn(requestUrl, { method: 'GET' }). Keeps existing auth state checks and mock behavior while centralizing auth header management.
Replace direct header handling and id token retrieval with the centralized AuthApiContext client. Updated useGetUpdatesStats and useGetUserDetails to use useAuthApiClient() for fetch requests, removed addApiHeaders import and getIdToken usage, and simplified auth-related destructuring. This centralizes auth header logic and reduces duplicated token handling across hooks.
Replace manual id token/header handling with the AuthApiContext client. Both useGetProjectDetails and useGetProjects now import useAuthApiClient and call the returned fetchFn instead of calling getIdToken and addApiHeaders; getIdToken was removed from asgardeo destructuring. This centralizes auth header logic in the auth API client and keeps mock behavior unchanged.
Replace manual token/header handling with the fetch function from AuthApiContext in usePostCaseClassifications and usePostComment. Removed addApiHeaders import and getIdToken calls, and now call fetchFn(requestUrl, { ... }) so the AuthApiContext provides auth headers; existing auth checks and mock behavior are preserved.
Replace direct idToken/header handling with the context-provided fetch client in two hooks. useAuthApiClient is imported and used as fetchFn in useGetProjectSupportStats and useGetProjectTimeTrackingStat, removing addApiHeaders and getIdToken usage and simplifying fetch calls to fetchFn(requestUrl, { method: "GET" }). This centralizes auth header management and keeps existing auth loading/sign-in checks intact.
Replace direct token/header handling in usePostCase and usePostAttachments with the shared fetch function from useAuthApiClient. Removed addApiHeaders import and getIdToken calls, and now call fetchFn(requestUrl, {...}) for POST requests. Keeps existing auth checks, mock config and logging; centralizes auth/header logic in the AuthApiContext.
Import and add AuthApiProvider in apps/customer-portal/webapp/src/App.tsx, wrapping the existing provider tree (LoaderProvider, ErrorBannerProvider, SuccessBannerProvider, Routes). This exposes the authentication API context to the app so auth-related hooks/components can access it globally.
Create apps/customer-portal/webapp/src/constants/authConstants.ts exporting idle-related constants: IDLE_TIMEOUT_MS (15 minutes), IDLE_PROMPT_BEFORE_MS (4000 ms) and IDLE_THROTTLE_MS (500 ms). File includes WSO2/Apache-2.0 license header and centralizes idle timer configuration for the customer-portal webapp.
@dileepapeiris dileepapeiris self-assigned this Feb 16, 2026
@coderabbitai

coderabbitai Bot commented Feb 16, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds an AuthApiProvider with an authenticated fetch client, replaces per-hook token/header logic with the new client, implements a Lexical rich-text editor (nodes, toolbar, plugins, tests), introduces idle-session timeout and session warning dialog, updates attachment flows, sanitization utils, and multiple UI/tests.

Changes

Cohort / File(s) Summary
Auth API client & helpers
apps/customer-portal/webapp/src/context/AuthApiContext.tsx, apps/customer-portal/webapp/src/utils/apiClient.ts
New AuthApiProvider and useAuthApiClient hook; authenticatedFetch with token fetching, silent sign-in, 401 refresh/retry and related types.
API hooks refactor
apps/customer-portal/webapp/src/api/*
~20 hooks updated to remove per-call getIdToken/addApiHeaders and use useAuthApiClient() → fetchFn(...); useGetDeploymentsProducts exports new FetchFn type.
Idle/session management
apps/customer-portal/webapp/src/providers/IdleTimeoutProvider.tsx, apps/customer-portal/webapp/src/components/common/SessionWarningDialog.tsx, apps/customer-portal/webapp/src/constants/authConstants.ts
New IdleTimeoutProvider using react-idle-timer, SessionWarningDialog component, and idle-related constants.
Background token refresh removal
apps/customer-portal/webapp/src/providers/BackgroundTokenRefresh.tsx
Removed BackgroundTokenRefresh component replaced by AuthApiProvider + IdleTimeoutProvider approach.
Rich text editor (components & plugins)
apps/customer-portal/webapp/src/components/common/rich-text-editor/Editor.tsx, .../ToolBar.tsx, .../ImagesPlugin.tsx, .../ImageNode.tsx
Adds Lexical-based Editor, Toolbar, ImageNode, ImagesPlugin, attachments UI and integration. Large new components and behavior.
Rich text editor utils & tests
apps/customer-portal/webapp/src/utils/richTextEditor.tsx, apps/customer-portal/webapp/src/components/common/rich-text-editor/__tests__/*
New editor utilities (deriveAltFromFilename, htmlToPlainText, sanitizeUrl, getFileIcon, scrollElement, INSERT_IMAGE_COMMAND) and unit tests for editor parts.
Removed old rich text utilities
apps/customer-portal/webapp/src/utils/richTextEditor.ts
Old richTextEditor.ts removed (markdown/html conversion and many formatting helpers).
Attachments & case creation
apps/customer-portal/webapp/src/pages/CreateCasePage.tsx, apps/customer-portal/webapp/src/components/support/case-details/attachments-tab/UploadAttachmentModal.tsx
CreateCasePage: attachment state/handlers and upload flow; UploadAttachmentModal: caseId optional and new onSelect callback to allow selection without immediate upload.
Case details & editing flow
apps/customer-portal/webapp/src/components/support/.../BasicInformationSection.tsx, .../CaseDetailsSection.tsx, .../CaseDetailsContent.tsx, .../CaseDetailsHeader.tsx, .../CaseDetailsActionRow.tsx
Introduces edit snapshot/revert flow, replaces plain description with rich Editor, adds attachment props, moves from isError branches to statusLabel-driven actions and rendering. Some public prop changes (CaseDetailsSection props, CaseDetailsActionRow/CaseDetailsHeader prop shapes).
Support utilities & constants
apps/customer-portal/webapp/src/utils/support.ts, apps/customer-portal/webapp/src/constants/supportConstants.ts
Adds stripHtml and getAvailableCaseActions, CaseStatus constants, MAX_IMAGE_SIZE_BYTES, and updates CASE_STATUS_ACTIONS; refactors several status/icon helpers.
UI sanitization updates
apps/customer-portal/webapp/src/components/support/all-cases/AllCasesList.tsx, .../support-overview-cards/OutstandingCasesList.tsx
Apply stripHtml to case titles/descriptions to sanitize rendered HTML.
App wiring & small infra changes
apps/customer-portal/webapp/src/layouts/AppLayout.tsx, apps/customer-portal/webapp/src/App.tsx, apps/customer-portal/webapp/src/main.tsx, apps/customer-portal/webapp/index.html
Wrap app with AuthApiProvider, add IdleTimeoutProvider, remove BackgroundTokenRefresh, expose Prism globally, and add dark/light favicons.
Tests & test setup
apps/customer-portal/webapp/src/vitest.setup.ts, apps/customer-portal/webapp/src/**/__tests__/*
Vitest mock for useAuthApiClient; many new/updated tests for editor, image node, toolbar, support utils, and adjusted tests for new props/behavior.
Dependencies
apps/customer-portal/webapp/package.json
Added Lexical packages, prismjs, react-idle-timer, and @types/prismjs.
Minor UX/text edits
apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx, other small files
Label and placeholder text changes (e.g., "Product Version", "Category") and small render tweaks.

Sequence Diagram(s)

sequenceDiagram
    participant Component as Client Component
    participant AuthCtx as AuthApiProvider
    participant AuthFetch as authenticatedFetch()
    participant Asgardeo
    participant API

    Component->>AuthCtx: useAuthApiClient() -> fetchFn
    Component->>AuthFetch: fetchFn(url, init)
    AuthFetch->>Asgardeo: getToken()
    alt token available
        Asgardeo-->>AuthFetch: token
        AuthFetch->>API: fetch(url, headers with token)
        alt 200
            API-->>AuthFetch: response
            AuthFetch-->>Component: response
        else 401
            AuthFetch->>Asgardeo: signInSilently()/refresh
            Asgardeo-->>AuthFetch: new token
            AuthFetch->>API: fetch(url, headers with new token)
            API-->>AuthFetch: response
            AuthFetch-->>Component: response
        end
    else token error
        AuthFetch->>Asgardeo: signInSilently()
        Asgardeo-->>AuthFetch: session restored / error
        AuthFetch->>API: fetch or signOut flow
        AuthFetch-->>Component: response or throw
    end
Loading
sequenceDiagram
    participant User
    participant IdleHook as useIdleTimer
    participant IdleProvider as IdleTimeoutProvider
    participant Dialog as SessionWarningDialog
    participant Auth as Asgardeo

    IdleHook->>IdleProvider: onPrompt()
    IdleProvider->>Dialog: open=true
    Dialog-->>User: show "Are you still there?"
    alt User continues
        User->>Dialog: Click Continue
        Dialog->>IdleProvider: onContinue()
        IdleProvider->>IdleHook: activate()
        IdleHook-->>User: timer reset
    else User logs out or times out
        User->>Dialog: Click Logout or idle expires
        Dialog->>IdleProvider: onLogout()
        IdleProvider->>Auth: signOut()
        IdleProvider-->>User: navigate /login
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

Type/Bug

Suggested reviewers

  • Rashmika998

Poem

🐰
I nibble bytes beneath the log,
Tokens snug inside my bog.
Editors bloom with images bright,
A gentle prompt before the night.
Hooray — the portal hops to light!

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (42 files):

⚔️ .coderabbit.yaml (content)
⚔️ apps/customer-portal/backend/Dependencies.toml (content)
⚔️ apps/customer-portal/backend/constants.bal (content)
⚔️ apps/customer-portal/backend/mock.bal (content)
⚔️ apps/customer-portal/backend/modules/updates/types.bal (content)
⚔️ apps/customer-portal/backend/modules/updates/utils.bal (content)
⚔️ apps/customer-portal/backend/service.bal (content)
⚔️ apps/customer-portal/backend/utils.bal (content)
⚔️ apps/customer-portal/webapp/package.json (content)
⚔️ apps/customer-portal/webapp/pnpm-lock.yaml (content)
⚔️ apps/customer-portal/webapp/src/App.tsx (content)
⚔️ apps/customer-portal/webapp/src/api/useGetCaseAttachments.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetCaseComments.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetCaseDetails.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetCasesFilters.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetChatHistory.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetDeployments.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetDeploymentsProducts.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProductUpdateLevels.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProductUpdatesStats.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProjectCases.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProjectCasesStats.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProjectDeployments.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProjectDetails.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProjectStat.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProjectSupportStats.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProjectTimeTrackingStat.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetProjects.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetUpdatesStats.ts (content)
⚔️ apps/customer-portal/webapp/src/api/useGetUserDetails.ts (content)
⚔️ apps/customer-portal/webapp/src/api/usePostAttachments.ts (content)
⚔️ apps/customer-portal/webapp/src/api/usePostCase.ts (content)
⚔️ apps/customer-portal/webapp/src/api/usePostCaseClassifications.ts (content)
⚔️ apps/customer-portal/webapp/src/api/usePostComment.ts (content)
⚔️ apps/customer-portal/webapp/src/components/common/rich-text-editor/ImageNode.tsx (content)
⚔️ apps/customer-portal/webapp/src/components/common/rich-text-editor/ToolBar.tsx (content)
⚔️ apps/customer-portal/webapp/src/components/support/case-creation-layout/form-sections/case-details-section/CaseDetailsSection.tsx (content)
⚔️ apps/customer-portal/webapp/src/layouts/AppLayout.tsx (content)
⚔️ apps/customer-portal/webapp/src/layouts/__tests__/AppLayout.test.tsx (content)
⚔️ apps/customer-portal/webapp/src/pages/CreateCasePage.tsx (content)
⚔️ apps/customer-portal/webapp/src/utils/richTextEditor.tsx (content)
⚔️ apps/customer-portal/webapp/src/vitest.setup.ts (content)

These conflicts must be resolved before merging into customer-portal-milestone-1.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: auth UX improvements (idle timeout), token refresh error handling, and upload cleanup.
Description check ✅ Passed The description covers the purpose, goals, and approach with specific file changes and implementation details aligned with the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix/m1-auth-issues
  • Post resolved changes as copyable diffs in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dileepapeiris dileepapeiris added Type/Improvement Marks enhancements or improvements to existing features Type/Task General task that does not fit into other categories App/Customer Portal Area/Frontend labels Feb 16, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR centralizes authenticated API access (including 401 refresh/retry behavior), replaces background token refreshing with an idle-timeout warning flow, and introduces a new Lexical-based rich text editor with attachment support for case creation.

Changes:

  • Added an AuthApiContext + authenticatedFetch utility and migrated many API hooks to use it.
  • Replaced BackgroundTokenRefresh with IdleTimeoutProvider + SessionWarningDialog.
  • Added a new Lexical rich text editor implementation and integrated it into case creation (including attachment selection/upload flow).

Reviewed changes

Copilot reviewed 66 out of 69 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/customer-portal/webapp/src/vitest.setup.ts Adds a global test mock for AuthApiContext to support new authenticated fetch usage in hooks.
apps/customer-portal/webapp/src/utils/support.ts Adds stripHtml and case-action derivation helper; refactors some status helpers.
apps/customer-portal/webapp/src/utils/richTextEditor.tsx Introduces new rich text editor utilities (URL sanitization, file icons, scroll helper, Lexical command).
apps/customer-portal/webapp/src/utils/richTextEditor.ts Removes the prior rich-text utility implementation (markdown/html helpers, etc.).
apps/customer-portal/webapp/src/utils/apiClient.ts Adds authenticatedFetch with 401 refresh/retry + refresh de-duping.
apps/customer-portal/webapp/src/utils/tests/support.test.ts Adds tests for stripHtml and getAvailableCaseActions.
apps/customer-portal/webapp/src/utils/tests/richTextEditor.test.ts Updates tests to match the new rich text editor utilities.
apps/customer-portal/webapp/src/providers/IdleTimeoutProvider.tsx New provider for idle session warning and logout.
apps/customer-portal/webapp/src/providers/BackgroundTokenRefresh.tsx Removes the periodic background token refresh component.
apps/customer-portal/webapp/src/pages/tests/AllCasesPage.test.tsx Minor test callback cleanup.
apps/customer-portal/webapp/src/pages/CreateCasePage.tsx Integrates rich text editor output + attachment selection and post-create attachment uploads.
apps/customer-portal/webapp/src/main.tsx Exposes Prism on window for syntax highlighting integration.
apps/customer-portal/webapp/src/layouts/tests/AppLayout.test.tsx Updates provider mocking to match new IdleTimeoutProvider.
apps/customer-portal/webapp/src/layouts/AppLayout.tsx Wraps layout in IdleTimeoutProvider (replacing background refresh).
apps/customer-portal/webapp/src/context/AuthApiContext.tsx New context providing useAuthApiClient() authenticated fetch function.
apps/customer-portal/webapp/src/constants/supportConstants.ts Adds CaseStatus, MAX_IMAGE_SIZE_BYTES, and updates case action definitions.
apps/customer-portal/webapp/src/constants/authConstants.ts Adds idle timeout configuration constants.
apps/customer-portal/webapp/src/components/support/support-overview-cards/tests/OutstandingCasesList.test.tsx Updates mocks to include stripHtml.
apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesList.tsx Uses stripHtml when rendering case titles.
apps/customer-portal/webapp/src/components/support/case-details/header/tests/CaseDetailsHeader.test.tsx Updates tests after removing header-level error indicators.
apps/customer-portal/webapp/src/components/support/case-details/header/tests/CaseDetailsActionRow.test.tsx Updates tests for new action filtering behavior by status.
apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsHeader.tsx Removes isError path and always renders value fields.
apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx Filters action buttons based on status-derived available actions.
apps/customer-portal/webapp/src/components/support/case-details/details-tab/CaseDetailsContent.tsx Adds a dedicated error header section (moves error UI responsibility up).
apps/customer-portal/webapp/src/components/support/case-details/attachments-tab/UploadAttachmentModal.tsx Makes caseId optional; supports “select only” mode for pre-create attachments.
apps/customer-portal/webapp/src/components/support/case-creation-layout/form-sections/case-details-section/CaseDetailsSection.tsx Replaces description TextField with the new Editor and wires attachment UI props.
apps/customer-portal/webapp/src/components/support/case-creation-layout/form-sections/basic-information-section/tests/BasicInformationSection.test.tsx Updates test expectations for new Save/Cancel editing behavior and label text.
apps/customer-portal/webapp/src/components/support/case-creation-layout/form-sections/basic-information-section/BasicInformationSection.tsx Adds Save/Cancel editing controls and cancel-revert snapshot behavior; updates label text.
apps/customer-portal/webapp/src/components/support/all-cases/tests/AllCasesList.test.tsx Updates mocks to include stripHtml.
apps/customer-portal/webapp/src/components/support/all-cases/AllCasesList.tsx Uses stripHtml when rendering descriptions.
apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx Renames “Case Type” column label to “Category”.
apps/customer-portal/webapp/src/components/common/rich-text-editor/tests/ToolBar.test.tsx Adds smoke tests for the new rich-text toolbar.
apps/customer-portal/webapp/src/components/common/rich-text-editor/tests/ImagesPlugin.test.tsx Adds smoke tests for the images plugin.
apps/customer-portal/webapp/src/components/common/rich-text-editor/tests/ImageNode.test.tsx Adds tests for ImageNode serialization/import/clone.
apps/customer-portal/webapp/src/components/common/rich-text-editor/tests/Editor.test.tsx Adds smoke tests for the new editor wrapper component.
apps/customer-portal/webapp/src/components/common/rich-text-editor/ToolBar.tsx Adds a full Lexical toolbar implementation (formatting, links, images, attachments).
apps/customer-portal/webapp/src/components/common/rich-text-editor/ImagesPlugin.tsx Adds Lexical command handler to insert images.
apps/customer-portal/webapp/src/components/common/rich-text-editor/ImageNode.tsx Adds a custom Lexical ImageNode with DOM import/export support.
apps/customer-portal/webapp/src/components/common/rich-text-editor/Editor.tsx Adds Lexical editor wrapper with HTML import/export and attachment display strip.
apps/customer-portal/webapp/src/components/common/SessionWarningDialog.tsx Adds dialog UI for idle session warning.
apps/customer-portal/webapp/src/api/usePostComment.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/usePostCaseClassifications.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/usePostCase.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/usePostAttachments.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetUserDetails.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetUpdatesStats.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProjects.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProjectTimeTrackingStat.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProjectSupportStats.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProjectStat.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProjectDetails.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProjectDeployments.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProjectCasesStats.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProjectCases.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProductUpdatesStats.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetProductUpdateLevels.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetDeploymentsProducts.ts Refactors fetch helper to accept fetchFn (from useAuthApiClient).
apps/customer-portal/webapp/src/api/useGetDeployments.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetChatHistory.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetCasesFilters.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetCaseDetails.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetCaseComments.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/api/useGetCaseAttachments.ts Migrates to useAuthApiClient.
apps/customer-portal/webapp/src/App.tsx Wraps app routes with AuthApiProvider.
apps/customer-portal/webapp/public/logo-white.svg Adds dark-mode favicon asset.
apps/customer-portal/webapp/public/logo-dark.svg Adds light-mode favicon asset.
apps/customer-portal/webapp/pnpm-lock.yaml Adds Lexical/Prism/react-idle-timer dependency lock entries.
apps/customer-portal/webapp/package.json Adds Lexical/Prism/react-idle-timer dependencies and Prism types.
apps/customer-portal/webapp/index.html Switches favicon to dark/light variants via media query.
Files not reviewed (1)
  • apps/customer-portal/webapp/pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/customer-portal/webapp/src/providers/IdleTimeoutProvider.tsx
Comment thread apps/customer-portal/webapp/src/vitest.setup.ts
Comment thread apps/customer-portal/webapp/src/pages/CreateCasePage.tsx
Comment thread apps/customer-portal/webapp/src/utils/apiClient.ts Outdated
Detects when getToken() fails because the user is not authenticated and attempts a silent sign-in (via an optional signInSilently callback) before retrying getToken. Adds ApiClientCallbacks.signInSilently, a NOT_AUTHENTICATED_CODE constant, isNotAuthenticatedError helper, and deduplicated silent sign-in logic (isSilentSignInRunning / silentSignInPromise). If silent recovery is unavailable or fails the code falls back to signOut and throws. Also updates JSDoc to reflect the new behavior.
Expose useAsgardeo's signInSilently in the AuthApiProvider: add signInSilently to the hook destructure, wrap it in a memoized signInSilentlyFn, include it in the ApiClientCallbacks passed to authenticatedFetch, and update the useMemo dependency array. This enables the API client to attempt silent sign-ins (e.g. token refresh) when needed.
Move the navigate(...) call in CreateCasePage.tsx into the finally block so routing occurs after setIsUploadingAttachments(false) runs. This ensures the uploading state is cleared before navigating away (preserving the success/warning behavior while preventing navigation during the upload state).
Replace vi.fn().mockResolvedValue(new Response(...)) with vi.fn().mockImplementation(() => ...) for the useAuthApiClient mock in vitest.setup.ts. This switches the mock to a customizable implementation so tests can provide dynamic behavior for authenticated API hooks. AuthApiProvider remains unchanged.
Await signOut when token refresh fails to ensure cleanup completes before proceeding. Preserve and propagate the original refresh error (using the error as-is or wrapping with a new Error that sets the cause) instead of throwing a generic message. Also update the vitest mock for useAuthApiClient to return a resolved Response promise so tests receive a Response object.
Add an onIdle handler to IdleTimeoutProvider that closes the session warning, signs the user out, and navigates to /login. The handler is only triggered when the user is signed in, not loading, and mocks are disabled. Register onIdle with useIdleTimer so idle expiration triggers the sign-out/redirect flow.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/customer-portal/webapp/src/components/support/all-cases/AllCasesList.tsx (1)

168-182: ⚠️ Potential issue | 🟡 Minor

caseItem.title is not passed through stripHtml, unlike in OutstandingCasesList.tsx.

The PR objective mentions stripping HTML from both case titles and descriptions. OutstandingCasesList.tsx (Line 130) applies stripHtml(c.title), but here the title is rendered raw while only the description is sanitized. Consider applying stripHtml to the title as well for consistency.

Proposed fix
-                {caseItem.title || "--"}
+                {stripHtml(caseItem.title) || "--"}
apps/customer-portal/webapp/src/components/support/case-details/details-tab/CaseDetailsContent.tsx (1)

127-177: ⚠️ Potential issue | 🟡 Minor

Tabs render and remain interactive even when the API request fails.

When isError is true, the error banner displays but CaseDetailsTabs (line 171) and CaseDetailsTabPanels (line 194) render unconditionally. While the Details tab panel gracefully shows an error state, the Activity and Attachments tabs do not check isError and may display incomplete or misleading placeholder content instead of signaling the load failure. Consider either hiding the tabs on error or ensuring all tab panels explicitly handle the error state (similar to how CaseDetailsDetailsPanel does).

🤖 Fix all issues with AI agents
In `@apps/customer-portal/webapp/package.json`:
- Line 32: The dependency entry "react-idle-timer": "^5.7.2" in package.json may
not have been tested with React 19; verify compatibility by running your app and
test suite under React 19, check the react-idle-timer repo/changelog for React
19 notes, and if issues appear either upgrade to a version that explicitly
supports React 19 or replace/patch the package; update package.json (the
"react-idle-timer" dependency) and lockfile accordingly and add a brief note in
docs or a CI compatibility test to prevent regressions.

In
`@apps/customer-portal/webapp/src/components/common/rich-text-editor/ImageNode.tsx`:
- Around line 79-88: Image src values are not validated before being rendered or
exported (in methods like exportDOM and decorate) which can allow unsafe
data/URLs; fix by sanitizing the URL using the existing sanitizeUrl utility from
`@utils/richTextEditor` before assigning to the img src or creating ImageNode
instances — update exportDOM (ImageNode.exportDOM) and decorate to call
sanitizeUrl(this.__src) and use the sanitized result, and ensure
importJSON/importDOM/$createImageNode either sanitize incoming src or call the
factory with an already-sanitized URL so the node never stores or emits raw,
unsanitized __src.

In
`@apps/customer-portal/webapp/src/components/common/rich-text-editor/ToolBar.tsx`:
- Around line 123-151: The updateToolbar callback can throw from
selection.anchor.getNode().getTopLevelElementOrThrow() when the anchor is
detached; wrap the top-level element retrieval in a defensive check/try-catch:
first verify selection and selection.anchor exist and that getNode() returns a
node, then attempt to get the top-level element using a non-throwing approach
(or call getTopLevelElementOrThrow() inside a try/catch) and if it fails fall
back to a safe default (e.g., treat as body1 and clear code/heading/quote
states); update references in updateToolbar, selection.anchor.getNode, and
getTopLevelElementOrThrow accordingly so an exception cannot propagate to the
command listener.

In
`@apps/customer-portal/webapp/src/components/support/case-creation-layout/form-sections/case-details-section/CaseDetailsSection.tsx`:
- Around line 129-137: The Cancel button currently only toggles isEditing via
setIsEditing(false) and does not revert edits; update the component to snapshot
the current case fields (title, description, issue type, severity) when entering
edit mode (the handler that sets isEditing true) and restore that snapshot when
Cancel is clicked. Implement a local ref or state (e.g., originalDetailsRef) to
store the original values on edit entry, change the Cancel onClick to restore
those values into the component state (e.g., title, description, issueType,
severity) and then setIsEditing(false), and ensure the same snapshot/restore
pattern used in BasicInformationSection is followed for consistency.

In `@apps/customer-portal/webapp/src/providers/IdleTimeoutProvider.tsx`:
- Around line 67-71: handleLogout currently calls signOut() without error
handling so if signOut throws the dialog is closed and navigate("/login") never
runs; wrap the signOut call in a try/catch inside handleLogout (referencing
handleLogout, signOut, setSessionWarningOpen, and navigate) and ensure
setSessionWarningOpen(false) still runs, then on success or on error always call
navigate("/login") (or a designated fallback route) in the finally or catch
block and log or surface the error as appropriate.
- Around line 49-60: The idle timer only sets onPrompt but lacks an onIdle
handler, so when the full timeout elapses the warning dialog remains open; add
an onIdle callback to the useIdleTimer call (alongside onPrompt) that closes the
warning (setSessionWarningOpen(false)) and triggers your logout/session
termination routine (e.g., call the existing signOut or session-expire handler),
and ensure any UI cleanup/navigation is performed so the user is actually signed
out when IDLE_TIMEOUT_MS is reached.

In `@apps/customer-portal/webapp/src/utils/apiClient.ts`:
- Around line 76-81: The forEach callback in doFetch implicitly returns the
result of headers.set, violating the lint rule; change the iteration to a plain
for…of loop over Object.entries(authHeaders) (e.g., for (const [k, v] of
Object.entries(authHeaders)) { headers.set(k, v); }) so headers.set is called
without returning a value—update doFetch to use headers, authHeaders and
addApiHeaders accordingly.
- Around line 27-30: The module-level dedupe flags/promises (isRefreshing,
refreshPromise, isSilentSignInRunning, silentSignInPromise) can race and leak
across React lifecycles; fix by capturing the module promise into a local
variable immediately after checking/creating it (e.g., const localSilentSignIn =
silentSignInPromise) and await that local variable instead of referencing the
module variable directly, and when clearing state in the .then/.catch/.finally
handlers only null/reset the module-level promise/flag if the module variable
still equals the captured local (e.g., if (silentSignInPromise ===
localSilentSignIn) { silentSignInPromise = null; isSilentSignInRunning = false;
}); apply the same pattern for refreshPromise/isRefreshing; additionally add or
export a lightweight reset function to explicitly clear flags
(isRefreshing/isSilentSignInRunning and promises) so callers can call it on
unmount or remount to avoid stale true flags.
- Around line 144-149: The .catch handler currently calls signOut()
fire-and-forget which lets the rejection propagate before sign-out finishes;
update the handler so it awaits signOut() (make the callback async) or return
signOut().then(() => { throw err; }) so signOut() completes before re-throwing;
keep the existing resets of isRefreshing = false and refreshPromise = null and
ensure the throw of err happens only after signOut() has finished.
- Around line 134-161: The 401 retry logic uses getToken() which may return the
same stale token; update the refresh flow to call signInSilently() before
getToken() so the SDK can refresh credentials: inside the isRefreshing guard
where refreshPromise is created (the block that sets refreshPromise using
getToken()), call and await signInSilently() first and then call getToken(),
propagate errors the same way (resetting isRefreshing and refreshPromise and
calling signOut() on failure), and continue using the resolved token for doFetch
and the existing 401/signOut handling.

In `@apps/customer-portal/webapp/src/utils/richTextEditor.tsx`:
- Around line 51-56: The htmlToPlainText function currently sets div.innerHTML
which can trigger resource loads or inline handlers; instead, use DOMParser to
parse the input string as "text/html" and extract the plain text from the
parsedDocument (e.g., parsed.documentElement or parsed.body). Replace the
detached div.innerHTML approach in htmlToPlainText with a DOMParser
parseFromString(html, "text/html") call and return the parsed document's
textContent trimmed, ensuring you still guard for non-string/empty inputs as
done now.
🧹 Nitpick comments (29)
apps/customer-portal/webapp/index.html (1)

5-16: Consider adding a fallback favicon for browsers that don't support media on <link rel="icon">.

Browsers that ignore the media attribute (some older versions of Safari/Firefox) will pick the first <link rel="icon"> they find, so logo-dark.svg acts as an implicit fallback. If you want to be explicit, you could add a plain <link rel="icon" href="/logo-dark.svg" type="image/svg+xml" /> without a media attribute as a baseline. This is a minor compatibility concern and the current setup is acceptable for modern browsers.

apps/customer-portal/webapp/src/context/AuthApiContext.tsx (1)

54-58: The useCallback wrappers around Asgardeo functions may be redundant.

getIdToken, signOut, and signInSilently from useAsgardeo are typically stable references already. These extra useCallback wrappers add indirection without changing referential stability. Not harmful, but could be simplified by passing them directly into the callbacks object.

apps/customer-portal/webapp/src/App.tsx (1)

40-116: Inconsistent JSX indentation after adding AuthApiProvider.

The <Routes> block (line 44) and its children are not properly indented relative to the wrapping <SuccessBannerProvider> (line 43). This makes the nesting hierarchy hard to follow visually. Consider re-indenting the JSX tree so each level is consistently indented.

apps/customer-portal/webapp/src/api/useGetDeploymentsProducts.ts (1)

26-31: Consider reusing the AuthenticatedFetchFn type from AuthApiContext instead of declaring a local FetchFn.

If the standalone fetchDeploymentProducts is intentionally decoupled from the auth context (e.g., for testability with any fetch-compatible function), the local type is fine. Otherwise, importing the shared type avoids drift.

#!/bin/bash
# Check if AuthApiContext already exports a compatible fetch type
rg -n "export.*type.*Fetch" --type=ts --type=tsx -g '*.ts' -g '*.tsx'
apps/customer-portal/webapp/src/utils/richTextEditor.tsx (1)

74-78: sanitizeUrl looks solid for the intended use case.

Blocks javascript:, data:, and protocol-relative URLs. One minor hardening thought: whitespace characters beyond what trim() handles (e.g., \t, \n, \0 embedded before the protocol) could bypass the ^ anchor. If this is used on editor-pasted URLs, consider stripping control characters before the regex test.

Optional hardening
 export function sanitizeUrl(url: string): string {
-  const decoded = url.replace(/&amp;/g, "&").trim();
+  const decoded = url.replace(/&amp;/g, "&").replace(/[\x00-\x1f\x7f]/g, "").trim();
   return SAFE_URL_PATTERN.test(decoded) ? decoded : "";
 }
apps/customer-portal/webapp/src/utils/__tests__/richTextEditor.test.ts (3)

29-195: Missing test coverage for htmlToPlainText.

htmlToPlainText is exported from richTextEditor.tsx but has no tests in this suite. Given it handles potentially untrusted HTML (and has the XSS consideration noted in the implementation review), adding tests would be valuable — especially for edge cases like empty strings, nested tags, and HTML entities.


109-150: getFileIcon tests only assert that an SVG renders, not which icon type.

Each test case creates a different file type but all assertions are identical (expect(container.querySelector("svg")).toBeInTheDocument()). This wouldn't catch a regression where, say, a PDF file returns a FileArchive icon instead of FileText. Consider snapshot testing or checking a distinguishing attribute/class on the SVG.


152-187: scrollElement ref overload not tested.

The implementation accepts string | RefObject<HTMLElement | null>, but only the string-based path is tested. Consider adding a test case passing a RefObject to cover the other branch.

apps/customer-portal/webapp/src/components/common/rich-text-editor/__tests__/ImageNode.test.tsx (2)

61-73: All checks collapsed into a single boolean makes failures hard to diagnose.

If any individual check fails, the test reports only passed: false with no indication of which assertion failed. Consider using individual expect calls for each property so the failure message pinpoints the problem.


94-94: Inline arrow creates a new reference on every render, re-triggering the useEffect.

The runTest prop is (r) => results.push(r), which is a new function each render. Since runTest is in the useEffect dependency array (Line 75), the effect re-runs on every render cycle, pushing duplicate entries into results. The test still passes because it only checks results[0]?.passed, but this is unintended re-execution.

Wrap the callback with useCallback or use a ref to stabilize it.

apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsHeader.test.tsx (1)

33-41: Stale isError prop in defaultProps.

Per the summary, CaseDetailsHeaderProps no longer includes isError. This leftover in test defaults is harmless but misleading. Consider removing it.

Proposed fix
   const defaultProps = {
     caseNumber: "CUPRSUB-101",
     title: "Test case title",
     severityLabel: "S1",
     statusLabel: "Open",
     statusChipIcon: <span data-testid="status-chip-icon" />,
     statusChipSx: {},
-    isError: false,
   };
apps/customer-portal/webapp/src/components/common/rich-text-editor/__tests__/ImagesPlugin.test.tsx (1)

49-54: Consider adding a test for INSERT_IMAGE_COMMAND dispatch.

The current test only verifies the plugin renders without throwing. Testing that dispatching INSERT_IMAGE_COMMAND actually inserts an ImageNode into the editor state would exercise the plugin's core behavior and catch regressions.

apps/customer-portal/webapp/src/constants/authConstants.ts (1)

17-24: Consider whether 4 seconds is sufficient for the session warning prompt.

IDLE_PROMPT_BEFORE_MS = 4_000 gives users only 4 seconds to respond to the "Are you still there?" dialog before the idle timeout fires. This is a very tight window — users may not notice the dialog or react in time. A more typical value would be 30–60 seconds. If this is intentional, please disregard.

apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsActionRow.test.tsx (1)

22-26: Unused ErrorIndicator mock — can be removed.

The CaseDetailsActionRow component no longer imports or renders ErrorIndicator (per the AI summary and the removal of isError), so this mock is dead code.

🧹 Remove unused mock
-vi.mock("@components/common/error-indicator/ErrorIndicator", () => ({
-  default: ({ entityName }: { entityName: string }) => (
-    <span data-testid="error-indicator">{entityName}</span>
-  ),
-}));
-
apps/customer-portal/webapp/src/components/support/case-creation-layout/form-sections/basic-information-section/BasicInformationSection.tsx (1)

148-155: disabled={isEditing} is always false in this branch.

This code is inside the : ( (else) branch of isEditing ? … : …, so isEditing is guaranteed to be false here. The prop is harmless but redundant.

🔧 Suggested cleanup
               <IconButton
                 onClick={() => setIsEditing(true)}
                 aria-label="Edit basic information"
-                disabled={isEditing}
               >
apps/customer-portal/webapp/src/components/support/case-creation-layout/form-sections/case-details-section/CaseDetailsSection.tsx (2)

93-101: Duplicate type-narrowing logic for issueTypes.

The typeof type === "string" ? type : (type as { label?: string }).label pattern appears twice — once in the filter (Line 93) and again in the map (Line 267). Consider extracting a small helper to DRY this up.

♻️ Example helper
+const getIssueTypeLabel = (type: unknown): string | undefined =>
+  typeof type === "string" ? type : (type as { label?: string }).label;
+

Then replace both inline narrowings with getIssueTypeLabel(type).

Also applies to: 266-284


144-151: disabled={isEditing} is always false in the else branch.

Same redundant prop as in BasicInformationSection — this code is inside the !isEditing path.

apps/customer-portal/webapp/src/components/support/case-details/details-tab/CaseDetailsContent.tsx (1)

151-166: isLoading is always false here due to the early return on Line 91.

The loading guard at Line 91 ensures that execution never reaches this point while isLoading is true. Passing isLoading to CaseDetailsHeader and CaseDetailsActionRow is a no-op. Consider removing these props or passing a literal false to make the invariant explicit.

apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx (1)

68-72: Stale JSDoc: still references "error state" but isError was removed.

The @param description mentions "error state" but the component no longer accepts or handles an isError prop.

🔧 Suggested fix
 /**
  * Support action row: avatar, name, "Support Engineer" label, and "Manage case status" actions.
  *
- * `@param` {CaseDetailsActionRowProps} props - Action display data and error state.
+ * `@param` {CaseDetailsActionRowProps} props - Action display data and loading state.
  * `@returns` {JSX.Element} The action row wrapped in Paper.
  */
apps/customer-portal/webapp/src/components/common/rich-text-editor/Editor.tsx (2)

161-170: disabled in useMemo deps is misleading — initialConfig is only read on mount.

LexicalComposer uses initialConfig only at mount time; subsequent changes are ignored. Runtime editability is already handled by EditableStatePlugin. Including disabled in the deps array causes unnecessary config object recreation with no effect.

♻️ Suggested cleanup
   const memoizedEditorConfig = useMemo(
     () => ({
       ...DEFAULT_EDITOR_CONFIG,
       onError: (error: Error) => {
         logger.error("Error occurred in rich text editor", error);
       },
-      editable: !disabled,
+      editable: true,
     }),
-    [logger, disabled],
+    [logger],
   );

95-112: $generateHtmlFromNodes on every editor change could become expensive.

This fires HTML serialization on every keystroke. For a support case description this is likely fine, but if the editor is reused for larger content, consider debouncing the onChange callback.

apps/customer-portal/webapp/src/utils/support.ts (1)

477-500: Fragile coupling: hardcoded action label strings must match CASE_STATUS_ACTIONS entries.

The returned arrays (e.g., "Closed", "Accept Solution") must exactly match the label field of CASE_STATUS_ACTIONS. If a label is renamed in the constant, this function silently returns labels that match nothing, and no action buttons appear. Consider importing the action labels from CASE_STATUS_ACTIONS or co-locating these mappings to keep them in sync.

#!/bin/bash
# Verify the labels used here exist in CASE_STATUS_ACTIONS
fd "supportConstants.ts" --exec rg -n "label:" {}
apps/customer-portal/webapp/src/pages/CreateCasePage.tsx (3)

255-322: async callback with mutate is not awaited — potential unmounted-state-update risk.

React Query's mutate does not await the onSuccess callback. The attachment upload runs as a detached async operation. If the user navigates away during uploads, setIsUploadingAttachments(false) may fire against an unmounted component. Consider using mutateAsync with a surrounding try/catch to keep the async flow within the handleSubmit scope, or use an AbortController / cleanup mechanism.

♻️ Sketch using mutateAsync
-    postCase(payload, {
-      onSuccess: async (data) => {
-        const caseId = data.id;
-        // ... attachment upload logic ...
-      },
-      onError: () => {
-        showError("We couldn't create your case. Please try again.");
-      },
-    });
+    try {
+      const data = await postCase.mutateAsync(payload);
+      const caseId = data.id;
+      // ... attachment upload logic (unchanged) ...
+    } catch {
+      showError("We couldn't create your case. Please try again.");
+    }

261-295: All attachment uploads fire in parallel — consider concurrency limits.

FileReader.readAsDataURL + postAttachments.mutateAsync is called for every attachment simultaneously. For a handful of files this is fine, but if users attach many files, this could overwhelm the server or hit rate limits. A simple sequential loop or batching (e.g., p-limit) would be more resilient.


78-78: Local type alias inside the component body.

AttachmentItem is defined inside the function body. Moving it to module scope (or a shared types file) improves readability and reusability.

apps/customer-portal/webapp/src/components/support/case-details/attachments-tab/UploadAttachmentModal.tsx (1)

46-51: JSDoc is missing the new onSelect prop.

The @param description still only lists open, caseId, onClose, optional onSuccess.

📝 Suggested doc update
 /**
  * Modal for uploading a case attachment: drag-and-drop or file picker, optional name (defaults to file name).
  * Upload is disabled when isMockEnabled. Max file size 15 MB; shows ErrorBanner when exceeded.
  *
- * `@param` {UploadAttachmentModalProps} props - open, caseId, onClose, optional onSuccess.
+ * `@param` {UploadAttachmentModalProps} props - open, caseId, onClose, optional onSuccess, optional onSelect.
  * `@returns` {JSX.Element} The upload attachment modal.
  */
apps/customer-portal/webapp/src/components/common/rich-text-editor/ToolBar.tsx (2)

286-312: Link popover: URL/text state persists after closing without submitting, and Enter key doesn't submit.

If the user opens the link popover, types a URL, and then closes the popover (clicking outside), linkUrl and linkText are not cleared. This stale state reappears on the next open. Additionally, pressing Enter in the URL field doesn't submit the link — only the button works.

♻️ Clear state on close and add Enter-key handling
           onClose={() => setLinkAnchorEl(null)}
+          onClose={() => {
+            setLinkAnchorEl(null);
+            setLinkUrl("");
+            setLinkText("");
+          }}

Add onKeyDown to the URL TextField:

               <TextField
                 label="URL"
                 size="small"
                 fullWidth
                 value={linkUrl}
                 onChange={(e) => setLinkUrl(e.target.value)}
                 autoFocus
+                onKeyDown={(e) => {
+                  if (e.key === "Enter" && linkUrl) {
+                    e.preventDefault();
+                    onLinkSubmit();
+                  }
+                }}
                 slotProps={{

245-272: onBlockChange sets blockVariant eagerly before the editor update.

Line 247 calls setBlockVariant(variant) before the editor.update() call on line 248. If the editor update fails or the variant doesn't apply as expected, the React state and editor state would be out of sync. The updateToolbar callback (triggered by the editor's update listener) will eventually reconcile this, so in practice it self-corrects. Noting this for awareness — not blocking.

apps/customer-portal/webapp/src/constants/supportConstants.ts (1)

238-244: CASE_STATUS_ACTIONS labels are string literals — consider referencing CaseStatus constants where applicable.

"Closed" and "Waiting on WSO2" appear both here and in the new CaseStatus enum. Using CaseStatus.CLOSED and CaseStatus.WAITING_ON_WSO2 would tie them together and prevent drift.

♻️ Example
 export const CASE_STATUS_ACTIONS: CaseStatusAction[] = [
-  { label: "Closed", Icon: CircleX, paletteIntent: "info" },
-  { label: "Waiting on WSO2", Icon: CirclePause, paletteIntent: "warning" },
+  { label: CaseStatus.CLOSED, Icon: CircleX, paletteIntent: "info" },
+  { label: CaseStatus.WAITING_ON_WSO2, Icon: CirclePause, paletteIntent: "warning" },
   { label: "Accept Solution", Icon: CircleCheck, paletteIntent: "success" },
   { label: "Reject Solution", Icon: TriangleAlert, paletteIntent: "error" },
   { label: "Open Related Case", Icon: RotateCcw, paletteIntent: "info" },
 ];

Comment thread apps/customer-portal/webapp/package.json
Comment thread apps/customer-portal/webapp/src/providers/IdleTimeoutProvider.tsx
Comment thread apps/customer-portal/webapp/src/utils/apiClient.ts
Comment thread apps/customer-portal/webapp/src/utils/apiClient.ts
Comment thread apps/customer-portal/webapp/src/utils/apiClient.ts
Comment thread apps/customer-portal/webapp/src/utils/apiClient.ts Outdated
Comment thread apps/customer-portal/webapp/src/utils/richTextEditor.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/customer-portal/webapp/src/providers/IdleTimeoutProvider.tsx`:
- Around line 55-60: The onIdle handler currently calls signOut().then(() =>
navigate("/login")) after setSessionWarningOpen(false) which leaves the user
stranded if signOut rejects; update onIdle to ensure navigation always occurs by
attaching a .finally(() => navigate("/login")) or adding a .catch(() =>
navigate("/login")) after signOut, and keep setSessionWarningOpen(false) as-is
so the dialog is closed before navigating; reference the onIdle function,
signOut call, setSessionWarningOpen, and navigate to locate and modify the code.
🧹 Nitpick comments (3)
apps/customer-portal/webapp/src/pages/CreateCasePage.tsx (3)

78-78: Move the AttachmentItem type outside the component.

Defining a type inside the function body re-evaluates it every render and clutters the component. Hoist it to module scope.

Proposed fix
+type AttachmentItem = { id: string; file: File };
+
 export default function CreateCasePage(): JSX.Element {
   // ...
-  type AttachmentItem = { id: string; file: File };
   const [attachments, setAttachments] = useState<AttachmentItem[]>([]);

255-321: Async onSuccess callback in mutate() creates a detached promise.

React Query's mutate() does not await its onSuccess callback, so the entire attachment-upload flow runs as a floating promise. If an unexpected error escapes the try/catch, it becomes an unhandled rejection. Consider using mutateAsync instead so you control the full lifecycle:

Sketch
-    postCase(payload, {
-      onSuccess: async (data) => {
+    try {
+      const data = await postCase.mutateAsync(payload);
         const caseId = data.id;
         // ... attachment upload logic ...
-      },
-      onError: () => {
-        showError("We couldn't create your case. Please try again.");
-      },
-    });
+    } catch {
+      showError("We couldn't create your case. Please try again.");
+    }

This also avoids the subtle issue where isCreatePending might flip to false before the attachment uploads finish, since mutate considers its job done once onSuccess is invoked (not when the async callback resolves).


260-294: Large files will be fully loaded into memory as base64 (≈33% overhead).

FileReader.readAsDataURL loads the entire file into a base64 string. For large attachments this doubles memory usage. If the API supports multipart/form-data uploads, that would be more efficient. If base64 is required, consider adding a client-side file-size limit to prevent OOM on large files.

Comment thread apps/customer-portal/webapp/src/providers/IdleTimeoutProvider.tsx
@dileepapeiris dileepapeiris changed the title Fix/m1 auth issues [Customer Portal][FE][Web] fix: Auth UX – idle timeout, token refresh error handling, upload cleanup Feb 16, 2026
Use finally/try...finally when calling signOut so navigate("/login") always runs even if signOut rejects (IdleTimeoutProvider). Replace Object.entries(...).forEach with a for..of loop when setting request headers for clearer iteration (apiClient). Parse HTML with DOMParser and use body/documentElement textContent instead of creating a div and using innerHTML/innerText to more robustly extract plain text (richTextEditor).
ImageNode: Use sanitizeUrl for image sources everywhere (importJSON, exportDOM, decorate, conversion, and $createImageNode) to ensure unsafe URLs are normalized/blocked and alt text derivation preserves the raw filename.

ToolBar: Wrap selection anchor element retrieval in a try/catch to prevent crashes when getTopLevelElementOrThrow fails; on error the toolbar falls back to default block variant and clears code state.

CaseDetailsSection: Add originalDetailsRef plus handleEditStart/handleCancel to capture original case fields when editing and restore them if the user cancels, improving the edit UX.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@apps/customer-portal/webapp/src/components/common/rich-text-editor/ToolBar.tsx`:
- Around line 293-319: onLinkSubmit currently treats a falsy sanitized URL as
success and always closes the popover and clears inputs; change it so that when
sanitizeUrl(linkUrl) returns an empty string you set an explicit validation
error and keep the popover open instead of clearing fields. Concretely, in
onLinkSubmit use the sanitized result and if (!sanitized) set a local error
state (e.g. linkUrlError) or call a provided setError handler, do not call
setLinkAnchorEl(null)/setLinkUrl("")/setLinkText(""), and return early; only run
editor.update and clear fields when sanitized is truthy and the
TOGGLE_LINK_COMMAND is dispatched successfully. Ensure the error state is used
by the popover UI to display a message so the user can correct the URL.
🧹 Nitpick comments (5)
apps/customer-portal/webapp/src/components/support/case-creation-layout/form-sections/case-details-section/CaseDetailsSection.tsx (1)

167-171: disabled={isEditing} is always false here.

This IconButton only renders in the !isEditing branch (line 161), so disabled={isEditing} is always false — it's dead code.

Proposed fix
              <IconButton
                onClick={handleEditStart}
                aria-label="Edit case details"
-               disabled={isEditing}
              >
apps/customer-portal/webapp/src/providers/IdleTimeoutProvider.tsx (1)

49-68: Stale closure risk with onPrompt/onIdle callbacks.

onPrompt and onIdle close over isSignedIn, isLoading, and isMockEnabled but are recreated every render without memoization. Whether useIdleTimer picks up the latest references on each render depends on its internal implementation — most versions do, but it's worth being aware of. If you ever notice the guards misfiring (e.g., dialog appearing after sign-out), wrapping these in useCallback with proper deps or using refs for the latest values would fix it.

apps/customer-portal/webapp/src/components/common/rich-text-editor/ToolBar.tsx (3)

329-345: Scroll-state check may not fire when toolbar content changes size.

The useEffect listens to scroll and resize events, but if the toolbar's scrollable content width changes (e.g., buttons become visible/hidden due to state changes), canScrollLeft/canScrollRight won't update until the next scroll or resize. A ResizeObserver on the scrollable container would cover this case.


252-279: Optimistic setBlockVariant before the editor update succeeds.

Line 254 sets blockVariant eagerly, but if the editor.update() callback doesn't run as expected (e.g., editor is read-only), the state will be out of sync until the next updateToolbar call. This is low-risk since updateToolbar will re-sync on the next selection change, but worth noting.


206-237: Image upload relies solely on the accept attribute for type filtering.

The accept="image/*" attribute is a browser hint and can be bypassed. If non-image files being read as data URLs and inserted into the editor would cause problems downstream (e.g., during export or rendering), consider also checking file.type.startsWith("image/") in onImageUpload.

@Rashmika998 Rashmika998 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

App/Customer Portal Area/Frontend Platform/Web Type/Improvement Marks enhancements or improvements to existing features Type/Task General task that does not fit into other categories

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants