Repository navigation
[Customer Portal][FE][Web] fix: Auth UX – idle timeout, token refresh error handling, upload cleanup - #158
Conversation
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.
📝 WalkthroughWalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (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.
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+authenticatedFetchutility and migrated many API hooks to use it. - Replaced
BackgroundTokenRefreshwithIdleTimeoutProvider+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.
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.
There was a problem hiding this comment.
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.titleis not passed throughstripHtml, unlike inOutstandingCasesList.tsx.The PR objective mentions stripping HTML from both case titles and descriptions.
OutstandingCasesList.tsx(Line 130) appliesstripHtml(c.title), but here the title is rendered raw while only the description is sanitized. Consider applyingstripHtmlto 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 | 🟡 MinorTabs render and remain interactive even when the API request fails.
When
isErroristrue, the error banner displays butCaseDetailsTabs(line 171) andCaseDetailsTabPanels(line 194) render unconditionally. While the Details tab panel gracefully shows an error state, the Activity and Attachments tabs do not checkisErrorand 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 howCaseDetailsDetailsPaneldoes).
🤖 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 supportmediaon<link rel="icon">.Browsers that ignore the
mediaattribute (some older versions of Safari/Firefox) will pick the first<link rel="icon">they find, sologo-dark.svgacts 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 amediaattribute 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: TheuseCallbackwrappers around Asgardeo functions may be redundant.
getIdToken,signOut, andsignInSilentlyfromuseAsgardeoare typically stable references already. These extrauseCallbackwrappers add indirection without changing referential stability. Not harmful, but could be simplified by passing them directly into thecallbacksobject.apps/customer-portal/webapp/src/App.tsx (1)
40-116: Inconsistent JSX indentation after addingAuthApiProvider.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 theAuthenticatedFetchFntype fromAuthApiContextinstead of declaring a localFetchFn.If the standalone
fetchDeploymentProductsis 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:sanitizeUrllooks solid for the intended use case.Blocks
javascript:,data:, and protocol-relative URLs. One minor hardening thought: whitespace characters beyond whattrim()handles (e.g.,\t,\n,\0embedded 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(/&/g, "&").trim(); + const decoded = url.replace(/&/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 forhtmlToPlainText.
htmlToPlainTextis exported fromrichTextEditor.tsxbut 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:getFileIcontests 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 aFileArchiveicon instead ofFileText. Consider snapshot testing or checking a distinguishing attribute/class on the SVG.
152-187:scrollElementref overload not tested.The implementation accepts
string | RefObject<HTMLElement | null>, but only the string-based path is tested. Consider adding a test case passing aRefObjectto 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: falsewith no indication of which assertion failed. Consider using individualexpectcalls for each property so the failure message pinpoints the problem.
94-94: Inline arrow creates a new reference on every render, re-triggering theuseEffect.The
runTestprop is(r) => results.push(r), which is a new function each render. SincerunTestis in theuseEffectdependency array (Line 75), the effect re-runs on every render cycle, pushing duplicate entries intoresults. The test still passes because it only checksresults[0]?.passed, but this is unintended re-execution.Wrap the callback with
useCallbackor use arefto stabilize it.apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsHeader.test.tsx (1)
33-41: StaleisErrorprop indefaultProps.Per the summary,
CaseDetailsHeaderPropsno longer includesisError. 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 forINSERT_IMAGE_COMMANDdispatch.The current test only verifies the plugin renders without throwing. Testing that dispatching
INSERT_IMAGE_COMMANDactually inserts anImageNodeinto 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_000gives 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: UnusedErrorIndicatormock — can be removed.The
CaseDetailsActionRowcomponent no longer imports or rendersErrorIndicator(per the AI summary and the removal ofisError), 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 alwaysfalsein this branch.This code is inside the
: ((else) branch ofisEditing ? … : …, soisEditingis guaranteed to befalsehere. 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 forissueTypes.The
typeof type === "string" ? type : (type as { label?: string }).labelpattern 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 alwaysfalsein the else branch.Same redundant prop as in
BasicInformationSection— this code is inside the!isEditingpath.apps/customer-portal/webapp/src/components/support/case-details/details-tab/CaseDetailsContent.tsx (1)
151-166:isLoadingis alwaysfalsehere due to the early return on Line 91.The loading guard at Line 91 ensures that execution never reaches this point while
isLoadingistrue. PassingisLoadingtoCaseDetailsHeaderandCaseDetailsActionRowis a no-op. Consider removing these props or passing a literalfalseto 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" butisErrorwas removed.The
@paramdescription mentions "error state" but the component no longer accepts or handles anisErrorprop.🔧 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:disabledinuseMemodeps is misleading —initialConfigis only read on mount.
LexicalComposerusesinitialConfigonly at mount time; subsequent changes are ignored. Runtime editability is already handled byEditableStatePlugin. Includingdisabledin 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:$generateHtmlFromNodeson 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
onChangecallback.apps/customer-portal/webapp/src/utils/support.ts (1)
477-500: Fragile coupling: hardcoded action label strings must matchCASE_STATUS_ACTIONSentries.The returned arrays (e.g.,
"Closed","Accept Solution") must exactly match thelabelfield ofCASE_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 fromCASE_STATUS_ACTIONSor 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:asynccallback withmutateis not awaited — potential unmounted-state-update risk.React Query's
mutatedoes not await theonSuccesscallback. 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 usingmutateAsyncwith a surroundingtry/catchto keep the async flow within thehandleSubmitscope, or use anAbortController/ 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.mutateAsyncis 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.
AttachmentItemis 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 newonSelectprop.The
@paramdescription still only listsopen, 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),
linkUrlandlinkTextare 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
onKeyDownto the URLTextField:<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:onBlockChangesetsblockVarianteagerly before the editor update.Line 247 calls
setBlockVariant(variant)before theeditor.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. TheupdateToolbarcallback (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_ACTIONSlabels are string literals — consider referencingCaseStatusconstants where applicable.
"Closed"and"Waiting on WSO2"appear both here and in the newCaseStatusenum. UsingCaseStatus.CLOSEDandCaseStatus.WAITING_ON_WSO2would 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" }, ];
There was a problem hiding this comment.
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 theAttachmentItemtype 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: AsynconSuccesscallback inmutate()creates a detached promise.React Query's
mutate()does notawaititsonSuccesscallback, so the entire attachment-upload flow runs as a floating promise. If an unexpected error escapes thetry/catch, it becomes an unhandled rejection. Consider usingmutateAsyncinstead 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
isCreatePendingmight flip tofalsebefore the attachment uploads finish, sincemutateconsiders its job done onceonSuccessis invoked (not when the async callback resolves).
260-294: Large files will be fully loaded into memory as base64 (≈33% overhead).
FileReader.readAsDataURLloads 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.
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.
There was a problem hiding this comment.
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 alwaysfalsehere.This
IconButtononly renders in the!isEditingbranch (line 161), sodisabled={isEditing}is alwaysfalse— 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 withonPrompt/onIdlecallbacks.
onPromptandonIdleclose overisSignedIn,isLoading, andisMockEnabledbut are recreated every render without memoization. WhetheruseIdleTimerpicks 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 inuseCallbackwith 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
useEffectlistens toscrollandresizeevents, but if the toolbar's scrollable content width changes (e.g., buttons become visible/hidden due to state changes),canScrollLeft/canScrollRightwon't update until the next scroll or resize. AResizeObserveron the scrollable container would cover this case.
252-279: OptimisticsetBlockVariantbefore the editor update succeeds.Line 254 sets
blockVarianteagerly, but if theeditor.update()callback doesn't run as expected (e.g., editor is read-only), the state will be out of sync until the nextupdateToolbarcall. This is low-risk sinceupdateToolbarwill re-sync on the next selection change, but worth noting.
206-237: Image upload relies solely on theacceptattribute 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 checkingfile.type.startsWith("image/")inonImageUpload.
a8f6f1b
into
wso2-open-operations:customer-portal-milestone-1
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.tsxonIdlehandler: when the user ignores the "Are you still there?" prompt, sign out and redirect to/loginwhen the idle timeout is reached.API client
src/utils/apiClient.tsawait signOut()in thecatchblock and rethrow the original error (or wrap withcause) instead of replacing it with"Token refresh failed".Test setup
src/vitest.setup.tsuseAuthApiClientwithmockImplementation()so each call gets a newResponseinstance instead of a reused one (response bodies can only be consumed once).UI flow
src/pages/CreateCasePage.tsxsetIsUploadingAttachments(false)in thefinallyblock beforenavigate(), so state is reset while the component is still mounted and avoids setState on an unmounted component.Files changed
src/providers/IdleTimeoutProvider.tsxonIdlehandler for forced sign-outsrc/utils/apiClient.tssignOut, preserve original error in refresh flowsrc/vitest.setup.tsResponseper mock callsrc/pages/CreateCasePage.tsxSummary by CodeRabbit
Release Notes
New Features
Improvements
Style