From 330922d6421434ca5875dd323d8e09f4134258cc Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:28:31 +0530 Subject: [PATCH 01/43] Add @case-details alias to Vite config Introduce a new path alias "@case-details" in apps/customer-portal/webapp/vite.config.ts that maps to ./src/components/support/case-details/header. This simplifies imports for case details header components and keeps import paths consistent across the codebase. --- apps/customer-portal/webapp/vite.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/customer-portal/webapp/vite.config.ts b/apps/customer-portal/webapp/vite.config.ts index 981ee8af6c..28de26e5ea 100644 --- a/apps/customer-portal/webapp/vite.config.ts +++ b/apps/customer-portal/webapp/vite.config.ts @@ -34,6 +34,9 @@ export default defineConfig({ "@components": fileURLToPath( new URL("./src/components", import.meta.url), ), + "@case-details": fileURLToPath( + new URL("./src/components/support/case-details/header", import.meta.url), + ), "@config": fileURLToPath(new URL("./src/config", import.meta.url)), "@constants": fileURLToPath(new URL("./src/constants", import.meta.url)), "@context": fileURLToPath(new URL("./src/context", import.meta.url)), From 0e6733b45628a81fe4cd8c82090c436fa70d79b3 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:28:50 +0530 Subject: [PATCH 02/43] Format renderHook invocation in test Reformat the renderHook call in apps/customer-portal/webapp/src/api/__tests__/useGetCaseAttachments.test.tsx to a single-line invocation with the wrapper option on the following line. This is a purely stylistic change and does not alter test behavior. --- .../src/api/__tests__/useGetCaseAttachments.test.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/customer-portal/webapp/src/api/__tests__/useGetCaseAttachments.test.tsx b/apps/customer-portal/webapp/src/api/__tests__/useGetCaseAttachments.test.tsx index 13958b8c1f..7652127905 100644 --- a/apps/customer-portal/webapp/src/api/__tests__/useGetCaseAttachments.test.tsx +++ b/apps/customer-portal/webapp/src/api/__tests__/useGetCaseAttachments.test.tsx @@ -63,10 +63,9 @@ describe("useGetCaseAttachments", () => { }); it("should return mock attachments when isMockEnabled is true", async () => { - const { result } = renderHook( - () => useGetCaseAttachments("case-001"), - { wrapper }, - ); + const { result } = renderHook(() => useGetCaseAttachments("case-001"), { + wrapper, + }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); From 2f21b54c8cc06b1297d3f9e21abf29f6b8b011d8 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:29:13 +0530 Subject: [PATCH 03/43] Refactor test formatting and type annotations Adjust formatting in useGetProductUpdateLevels tests: expand the importOriginal type annotation to a multi-line type and break up the window.config type casts across multiple lines. These are non-functional, readability/linting-oriented changes. --- .../useGetProductUpdateLevels.test.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/customer-portal/webapp/src/api/__tests__/useGetProductUpdateLevels.test.tsx b/apps/customer-portal/webapp/src/api/__tests__/useGetProductUpdateLevels.test.tsx index 5ecc6e7f64..7961457432 100644 --- a/apps/customer-portal/webapp/src/api/__tests__/useGetProductUpdateLevels.test.tsx +++ b/apps/customer-portal/webapp/src/api/__tests__/useGetProductUpdateLevels.test.tsx @@ -29,7 +29,9 @@ vi.mock("@hooks/useLogger", () => ({ })); vi.mock("@constants/apiConstants", async (importOriginal) => { - const actual = (await importOriginal()) as { ApiQueryKeys: Record }; + const actual = (await importOriginal()) as { + ApiQueryKeys: Record; + }; return { ...actual, API_MOCK_DELAY: 0, @@ -132,8 +134,12 @@ describe("useGetProductUpdateLevels", () => { }, ]; - const originalWindowConfig = (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config; - (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config = { + const originalWindowConfig = ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config; + ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config = { CUSTOMER_PORTAL_BACKEND_BASE_URL: "https://api.example.com", }; @@ -166,8 +172,12 @@ describe("useGetProductUpdateLevels", () => { it("should handle API error when isMockEnabled is false", async () => { mockIsMockEnabled = false; - const originalWindowConfig = (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config; - (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config = { + const originalWindowConfig = ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config; + ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config = { CUSTOMER_PORTAL_BACKEND_BASE_URL: "https://api.example.com", }; From e03b0125605aa27ca715d927356f147a96eabc84 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:29:29 +0530 Subject: [PATCH 04/43] Reformat useGetProductUpdatesStats tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adjust formatting in useGetProductUpdatesStats.test.tsx for readability: expand single-line type casts and renderHook calls to multi-line, and reformat window.config typing. No functional changes — purely stylistic/refactor to improve clarity in the test file. --- .../useGetProductUpdatesStats.test.tsx | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/apps/customer-portal/webapp/src/api/__tests__/useGetProductUpdatesStats.test.tsx b/apps/customer-portal/webapp/src/api/__tests__/useGetProductUpdatesStats.test.tsx index 895e8cdd4a..7637c88c81 100644 --- a/apps/customer-portal/webapp/src/api/__tests__/useGetProductUpdatesStats.test.tsx +++ b/apps/customer-portal/webapp/src/api/__tests__/useGetProductUpdatesStats.test.tsx @@ -29,7 +29,9 @@ vi.mock("@hooks/useLogger", () => ({ })); vi.mock("@constants/apiConstants", async (importOriginal) => { - const actual = (await importOriginal()) as { ApiQueryKeys: Record }; + const actual = (await importOriginal()) as { + ApiQueryKeys: Record; + }; return { ...actual, API_MOCK_DELAY: 0, @@ -78,18 +80,24 @@ describe("useGetProductUpdatesStats", () => { ); it("should return loading state initially", async () => { - const { result } = renderHook(() => useGetProductUpdatesStats("project-1"), { - wrapper, - }); + const { result } = renderHook( + () => useGetProductUpdatesStats("project-1"), + { + wrapper, + }, + ); expect(result.current.isLoading).toBe(true); }); it("should return mock data when isMockEnabled is true", async () => { mockIsMockEnabled = true; - const { result } = renderHook(() => useGetProductUpdatesStats("project-1"), { - wrapper, - }); + const { result } = renderHook( + () => useGetProductUpdatesStats("project-1"), + { + wrapper, + }, + ); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -130,8 +138,12 @@ describe("useGetProductUpdatesStats", () => { securityUpdatesPending: 25, }; - const originalWindowConfig = (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config; - (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config = { + const originalWindowConfig = ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config; + ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config = { CUSTOMER_PORTAL_BACKEND_BASE_URL: "https://api.example.com", }; @@ -145,9 +157,12 @@ describe("useGetProductUpdatesStats", () => { } as Response), ); - const { result } = renderHook(() => useGetProductUpdatesStats("project-1"), { - wrapper, - }); + const { result } = renderHook( + () => useGetProductUpdatesStats("project-1"), + { + wrapper, + }, + ); await waitFor(() => expect(result.current.isSuccess).toBe(true)); @@ -166,8 +181,12 @@ describe("useGetProductUpdatesStats", () => { it("should handle API error when isMockEnabled is false", async () => { mockIsMockEnabled = false; - const originalWindowConfig = (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config; - (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config = { + const originalWindowConfig = ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config; + ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config = { CUSTOMER_PORTAL_BACKEND_BASE_URL: "https://api.example.com", }; @@ -181,9 +200,12 @@ describe("useGetProductUpdatesStats", () => { } as Response), ); - const { result } = renderHook(() => useGetProductUpdatesStats("project-1"), { - wrapper, - }); + const { result } = renderHook( + () => useGetProductUpdatesStats("project-1"), + { + wrapper, + }, + ); await waitFor(() => expect(result.current.isError).toBe(true)); expect(result.current.error?.message).toContain( From 540e4e0cbe6a5ea64113680ac202a86b7711c511 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:29:50 +0530 Subject: [PATCH 05/43] Add case details route under support cases Import CaseDetailsPage and nest routes so support/cases now has an index for AllCasesPage and a dynamic route support/cases/:caseId for CaseDetailsPage. This enables viewing individual case details while keeping the list as the default cases route. --- apps/customer-portal/webapp/src/App.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/customer-portal/webapp/src/App.tsx b/apps/customer-portal/webapp/src/App.tsx index dc0943448c..ea35a5ba02 100644 --- a/apps/customer-portal/webapp/src/App.tsx +++ b/apps/customer-portal/webapp/src/App.tsx @@ -23,6 +23,7 @@ import DashboardPage from "@pages/DashboardPage"; import SupportPage from "@pages/SupportPage"; import UpdatesPage from "@pages/UpdatesPage"; import AllCasesPage from "@pages/AllCasesPage"; +import CaseDetailsPage from "@pages/CaseDetailsPage"; import NoveraChatPage from "@pages/NoveraChatPage"; import CreateCasePage from "@pages/CreateCasePage"; import AppLayout from "@layouts/AppLayout"; @@ -57,7 +58,10 @@ export default function App(): JSX.Element { {/* Support */} } /> - } /> + + } /> + } /> + } /> } /> From cf34a174cf8bcd9445dded3b4060d430ea79e01a Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:30:01 +0530 Subject: [PATCH 06/43] Format enabled condition in useGetCaseAttachments Combine the multi-line `enabled` property into a single line for readability. No functional change: the condition remains `!!caseId && (isMockEnabled || (isSignedIn && !isAuthLoading))`. --- apps/customer-portal/webapp/src/api/useGetCaseAttachments.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/customer-portal/webapp/src/api/useGetCaseAttachments.ts b/apps/customer-portal/webapp/src/api/useGetCaseAttachments.ts index 2d31ae7730..0e0b7cae84 100644 --- a/apps/customer-portal/webapp/src/api/useGetCaseAttachments.ts +++ b/apps/customer-portal/webapp/src/api/useGetCaseAttachments.ts @@ -98,8 +98,7 @@ export default function useGetCaseAttachments( throw error; } }, - enabled: - !!caseId && (isMockEnabled || (isSignedIn && !isAuthLoading)), + enabled: !!caseId && (isMockEnabled || (isSignedIn && !isAuthLoading)), staleTime: 5 * 60 * 1000, // 5 minutes }); } From 263a3acadf360e8f29f8c21dd86b5c0b8d4dd71d Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:30:18 +0530 Subject: [PATCH 07/43] Add CaseDetailsSkeleton header test Add a Vitest + React Testing Library test for the CaseDetailsSkeleton component in the case-details header. The test renders the component within ThemeProvider (createTheme) and asserts that multiple MUI Skeleton elements and at least one MUI Divider are present, ensuring the skeleton layout renders as expected. (apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsSkeleton.test.tsx) --- .../__tests__/CaseDetailsSkeleton.test.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsSkeleton.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsSkeleton.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsSkeleton.test.tsx new file mode 100644 index 0000000000..a4feb48981 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsSkeleton.test.tsx @@ -0,0 +1,36 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import CaseDetailsSkeleton from "@case-details/CaseDetailsSkeleton"; +import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; + +describe("CaseDetailsSkeleton", () => { + it("should render divided skeleton sections", () => { + const { container } = render( + + + , + ); + + const skeletons = container.querySelectorAll(".MuiSkeleton-root"); + expect(skeletons.length).toBeGreaterThan(5); + + const dividers = container.querySelectorAll(".MuiDivider-root"); + expect(dividers.length).toBeGreaterThan(0); + }); +}); From f6056ab39065a52d7b0803c88fa82c62b7c58e5a Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:30:31 +0530 Subject: [PATCH 08/43] Add tests for CaseDetailsContent header Add a new test suite for CaseDetailsContent header (apps/customer-portal/.../header/__tests__/CaseDetailsContent.test.tsx). Tests cover loading skeleton, rendering with data, Details tab placeholder, Back button behavior, Focus Mode toggle (hide/show header content), and presence of action buttons. Uses vitest and React Testing Library, and mocks support and casesTable utilities as well as ThemeProvider wrapping. --- .../__tests__/CaseDetailsContent.test.tsx | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsContent.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsContent.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsContent.test.tsx new file mode 100644 index 0000000000..1342ce52c2 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsContent.test.tsx @@ -0,0 +1,152 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import CaseDetailsContent from "@case-details/CaseDetailsContent"; +import { mockCaseDetails } from "@models/mockData"; +import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; + +vi.mock("@utils/casesTable", () => ({ + getStatusColor: vi.fn(() => "warning.main"), + getSeverityColor: vi.fn(() => "error.main"), +})); + +vi.mock("@utils/support", () => ({ + resolveColorFromTheme: vi.fn(() => "#fa7b3f"), + getStatusIcon: vi.fn(() => () => ), + getStatusIconElement: vi.fn(() => ), + formatValue: vi.fn((v: unknown) => (v == null || v === "" ? "--" : String(v))), +})); + +describe("CaseDetailsContent", () => { + const theme = createTheme(); + const onBack = vi.fn(); + + it("should render skeleton when loading", () => { + render( + + + , + ); + + expect(screen.getByText("Back to Support Center")).toBeInTheDocument(); + // CaseDetailsSkeleton renders multiple Skeleton elements + const skeletons = document.querySelectorAll(".MuiSkeleton-root"); + expect(skeletons.length).toBeGreaterThan(0); + }); + + it("should render case details when data is provided", () => { + render( + + + , + ); + + expect(screen.getByText(mockCaseDetails.number!)).toBeInTheDocument(); + expect(screen.getByText(mockCaseDetails.title!)).toBeInTheDocument(); + fireEvent.click(screen.getByRole("tab", { name: /details/i })); + expect(screen.getByText("Details appear here.")).toBeInTheDocument(); + }); + + it("should show details placeholder when Details tab is selected", () => { + render( + + + , + ); + + fireEvent.click(screen.getByRole("tab", { name: /details/i })); + expect(screen.getByText("Details appear here.")).toBeInTheDocument(); + }); + + it("should call onBack when Back button is clicked", () => { + render( + + + , + ); + + fireEvent.click(screen.getByText("Back to Support Center")); + expect(onBack).toHaveBeenCalledTimes(1); + }); + + it("should hide content above sub nav when Focus Mode is toggled", () => { + render( + + + , + ); + + expect(screen.getByText("Back to Support Center")).toBeInTheDocument(); + expect(screen.getByText(mockCaseDetails.title!)).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /focus mode/i })); + expect(screen.getByText("Back to Support Center")).toBeInTheDocument(); + expect(screen.queryByText(mockCaseDetails.title!)).not.toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /exit focus mode/i }), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /exit focus mode/i })); + expect(screen.getByText("Back to Support Center")).toBeInTheDocument(); + expect(screen.getByText(mockCaseDetails.title!)).toBeInTheDocument(); + }); + + it("should render case action buttons in CaseDetailsActionRow", () => { + render( + + + , + ); + + expect(screen.getByText("Escalate Case")).toBeInTheDocument(); + expect(screen.getByText("Waiting on WSO2")).toBeInTheDocument(); + expect(screen.getByText("Mark as Resolved")).toBeInTheDocument(); + expect(screen.getByText("Close Case")).toBeInTheDocument(); + expect(screen.getByText("Manage case status")).toBeInTheDocument(); + }); +}); From 816d216cb045991087bd21f1cbe9b68cf15de785 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:30:48 +0530 Subject: [PATCH 09/43] Refactor test typings and window config casts Reformat TypeScript annotations and window.config casts in apps/customer-portal/webapp/src/api/__tests__/useGetUpdatesStats.test.tsx. The importOriginal mock type and the window.config cast/assignment were split into multiline expressions for improved readability and to satisfy linting/formatting rules. No functional behavior was changed. --- .../api/__tests__/useGetUpdatesStats.test.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/customer-portal/webapp/src/api/__tests__/useGetUpdatesStats.test.tsx b/apps/customer-portal/webapp/src/api/__tests__/useGetUpdatesStats.test.tsx index a6306a984d..ce4de97666 100644 --- a/apps/customer-portal/webapp/src/api/__tests__/useGetUpdatesStats.test.tsx +++ b/apps/customer-portal/webapp/src/api/__tests__/useGetUpdatesStats.test.tsx @@ -29,7 +29,9 @@ vi.mock("@hooks/useLogger", () => ({ })); vi.mock("@constants/apiConstants", async (importOriginal) => { - const actual = (await importOriginal()) as { ApiQueryKeys: Record }; + const actual = (await importOriginal()) as { + ApiQueryKeys: Record; + }; return { ...actual, API_MOCK_DELAY: 0, @@ -126,8 +128,12 @@ describe("useGetUpdatesStats", () => { securityUpdatesPending: 25, }; - const originalWindowConfig = (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config; - (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config = { + const originalWindowConfig = ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config; + ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config = { CUSTOMER_PORTAL_BACKEND_BASE_URL: "https://api.example.com", }; @@ -162,8 +168,12 @@ describe("useGetUpdatesStats", () => { it("should handle API error when isMockEnabled is false", async () => { mockIsMockEnabled = false; - const originalWindowConfig = (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config; - (window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } }).config = { + const originalWindowConfig = ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config; + ( + window as { config?: { CUSTOMER_PORTAL_BACKEND_BASE_URL?: string } } + ).config = { CUSTOMER_PORTAL_BACKEND_BASE_URL: "https://api.example.com", }; From b37cad251fd839f32d4f32f2ef38fd070617436f Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:30:58 +0530 Subject: [PATCH 10/43] Add optional onCaseClick handler to AllCasesList Expose an optional `onCaseClick` prop on AllCasesList and wire it to each case CardButton. When provided the callback will be invoked with the clicked `CaseListItem`; if omitted the behavior is unchanged. This allows parent components to handle case selection/click events. --- .../webapp/src/components/support/all-cases/AllCasesList.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesList.tsx b/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesList.tsx index b71e9baf96..4b409f9b8b 100644 --- a/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesList.tsx +++ b/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesList.tsx @@ -37,6 +37,7 @@ import AllCasesListSkeleton from "@components/support/all-cases/AllCasesListSkel export interface AllCasesListProps { cases: CaseListItem[]; isLoading: boolean; + onCaseClick?: (caseItem: CaseListItem) => void; } /** @@ -48,6 +49,7 @@ export interface AllCasesListProps { export default function AllCasesList({ cases, isLoading, + onCaseClick, }: AllCasesListProps): JSX.Element { const theme = useTheme(); @@ -75,6 +77,7 @@ export default function AllCasesList({ return ( onCaseClick(caseItem) : undefined} sx={{ p: 3, display: "flex", From 312fe44d58592de757c7791c39891b61d00453fa Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:32:04 +0530 Subject: [PATCH 11/43] Remove project prefix from case details API Change the request URL in useGetCaseDetails to call /cases/{caseId} instead of /projects/{projectId}/cases/{caseId}. This removes the projectId segment from the API path while keeping the auth token and headers unchanged, aligning the client with the updated backend route. --- apps/customer-portal/webapp/src/api/useGetCaseDetails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/customer-portal/webapp/src/api/useGetCaseDetails.ts b/apps/customer-portal/webapp/src/api/useGetCaseDetails.ts index 6b0aa52daa..d0cf341935 100644 --- a/apps/customer-portal/webapp/src/api/useGetCaseDetails.ts +++ b/apps/customer-portal/webapp/src/api/useGetCaseDetails.ts @@ -57,7 +57,7 @@ export default function useGetCaseDetails( } const idToken = await getIdToken(); - const requestUrl = `${baseUrl}/projects/${projectId}/cases/${caseId}`; + const requestUrl = `${baseUrl}/cases/${caseId}`; const response = await fetch(requestUrl, { method: "GET", headers: addApiHeaders(idToken), From ed67c8a9bd87942339dca65e0166855c9c4d6687 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:33:06 +0530 Subject: [PATCH 12/43] Add CaseDetailField unit tests Add tests for the CaseDetailField component using Vitest and React Testing Library. Covers: rendering label and value, null/undefined values showing "--", error state rendering a mocked ErrorIndicator, and loading state hiding the value. Tests wrap the component in ThemeProvider (oxygen-ui) to provide theming. --- .../header/__tests__/CaseDetailField.test.tsx | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailField.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailField.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailField.test.tsx new file mode 100644 index 0000000000..5194340fbd --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailField.test.tsx @@ -0,0 +1,91 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import CaseDetailField from "@case-details/CaseDetailField"; +import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; + +vi.mock("@components/common/error-indicator/ErrorIndicator", () => ({ + default: ({ entityName }: { entityName: string }) => ( + {entityName} + ), +})); + +function renderCaseDetailField(props: { + label: string; + value?: string | null; + isLoading?: boolean; + isError?: boolean; +}) { + return render( + + + , + ); +} + +describe("CaseDetailField", () => { + it("should render label and value when not loading and not error", () => { + renderCaseDetailField({ label: "Product", value: "Choreo" }); + + expect(screen.getByText("Product")).toBeInTheDocument(); + expect(screen.getByText("Choreo")).toBeInTheDocument(); + }); + + it("should render -- for null value", () => { + renderCaseDetailField({ label: "CS Manager", value: null }); + + expect(screen.getByText("CS Manager")).toBeInTheDocument(); + expect(screen.getByText("--")).toBeInTheDocument(); + }); + + it("should render -- for undefined value", () => { + renderCaseDetailField({ label: "Account", value: undefined }); + + expect(screen.getByText("--")).toBeInTheDocument(); + }); + + it("should render error indicator when isError is true", () => { + renderCaseDetailField({ + label: "Description", + value: "Some text", + isError: true, + }); + + expect(screen.getByText("Description")).toBeInTheDocument(); + expect(screen.getByTestId("error-indicator")).toBeInTheDocument(); + expect(screen.getByTestId("error-indicator")).toHaveTextContent( + "case details", + ); + }); + + it("should render loading placeholder when isLoading is true", () => { + renderCaseDetailField({ + label: "Created", + value: "2026-01-01", + isLoading: true, + }); + + expect(screen.getByText("Created")).toBeInTheDocument(); + expect(screen.queryByText("2026-01-01")).not.toBeInTheDocument(); + }); +}); From 3d35c2143e18dc084ea4a537f88ff838fdbf358f Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:34:16 +0530 Subject: [PATCH 13/43] Compact logger.debug message Collapse a multi-line logger.debug call into a single-line string in useGetProductUpdateLevels.ts for improved formatting/readability; this is a non-functional change. --- .../webapp/src/api/useGetProductUpdateLevels.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/customer-portal/webapp/src/api/useGetProductUpdateLevels.ts b/apps/customer-portal/webapp/src/api/useGetProductUpdateLevels.ts index 87f7400324..9acb2b67b9 100644 --- a/apps/customer-portal/webapp/src/api/useGetProductUpdateLevels.ts +++ b/apps/customer-portal/webapp/src/api/useGetProductUpdateLevels.ts @@ -40,9 +40,7 @@ export function useGetProductUpdateLevels(): UseQueryResult< return useQuery({ queryKey: [ApiQueryKeys.PRODUCT_UPDATE_LEVELS, isMockEnabled], queryFn: async (): Promise => { - logger.debug( - `Fetching product update levels, mock: ${isMockEnabled}`, - ); + logger.debug(`Fetching product update levels, mock: ${isMockEnabled}`); if (isMockEnabled) { await new Promise((resolve) => setTimeout(resolve, API_MOCK_DELAY)); From a5b799b3f87c4d9f49a2a9865107d2c99ada376a Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:35:55 +0530 Subject: [PATCH 14/43] Add tests for CaseDetailsActionRow component Add Vitest unit tests for the CaseDetailsActionRow component. Tests verify normal render shows the assigned engineer, the "Support Engineer" label and all action buttons; loading state shows skeletons while retaining action controls; and error state renders the ErrorIndicator (mocked). Tests wrap the component in ThemeProvider to match the UI theme. --- .../__tests__/CaseDetailsActionRow.test.tsx | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsActionRow.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsActionRow.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsActionRow.test.tsx new file mode 100644 index 0000000000..3bfa09f422 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsActionRow.test.tsx @@ -0,0 +1,81 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import CaseDetailsActionRow from "@case-details/CaseDetailsActionRow"; +import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; + +vi.mock("@components/common/error-indicator/ErrorIndicator", () => ({ + default: ({ entityName }: { entityName: string }) => ( + {entityName} + ), +})); + +describe("CaseDetailsActionRow", () => { + it("should render engineer name, Support Engineer label and action buttons when not loading", () => { + render( + + + , + ); + expect(screen.getByText("Jane Doe")).toBeInTheDocument(); + expect(screen.getByText("Support Engineer")).toBeInTheDocument(); + expect(screen.getByText("Manage case status")).toBeInTheDocument(); + expect(screen.getByText("Escalate Case")).toBeInTheDocument(); + expect(screen.getByText("Waiting on WSO2")).toBeInTheDocument(); + expect(screen.getByText("Mark as Resolved")).toBeInTheDocument(); + expect(screen.getByText("Close Case")).toBeInTheDocument(); + }); + + it("should show skeletons for avatar and engineer but keep play icon, Manage case status, and action buttons when isLoading", () => { + const { container } = render( + + + , + ); + expect(screen.getByText("Support Engineer")).toBeInTheDocument(); + expect(screen.getByText("Manage case status")).toBeInTheDocument(); + expect(screen.getByText("Escalate Case")).toBeInTheDocument(); + expect(screen.getByText("Waiting on WSO2")).toBeInTheDocument(); + expect(screen.getByText("Mark as Resolved")).toBeInTheDocument(); + expect(screen.getByText("Close Case")).toBeInTheDocument(); + const skeletons = container.querySelectorAll(".MuiSkeleton-root"); + expect(skeletons.length).toBeGreaterThan(0); + }); + + it("should render error indicator when isError is true", () => { + render( + + + , + ); + expect(screen.getByTestId("error-indicator")).toBeInTheDocument(); + }); +}); From e2ffdd298e51b908d65c4ce581b218f4a3770905 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:36:05 +0530 Subject: [PATCH 15/43] Add tests for CaseDetailsBackButton Add a new test file for the CaseDetailsBackButton component at apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsBackButton.test.tsx. The tests (using vitest and React Testing Library) verify that the component renders the "Back to Support Center" label and calls the provided onClick handler when clicked. The component is wrapped with ThemeProvider/createTheme from @wso2/oxygen-ui. File includes project license header. --- .../__tests__/CaseDetailsBackButton.test.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsBackButton.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsBackButton.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsBackButton.test.tsx new file mode 100644 index 0000000000..78a64181ff --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsBackButton.test.tsx @@ -0,0 +1,43 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import CaseDetailsBackButton from "@case-details/CaseDetailsBackButton"; +import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; + +describe("CaseDetailsBackButton", () => { + it("should render Back to Support Center button", () => { + const onClick = vi.fn(); + render( + + + , + ); + expect(screen.getByText("Back to Support Center")).toBeInTheDocument(); + }); + + it("should call onClick when clicked", () => { + const onClick = vi.fn(); + render( + + + , + ); + fireEvent.click(screen.getByText("Back to Support Center")); + expect(onClick).toHaveBeenCalledTimes(1); + }); +}); From c77784e52635bd27122acbdefe41e6fb7d4fdd96 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:36:17 +0530 Subject: [PATCH 16/43] Add CaseDetailsPage loading state tests Add a new test file for CaseDetailsPage (apps/customer-portal/webapp/src/pages/__tests__/CaseDetailsPage.test.tsx). The tests mock react-router, loader and error banner contexts, and the useGetCaseDetails hook to verify the page renders the "Back to Support Center" text during initial loading and displays skeletons when isFetching (refresh) is true. Uses Vitest and React Testing Library. --- .../pages/__tests__/CaseDetailsPage.test.tsx | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 apps/customer-portal/webapp/src/pages/__tests__/CaseDetailsPage.test.tsx diff --git a/apps/customer-portal/webapp/src/pages/__tests__/CaseDetailsPage.test.tsx b/apps/customer-portal/webapp/src/pages/__tests__/CaseDetailsPage.test.tsx new file mode 100644 index 0000000000..0358e26e58 --- /dev/null +++ b/apps/customer-portal/webapp/src/pages/__tests__/CaseDetailsPage.test.tsx @@ -0,0 +1,66 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import CaseDetailsPage from "@pages/CaseDetailsPage"; +import useGetCaseDetails from "@api/useGetCaseDetails"; + +vi.mock("react-router", () => ({ + useNavigate: () => vi.fn(), + useParams: () => ({ projectId: "project-1", caseId: "case-001" }), +})); + +vi.mock("@context/linear-loader/LoaderContext", () => ({ + useLoader: () => ({ + showLoader: vi.fn(), + hideLoader: vi.fn(), + }), +})); + +vi.mock("@context/error-banner/ErrorBannerContext", () => ({ + useErrorBanner: () => ({ + showError: vi.fn(), + }), +})); + +vi.mock("@api/useGetCaseDetails", () => ({ + default: vi.fn(() => ({ + data: undefined, + isLoading: true, + isFetching: false, + isError: false, + })), +})); + +describe("CaseDetailsPage", () => { + it("should render Back to Support Center when loading", () => { + render(); + expect(screen.getByText("Back to Support Center")).toBeInTheDocument(); + }); + + it("should show skeletons when isFetching (refresh) so loader and content stay in sync", () => { + vi.mocked(useGetCaseDetails).mockReturnValue({ + data: undefined, + isLoading: false, + isFetching: true, + isError: false, + } as unknown as ReturnType); + const { container } = render(); + const skeletons = container.querySelectorAll(".MuiSkeleton-root"); + expect(skeletons.length).toBeGreaterThan(0); + }); +}); From 9bd397cc4aba837b4a492279aecdc0b1552a5292 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:36:37 +0530 Subject: [PATCH 17/43] Add JSDoc for mock case classification Add a JSDoc comment above getMockCaseClassification describing that it returns a mock case classification response and accepts a CaseClassificationRequest parameter. Improves inline documentation for mock functions in the customer-portal webapp. --- apps/customer-portal/webapp/src/models/mockFunctions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/customer-portal/webapp/src/models/mockFunctions.ts b/apps/customer-portal/webapp/src/models/mockFunctions.ts index e7a3b5d08a..a5388f2a1d 100644 --- a/apps/customer-portal/webapp/src/models/mockFunctions.ts +++ b/apps/customer-portal/webapp/src/models/mockFunctions.ts @@ -314,6 +314,7 @@ export const getMockUpdatesStats = (): UpdatesStats => mockUpdatesStats; export const getMockProductUpdateLevels = (): ProductUpdateLevelsResponse => mockProductUpdateLevels; +/** * Returns mock case classification response. * * @param {CaseClassificationRequest} request - Classification request body. From 4ce43c9e7dc76f25b59d68539ee0888ee4058344 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:37:06 +0530 Subject: [PATCH 18/43] Add case details tabs config & icons Introduce a CaseDetailsTabConfig interface and CASE_DETAILS_TABS array to centralize labels and icons for case detail tabs (Activity, Details, Attachments, Calls, Knowledge Base). Import additional oxygen UI icons (BookOpen, Info, Paperclip, Phone) and wire them into the new config to drive the support UI tabs. --- .../webapp/src/constants/supportConstants.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/customer-portal/webapp/src/constants/supportConstants.ts b/apps/customer-portal/webapp/src/constants/supportConstants.ts index 5a11b358b9..acc7228f72 100644 --- a/apps/customer-portal/webapp/src/constants/supportConstants.ts +++ b/apps/customer-portal/webapp/src/constants/supportConstants.ts @@ -16,14 +16,18 @@ import { Activity, + BookOpen, Bot, CircleAlert, CircleCheck, CircleQuestionMark, Clock, FileText, + Info, MessageCircle, MessageSquare, + Paperclip, + Phone, TrendingUp, Zap, } from "@wso2/oxygen-ui-icons-react"; @@ -178,6 +182,22 @@ export const SUPPORT_STAT_CONFIGS: SupportStatConfig[] = [ }, ]; +/** + * Case details tab configuration (label + icon for Activity, Details, Attachments, etc.). + */ +export interface CaseDetailsTabConfig { + label: string; + Icon: ComponentType<{ size?: number }>; +} + +export const CASE_DETAILS_TABS: CaseDetailsTabConfig[] = [ + { label: "Activity", Icon: MessageSquare }, + { label: "Details", Icon: Info }, + { label: "Attachments (0)", Icon: Paperclip }, + { label: "Calls (0)", Icon: Phone }, + { label: "Knowledge Base (0)", Icon: BookOpen }, +]; + // Number of outstanding cases to show on support overview cards. export const SUPPORT_OVERVIEW_CASES_LIMIT = 5; From 1b74093e9ce1b68edb9e2a3a0d0df1fd1fc0452f Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:37:19 +0530 Subject: [PATCH 19/43] Add DetailRow component for case details Introduce a new DetailRow React component for the customer-portal case details header. The component renders a label and formatted value, and handles loading and error states (shows a skeleton box or ErrorIndicator). Written in TypeScript with a DetailRowProps interface and uses Box/Typography from oxygen-ui and a formatValue utility. --- .../support/case-details/header/DetailRow.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/DetailRow.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/DetailRow.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/DetailRow.tsx new file mode 100644 index 0000000000..f44cdd0929 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/DetailRow.tsx @@ -0,0 +1,74 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ErrorIndicator from "@/components/common/error-indicator/ErrorIndicator"; +import { Box, Typography } from "@wso2/oxygen-ui"; +import { type JSX } from "react"; +import { formatValue } from "@utils/support"; + +export interface DetailRowProps { + label: string; + value: string | null | undefined; + isLoading: boolean; + isError: boolean; +} + +/** + * Single label-value row for case details (e.g. Description, Created, Product). + * + * @param {DetailRowProps} props - Label, value, and loading/error state. + * @returns {JSX.Element} The row. + */ +export default function DetailRow({ + label, + value, + isLoading, + isError, +}: DetailRowProps): JSX.Element { + return ( + + + {label} + + {isError ? ( + + ) : isLoading ? ( + + ) : ( + + {formatValue(value)} + + )} + + ); +} From d5d09486638fed0773a409d9fa3e1457565a44a6 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:37:32 +0530 Subject: [PATCH 20/43] Add CaseDetailsTabPanels component Introduce a new CaseDetailsTabPanels React component that renders tab panel content for case details (Activity, Details, Attachments, Calls, Knowledge Base) using Box and Typography from @wso2/oxygen-ui. Adds CaseDetailsTabPanelsProps (activeTab, data, isError, isLoading) and provides placeholder content per tab index; unknown indices return null. File includes Apache 2.0 license header and references the CaseDetails type. --- .../header/CaseDetailsTabPanels.tsx | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsTabPanels.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsTabPanels.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsTabPanels.tsx new file mode 100644 index 0000000000..b77fe849b5 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsTabPanels.tsx @@ -0,0 +1,90 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { Box, Typography } from "@wso2/oxygen-ui"; +import { type JSX } from "react"; +import type { CaseDetails } from "@models/responses"; + +export interface CaseDetailsTabPanelsProps { + activeTab: number; + data: CaseDetails | undefined; + isError: boolean; + isLoading?: boolean; +} + +/** + * Renders the active tab panel for case details (Activity, Details, Attachments, Calls, Knowledge Base). + * + * @param {CaseDetailsTabPanelsProps} props - Active tab index, case data, and error state. + * @returns {JSX.Element | null} The panel content. + */ +export default function CaseDetailsTabPanels({ + activeTab, +}: CaseDetailsTabPanelsProps): JSX.Element | null { + const panelBorderSx = { border: 1, borderColor: "divider", p: 2 }; + + if (activeTab === 0) { + return ( + + + Activity timeline will appear here. + + + ); + } + + if (activeTab === 1) { + return ( + + + Details appear here. + + + ); + } + + if (activeTab === 2) { + return ( + + + Attachments will appear here. + + + ); + } + + if (activeTab === 3) { + return ( + + + Calls will appear here. + + + ); + } + + if (activeTab === 4) { + return ( + + + Knowledge Base articles will appear here. + + + ); + } + + return null; +} From 49dd3039187375eae5f7dba92a17a8a97854982c Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:37:42 +0530 Subject: [PATCH 21/43] Add CaseDetailsBackButton component Introduce a new CaseDetailsBackButton component used in the case details header. The component renders an Oxygen UI Button with an ArrowLeft icon and the label "Back to Support Center", accepts an onClick handler and an optional sx prop with sensible defaults (mb, ml, alignSelf). File added at apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsBackButton.tsx. --- .../header/CaseDetailsBackButton.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsBackButton.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsBackButton.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsBackButton.tsx new file mode 100644 index 0000000000..5026bea851 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsBackButton.tsx @@ -0,0 +1,46 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { Button } from "@wso2/oxygen-ui"; +import { ArrowLeft } from "@wso2/oxygen-ui-icons-react"; +import { type JSX } from "react"; + +export interface CaseDetailsBackButtonProps { + onClick: () => void; + sx?: { mb?: number; ml?: number; alignSelf?: string }; +} + +/** + * Back navigation button for case details (e.g. "Back to Support Center"). + * + * @param {CaseDetailsBackButtonProps} props - onClick and optional sx. + * @returns {JSX.Element} The back button. + */ +export default function CaseDetailsBackButton({ + onClick, + sx = { mb: 1.5, ml: -0.5, alignSelf: "flex-start" }, +}: CaseDetailsBackButtonProps): JSX.Element { + return ( + + ); +} From 94dbae990d82a4053b9446a33dbddf26413bedbe Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:37:53 +0530 Subject: [PATCH 22/43] Add tests for CaseDetailsHeader component Introduce a new Vitest test suite for the CaseDetailsHeader component. Adds mocks for getSeverityColor and ErrorIndicator, wraps the component with ThemeProvider/createTheme, and includes tests verifying normal rendering (case number, title, severity, status chip), loading state skeletons, and error indicator rendering when isError is true. --- .../__tests__/CaseDetailsHeader.test.tsx | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsHeader.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsHeader.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsHeader.test.tsx new file mode 100644 index 0000000000..5403811fd1 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsHeader.test.tsx @@ -0,0 +1,82 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import CaseDetailsHeader from "@case-details/CaseDetailsHeader"; +import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; + +vi.mock("@utils/casesTable", () => ({ + getSeverityColor: vi.fn(() => "text.secondary"), +})); + +vi.mock("@components/common/error-indicator/ErrorIndicator", () => ({ + default: ({ entityName }: { entityName: string }) => ( + {entityName} + ), +})); + +describe("CaseDetailsHeader", () => { + const defaultProps = { + caseNumber: "CUPRSUB-101", + title: "Test case title", + severityLabel: "S1", + statusLabel: "Open", + statusChipIcon: , + statusChipSx: {}, + isError: false, + }; + + it("should render case number, severity, status chip and title when not loading", () => { + render( + + + , + ); + expect(screen.getByText("CUPRSUB-101")).toBeInTheDocument(); + expect(screen.getByText("Test case title")).toBeInTheDocument(); + expect(screen.getByText("S1")).toBeInTheDocument(); + expect(screen.getByText("Open")).toBeInTheDocument(); + expect(screen.getByTestId("status-chip-icon")).toBeInTheDocument(); + }); + + it("should render skeletons when isLoading is true", () => { + const { container } = render( + + + , + ); + const skeletons = container.querySelectorAll(".MuiSkeleton-root"); + expect(skeletons.length).toBeGreaterThan(0); + expect(screen.queryByText("CUPRSUB-101")).not.toBeInTheDocument(); + expect(screen.queryByText("Test case title")).not.toBeInTheDocument(); + }); + + it("should render error indicator in value fields only when isError is true", () => { + render( + + + , + ); + const errorIndicators = screen.getAllByText("case details"); + expect(errorIndicators.length).toBeGreaterThanOrEqual(1); + }); +}); From bb98862d76dfa16bee1b225003c4f882f301394d Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:38:06 +0530 Subject: [PATCH 23/43] Add tests for CaseDetailsTabPanels Add a new vitest/react-testing-library test file for CaseDetailsTabPanels. The tests render the component wrapped in ThemeProvider and assert the placeholder texts for activeTab values 0 (Activity), 1 (Details), 2 (Attachments), 3 (Calls) and 4 (Knowledge Base). File added at apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabPanels.test.tsx. --- .../__tests__/CaseDetailsTabPanels.test.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabPanels.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabPanels.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabPanels.test.tsx new file mode 100644 index 0000000000..dcb366a56e --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabPanels.test.tsx @@ -0,0 +1,59 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import CaseDetailsTabPanels from "@case-details/CaseDetailsTabPanels"; +import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; + +function renderTabPanels(activeTab: number) { + return render( + + + , + ); +} + +describe("CaseDetailsTabPanels", () => { + it("should show Activity placeholder when activeTab is 0", () => { + renderTabPanels(0); + expect(screen.getByText("Activity timeline will appear here.")).toBeInTheDocument(); + }); + + it("should show Details placeholder when activeTab is 1", () => { + renderTabPanels(1); + expect(screen.getByText("Details appear here.")).toBeInTheDocument(); + }); + + it("should show Attachments placeholder when activeTab is 2", () => { + renderTabPanels(2); + expect(screen.getByText("Attachments will appear here.")).toBeInTheDocument(); + }); + + it("should show Calls placeholder when activeTab is 3", () => { + renderTabPanels(3); + expect(screen.getByText("Calls will appear here.")).toBeInTheDocument(); + }); + + it("should show Knowledge Base placeholder when activeTab is 4", () => { + renderTabPanels(4); + expect(screen.getByText("Knowledge Base articles will appear here.")).toBeInTheDocument(); + }); +}); From d6519de7ce081b528ff7c4b1938de254766e4680 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:38:19 +0530 Subject: [PATCH 24/43] Add @case-details TS path aliases Add two TypeScript path mappings in apps/customer-portal/webapp/tsconfig.app.json: `@case-details` -> `src/components/support/case-details/header` and `@case-details/*` -> `src/components/support/case-details/header/*`. This enables concise imports for the case details header component within the customer-portal webapp. --- apps/customer-portal/webapp/tsconfig.app.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/customer-portal/webapp/tsconfig.app.json b/apps/customer-portal/webapp/tsconfig.app.json index 643e24e780..2f4ad75d0e 100644 --- a/apps/customer-portal/webapp/tsconfig.app.json +++ b/apps/customer-portal/webapp/tsconfig.app.json @@ -13,6 +13,8 @@ "@api/*": ["src/api/*"], "@assets/*": ["src/assets/*"], "@components/*": ["src/components/*"], + "@case-details": ["src/components/support/case-details/header"], + "@case-details/*": ["src/components/support/case-details/header/*"], "@config/*": ["src/config/*"], "@constants/*": ["src/constants/*"], "@context/*": ["src/context/*"], From 2117035443e6c348e5625508ea6703ee61d58e09 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:38:30 +0530 Subject: [PATCH 25/43] Add formatValue and getStatusIconElement helpers Add a formatValue utility to normalize display values (null/undefined/empty -> "--") and JSDoc comments. Tighten getStatusIcon typing to accept a size prop and introduce getStatusIconElement that returns a React element via createElement with a configurable size. Also import createElement and ReactNode to support the new helper. --- .../webapp/src/utils/support.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/customer-portal/webapp/src/utils/support.ts b/apps/customer-portal/webapp/src/utils/support.ts index 6bbd5fccc2..b75fcf14c1 100644 --- a/apps/customer-portal/webapp/src/utils/support.ts +++ b/apps/customer-portal/webapp/src/utils/support.ts @@ -23,7 +23,7 @@ import { } from "@wso2/oxygen-ui-icons-react"; import { ChatAction, ChatStatus } from "@constants/supportConstants"; import type { Theme } from "@wso2/oxygen-ui"; -import { type ComponentType } from "react"; +import { createElement, type ComponentType, type ReactNode } from "react"; export type ChatActionState = | "primary" @@ -32,6 +32,19 @@ export type ChatActionState = | "warning" | "error"; +/** + * Formats a value for display in case details; null/undefined/empty become "--". + * + * @param value - Raw value from API or state. + * @returns {string} Display string. + */ +export function formatValue( + value: string | number | null | undefined, +): string { + if (value == null || value === "") return "--"; + return String(value); +} + /** * Returns whether to show View or Resume for a chat status. * @@ -164,7 +177,7 @@ export function deriveFilterLabels(id: string): { * @param statusLabel - The case status label (e.g., "Open", "Working in Progress"). * @returns {ComponentType} The icon component. */ -export function getStatusIcon(statusLabel?: string): ComponentType { +export function getStatusIcon(statusLabel?: string): ComponentType<{ size?: number }> { const normalized = statusLabel?.toLowerCase() || ""; if (normalized.includes("open")) return CircleAlert; if (normalized.includes("progress")) return Clock; @@ -174,3 +187,18 @@ export function getStatusIcon(statusLabel?: string): ComponentType { return CircleCheck; return CircleAlert; } + +/** + * Returns the icon element for a given case status label (avoids creating a component during render). + * + * @param statusLabel - The case status label. + * @param size - Icon size in pixels. + * @returns {ReactNode} The icon element. + */ +export function getStatusIconElement( + statusLabel: string | null | undefined, + size = 12 +): ReactNode { + const Icon = getStatusIcon(statusLabel ?? undefined); + return createElement(Icon, { size }); +} From c66cefdbbf7c107089fbccb62198b7834c9751f3 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:38:42 +0530 Subject: [PATCH 26/43] Add tests for CaseDetailsTabs component Add a new test suite for the CaseDetailsTabs component using Vitest and React Testing Library. Tests verify that the Activity, Details, Attachments, Calls and Knowledge Base tabs render, that onChange is called when a tab is clicked, that the Focus Mode toggle button appears when an onFocusModeToggle prop is provided, and that the Exit Focus Mode label appears when focusMode is true. Tests wrap the component with ThemeProvider using createTheme. --- .../header/__tests__/CaseDetailsTabs.test.tsx | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabs.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabs.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabs.test.tsx new file mode 100644 index 0000000000..3a84d18d71 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabs.test.tsx @@ -0,0 +1,75 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import CaseDetailsTabs from "@case-details/CaseDetailsTabs"; +import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; + +describe("CaseDetailsTabs", () => { + it("should render Activity, Details, Attachments, Calls, Knowledge Base tabs", () => { + const onChange = vi.fn(); + render( + + + , + ); + expect(screen.getByRole("tab", { name: /activity/i })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: /details/i })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: /attachments/i })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: /calls/i })).toBeInTheDocument(); + expect(screen.getByRole("tab", { name: /knowledge base/i })).toBeInTheDocument(); + }); + + it("should call onChange when a tab is clicked", () => { + const onChange = vi.fn(); + render( + + + , + ); + fireEvent.click(screen.getByRole("tab", { name: /details/i })); + expect(onChange).toHaveBeenCalled(); + }); + + it("should show Focus Mode button when onFocusModeToggle is provided", () => { + const onFocusModeToggle = vi.fn(); + render( + + + , + ); + expect(screen.getByRole("button", { name: /focus mode/i })).toBeInTheDocument(); + }); + + it("should show Exit Focus Mode when focusMode is true", () => { + render( + + + , + ); + expect(screen.getByRole("button", { name: /exit focus mode/i })).toBeInTheDocument(); + }); +}); From 69840f85e2d57b2560c2edf607e995e41cfbcc2b Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:40:24 +0530 Subject: [PATCH 27/43] Add tests for DetailRow component Create unit tests for the DetailRow component (apps/customer-portal/.../DetailRow.test.tsx). Tests use vitest and @testing-library/react and wrap the component with ThemeProvider. Mocks added for ErrorIndicator and formatValue. Covers rendering label/value, null value showing "--", error state rendering the error indicator, and loading state hiding the value. --- .../header/__tests__/DetailRow.test.tsx | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/DetailRow.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/DetailRow.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/DetailRow.test.tsx new file mode 100644 index 0000000000..7e844e9829 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/DetailRow.test.tsx @@ -0,0 +1,72 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import DetailRow from "@case-details/DetailRow"; +import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; + +vi.mock("@/components/common/error-indicator/ErrorIndicator", () => ({ + default: ({ entityName }: { entityName: string }) => ( + {entityName} + ), +})); + +vi.mock("@utils/support", () => ({ + formatValue: (v: unknown) => (v == null || v === "" ? "--" : String(v)), +})); + +function renderDetailRow(props: { + label: string; + value?: string | null; + isLoading?: boolean; + isError?: boolean; +}) { + return render( + + + , + ); +} + +describe("DetailRow", () => { + it("should render label and value when not loading and not error", () => { + renderDetailRow({ label: "Product", value: "Choreo" }); + expect(screen.getByText("Product")).toBeInTheDocument(); + expect(screen.getByText("Choreo")).toBeInTheDocument(); + }); + + it("should render -- for null value", () => { + renderDetailRow({ label: "Account", value: null }); + expect(screen.getByText("--")).toBeInTheDocument(); + }); + + it("should render error indicator when isError is true", () => { + renderDetailRow({ label: "Description", value: "Text", isError: true }); + expect(screen.getByTestId("error-indicator")).toBeInTheDocument(); + }); + + it("should not show value when isLoading is true", () => { + renderDetailRow({ label: "Created", value: "2026-01-01", isLoading: true }); + expect(screen.getByText("Created")).toBeInTheDocument(); + expect(screen.queryByText("2026-01-01")).not.toBeInTheDocument(); + }); +}); From 2dee80da1319aeb0d16617ae655bde7e8cd5a932 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:40:36 +0530 Subject: [PATCH 28/43] Add CaseDetailField component Introduce a new CaseDetailField component used in the case details header to render a single label-value row. The component accepts label, value, isLoading and isError props and handles three states: error (renders ErrorIndicator), loading (renders skeleton box), and loaded (renders formatted value via formatValue). Uses WSO2 Oxygen UI Box and Typography and includes the Apache 2.0 license header. --- .../case-details/header/CaseDetailField.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailField.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailField.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailField.tsx new file mode 100644 index 0000000000..efcc4b2066 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailField.tsx @@ -0,0 +1,74 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { Box, Typography } from "@wso2/oxygen-ui"; +import { type JSX } from "react"; +import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator"; +import { formatValue } from "@utils/support"; + +export interface CaseDetailFieldProps { + label: string; + value: string | null | undefined; + isLoading: boolean; + isError: boolean; +} + +/** + * Single label-value field row for the case details panel (e.g. Description, Created, Product). + * + * @param {CaseDetailFieldProps} props - Label, value, and loading/error state. + * @returns {JSX.Element} The field row. + */ +export default function CaseDetailField({ + label, + value, + isLoading, + isError, +}: CaseDetailFieldProps): JSX.Element { + return ( + + + {label} + + {isError ? ( + + ) : isLoading ? ( + + ) : ( + + {formatValue(value)} + + )} + + ); +} From 3a0f4b310fd7bb7efc213870cb11b762ecefa8ea Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:40:48 +0530 Subject: [PATCH 29/43] Add CaseDetailsActionRow component Introduce CaseDetailsActionRow component for the support case details header. Renders engineer avatar/initials, name with loading/error states, a "Manage case status" label, and a row of status action buttons (Escalate, Waiting on WSO2, Mark as Resolved, Close Case) with intent-specific styling. Uses WSO2 Oxygen UI components and icons, ErrorIndicator, and formatValue util; accepts assignedEngineer, engineerInitials, isError, and optional isLoading props. --- .../header/CaseDetailsActionRow.tsx | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx new file mode 100644 index 0000000000..f7b50b96ae --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx @@ -0,0 +1,183 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { + Avatar, + Box, + Button, + Divider, + Paper, + Skeleton, + Stack, + Typography, + alpha, + useTheme, + type Theme, +} from "@wso2/oxygen-ui"; +import { + CircleCheck, + CirclePause, + CircleX, + CirclePlay, + TriangleAlert, +} from "@wso2/oxygen-ui-icons-react"; +import { type JSX, type ComponentType } from "react"; +import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator"; +import { formatValue } from "@utils/support"; + +type PaletteIntent = "error" | "warning" | "success" | "info"; + +interface CaseStatusAction { + label: string; + Icon: ComponentType<{ size?: number }>; + paletteIntent: PaletteIntent; +} + +const CASE_STATUS_ACTIONS: CaseStatusAction[] = [ + { label: "Escalate Case", Icon: TriangleAlert, paletteIntent: "error" }, + { label: "Waiting on WSO2", Icon: CirclePause, paletteIntent: "warning" }, + { label: "Mark as Resolved", Icon: CircleCheck, paletteIntent: "success" }, + { label: "Close Case", Icon: CircleX, paletteIntent: "info" }, +]; + +const ACTION_BUTTON_ICON_SIZE = 12; + +function getActionButtonSx( + theme: Theme, + intent: PaletteIntent, +): Record { + const light = theme.palette[intent].light; + return { + borderColor: light, + bgcolor: alpha(light, 0.1), + color: light, + fontSize: "0.7rem", + minHeight: 0, + py: 0.5, + px: 1, + "&:hover": { + borderColor: theme.palette[intent].main, + bgcolor: alpha(light, 0.2), + }, + textTransform: "none", + }; +} + +export interface CaseDetailsActionRowProps { + assignedEngineer: string | null | undefined; + engineerInitials: string; + isError: boolean; + isLoading?: boolean; +} + +/** + * Support action row: avatar, name, "Support Engineer" label, and "Manage case status" actions. + * + * @param {CaseDetailsActionRowProps} props - Action display data and error state. + * @returns {JSX.Element} The action row wrapped in Paper. + */ +export default function CaseDetailsActionRow({ + assignedEngineer, + engineerInitials, + isError, + isLoading = false, +}: CaseDetailsActionRowProps): JSX.Element { + const theme = useTheme(); + + return ( + + + {isLoading ? ( + + ) : ( + + {engineerInitials} + + )} + + {isError ? ( + + ) : isLoading ? ( + + ) : ( + + {formatValue(assignedEngineer)} + + )} + + Support Engineer + + + + + + + Manage case status + + + + + + {CASE_STATUS_ACTIONS.map(({ label, Icon, paletteIntent }) => ( + + ))} + + + ); +} From 9adea53115ddc54a2b573130f8dc980feb991d59 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:40:57 +0530 Subject: [PATCH 30/43] Add tests for formatValue utility Import formatValue in support.test.ts and add unit tests validating behavior for null/undefined, empty string, non-empty string, and number inputs. Confirms formatValue returns '--' for null/undefined/empty and stringified values for other inputs. --- .../src/utils/__tests__/support.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/customer-portal/webapp/src/utils/__tests__/support.test.ts b/apps/customer-portal/webapp/src/utils/__tests__/support.test.ts index f76690f569..08717a9e37 100644 --- a/apps/customer-portal/webapp/src/utils/__tests__/support.test.ts +++ b/apps/customer-portal/webapp/src/utils/__tests__/support.test.ts @@ -28,6 +28,7 @@ import { getChatStatusColor, getChatActionColor, formatRelativeTime, + formatValue, deriveFilterLabels, getStatusIcon, resolveColorFromTheme, @@ -96,6 +97,25 @@ describe("support utils", () => { }); }); + describe("formatValue", () => { + it("should return '--' for null and undefined", () => { + expect(formatValue(null)).toBe("--"); + expect(formatValue(undefined)).toBe("--"); + }); + + it("should return '--' for empty string", () => { + expect(formatValue("")).toBe("--"); + }); + + it("should return string for non-empty string", () => { + expect(formatValue("Choreo")).toBe("Choreo"); + }); + + it("should return string for number", () => { + expect(formatValue(42)).toBe("42"); + }); + }); + describe("formatRelativeTime", () => { it("should return '--' for undefined date", () => { expect(formatRelativeTime(undefined)).toBe("--"); From c64406b31e29c9e14cc31ce7ea8843210c495b96 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:41:09 +0530 Subject: [PATCH 31/43] Add CaseDetailsPage component Introduce CaseDetailsPage to render a support case details view. The component uses useGetCaseDetails to fetch case data, coordinates with LoaderContext to show skeletons/loader while loading or refetching (avoiding UI flash), and uses ErrorBannerContext to display errors once. It also provides a back handler that navigates to the project's cases list and delegates rendering to CaseDetailsContent. --- .../webapp/src/pages/CaseDetailsPage.tsx | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 apps/customer-portal/webapp/src/pages/CaseDetailsPage.tsx diff --git a/apps/customer-portal/webapp/src/pages/CaseDetailsPage.tsx b/apps/customer-portal/webapp/src/pages/CaseDetailsPage.tsx new file mode 100644 index 0000000000..f3e5112ae3 --- /dev/null +++ b/apps/customer-portal/webapp/src/pages/CaseDetailsPage.tsx @@ -0,0 +1,76 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { useEffect, useRef, type JSX } from "react"; +import { useNavigate, useParams } from "react-router"; +import { useLoader } from "@context/linear-loader/LoaderContext"; +import { useErrorBanner } from "@context/error-banner/ErrorBannerContext"; +import useGetCaseDetails from "@api/useGetCaseDetails"; +import CaseDetailsContent from "@case-details/CaseDetailsContent"; + +/** + * CaseDetailsPage displays details for a single support case. + * + * @returns {JSX.Element} The rendered Case Details page. + */ +export default function CaseDetailsPage(): JSX.Element { + const navigate = useNavigate(); + const { projectId, caseId } = useParams<{ projectId: string; caseId: string }>(); + const { showLoader, hideLoader } = useLoader(); + const { showError } = useErrorBanner(); + + const { + data, + isLoading, + isFetching, + isError, + } = useGetCaseDetails(projectId || "", caseId || ""); + + // Show skeletons immediately when no data (avoid "-" flash on refresh) and when loading/refetching. + const showSkeletons = + isLoading || + isFetching || + (data === undefined && !isError); + + useEffect(() => { + if (showSkeletons) { + showLoader(); + return () => hideLoader(); + } + hideLoader(); + }, [showSkeletons, showLoader, hideLoader]); + + const hasShownErrorRef = useRef(false); + useEffect(() => { + if (isError && !hasShownErrorRef.current) { + hasShownErrorRef.current = true; + showError("case details"); + } + }, [isError, showError]); + + const handleBack = () => { + navigate(`/${projectId}/support/cases`); + }; + + return ( + + ); +} From 20b7de6329ac1b9de953b5b76572f1da1257bb03 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:41:52 +0530 Subject: [PATCH 32/43] Show loader during fetch and add case click Use the LoaderContext to show a global loader while the page is loading (now includes isCasesFetching). Added a useEffect to show/hide the loader based on combined isStatsLoading / isCasesAreaLoading / isCasesFetching state. Also pass an onCaseClick handler into AllCasesList to navigate to the case detail route when a case is clicked. --- .../webapp/src/pages/AllCasesPage.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx b/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx index ca33e9643a..3c4a37ed89 100644 --- a/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx +++ b/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx @@ -22,6 +22,7 @@ import { type JSX, type ChangeEvent, } from "react"; +import { useLoader } from "@context/linear-loader/LoaderContext"; import { Box, Button, @@ -88,10 +89,13 @@ export default function AllCasesPage(): JSX.Element { const { data, isLoading: isCasesQueryLoading, + isFetching: isCasesFetching, hasNextPage, fetchNextPage, } = useGetProjectCases(projectId || "", caseSearchRequest); + const { showLoader, hideLoader } = useLoader(); + // Show skeletons until each section has a response (covers query disabled by auth + initial fetch). const hasStatsResponse = stats !== undefined; const hasCasesResponse = data !== undefined; @@ -100,6 +104,16 @@ export default function AllCasesPage(): JSX.Element { const isCasesAreaLoading = isCasesQueryLoading || (!!projectId && !hasCasesResponse); + const isPageLoading = isStatsLoading || isCasesAreaLoading || isCasesFetching; + + useEffect(() => { + if (isPageLoading) { + showLoader(); + return () => hideLoader(); + } + hideLoader(); + }, [isPageLoading, showLoader, hideLoader]); + // Background-load all remaining pages so search/filters work on full dataset. useEffect(() => { if (!data || !hasNextPage) { @@ -252,7 +266,11 @@ export default function AllCasesPage(): JSX.Element { {/* Cases list */} - + navigate(`/${projectId}/support/cases/${c.id}`)} + /> {/* Pagination */} {totalPages > 1 && ( From 764420715f9956dc04918703e47bce24f13d4b48 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:42:33 +0530 Subject: [PATCH 33/43] Make case titles clickable and adjust layout Add optional onCaseClick prop and CaseListItem type to CasesList to make case titles interactive. Titles now support click and keyboard activation (Enter/Space), include role/tabIndex for accessibility, conditional cursor/hover styles, and display as inline-flex. Also detect case details route in AppLayout and adjust container padding (remove top padding) when viewing a case detail page to align layout spacing. --- .../dashboard/cases-table/CasesList.tsx | 34 +++++++++++++-- .../dashboard/cases-table/CasesTable.tsx | 1 + .../cases-table/__tests__/CasesList.test.tsx | 43 +++++++++++++++++++ .../OutstandingCasesList.tsx | 4 ++ .../__tests__/OutstandingCasesList.test.tsx | 28 +++++++++++- .../webapp/src/layouts/AppLayout.tsx | 5 ++- .../webapp/src/pages/SupportPage.tsx | 10 ++++- 7 files changed, 117 insertions(+), 8 deletions(-) diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx index be44d22f1f..c338d75648 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx @@ -31,7 +31,7 @@ import { } from "@wso2/oxygen-ui"; import { ExternalLink, MoreVertical } from "@wso2/oxygen-ui-icons-react"; import { type JSX, type ChangeEvent } from "react"; -import type { CaseSearchResponse } from "@models/responses"; +import type { CaseSearchResponse, CaseListItem } from "@models/responses"; import { getSeverityColor, getStatusColor } from "@utils/casesTable"; import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator"; import CasesTableSkeleton from "@components/dashboard/cases-table/CasesTableSkeleton"; @@ -44,6 +44,8 @@ interface CasesListProps { rowsPerPage: number; onPageChange: (event: unknown, newPage: number) => void; onRowsPerPageChange: (event: ChangeEvent) => void; + /** When provided, case title is clickable and navigates to case details. */ + onCaseClick?: (caseItem: CaseListItem) => void; } const CasesList = ({ @@ -54,6 +56,7 @@ const CasesList = ({ rowsPerPage, onPageChange, onRowsPerPageChange, + onCaseClick, }: CasesListProps): JSX.Element => { return ( <> @@ -113,15 +116,38 @@ const CasesList = ({ { + if ( + e.key === "Enter" || + e.key === " " + ) { + e.preventDefault(); + onCaseClick(row); + } + } + : undefined + } + onClick={ + onCaseClick + ? () => onCaseClick(row) + : undefined + } sx={{ - display: "flex", + display: "inline-flex", alignItems: "center", gap: 0.5, - cursor: "pointer", + cursor: onCaseClick ? "pointer" : "default", fontWeight: 500, - "&:hover": { color: "primary.main" }, + "&:hover": onCaseClick + ? { color: "primary.main" } + : undefined, }} > {row.title || "--"} diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx index 29594db043..946a700fdb 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx @@ -227,6 +227,7 @@ const CasesTable = ({ projectId }: CasesTableProps): JSX.Element => { rowsPerPage={rowsPerPage} onPageChange={handleChangePage} onRowsPerPageChange={handleChangeRowsPerPage} + onCaseClick={(c) => navigate(`/${projectId}/support/cases/${c.id}`)} /> {/* Filter popover */} diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx index c99cf95417..02063af842 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx @@ -193,6 +193,49 @@ describe("CasesList", () => { expect(mockOnRowsPerPageChange).toHaveBeenCalled(); }); + it("should call onCaseClick with case when case title is clicked", () => { + const mockOnCaseClick = vi.fn(); + render( + , + ); + + fireEvent.click(screen.getByText("Test Case 1")); + expect(mockOnCaseClick).toHaveBeenCalledTimes(1); + expect(mockOnCaseClick).toHaveBeenCalledWith( + expect.objectContaining({ id: "1", title: "Test Case 1", number: "CS-001" }), + ); + + fireEvent.click(screen.getByText("Test Case 2")); + expect(mockOnCaseClick).toHaveBeenCalledTimes(2); + expect(mockOnCaseClick).toHaveBeenLastCalledWith( + expect.objectContaining({ id: "2", title: "Test Case 2", number: "CS-002" }), + ); + }); + + it("should not call onCaseClick when not provided (title still rendered)", () => { + render( + , + ); + + fireEvent.click(screen.getByText("Test Case 1")); + expect(screen.getByText("Test Case 1")).toBeInTheDocument(); + }); + it("should render '--' for null or undefined values", () => { const dataWithNulls = { cases: [ diff --git a/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesList.tsx b/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesList.tsx index 692a83cd17..40ef671a49 100644 --- a/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesList.tsx +++ b/apps/customer-portal/webapp/src/components/support/support-overview-cards/OutstandingCasesList.tsx @@ -34,6 +34,8 @@ import { formatRelativeTime, resolveColorFromTheme } from "@utils/support"; export interface OutstandingCasesListProps { cases: CaseListItem[]; isLoading?: boolean; + /** When provided, clicking a case card navigates to case details (caller should navigate). */ + onCaseClick?: (caseItem: CaseListItem) => void; } /** @@ -45,6 +47,7 @@ export interface OutstandingCasesListProps { export default function OutstandingCasesList({ cases, isLoading, + onCaseClick, }: OutstandingCasesListProps): JSX.Element { const theme = useTheme(); @@ -71,6 +74,7 @@ export default function OutstandingCasesList({ return ( onCaseClick(c) : undefined} sx={{ p: 2, display: "flex", diff --git a/apps/customer-portal/webapp/src/components/support/support-overview-cards/__tests__/OutstandingCasesList.test.tsx b/apps/customer-portal/webapp/src/components/support/support-overview-cards/__tests__/OutstandingCasesList.test.tsx index 78cdcb245b..b53018ab66 100644 --- a/apps/customer-portal/webapp/src/components/support/support-overview-cards/__tests__/OutstandingCasesList.test.tsx +++ b/apps/customer-portal/webapp/src/components/support/support-overview-cards/__tests__/OutstandingCasesList.test.tsx @@ -14,8 +14,8 @@ // specific language governing permissions and limitations // under the License. -import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; import OutstandingCasesList from "@components/support/support-overview-cards/OutstandingCasesList"; import type { CaseListItem } from "@models/responses"; @@ -52,4 +52,28 @@ describe("OutstandingCasesList", () => { expect(screen.getByText(/Assigned to Sarah Chen/)).toBeInTheDocument(); expect(screen.getByText("2 hours ago")).toBeInTheDocument(); }); + + it("should call onCaseClick with case when card is clicked", () => { + const onCaseClick = vi.fn(); + render( + , + ); + + fireEvent.click(screen.getByText("API Gateway timeout issues")); + expect(onCaseClick).toHaveBeenCalledTimes(1); + expect(onCaseClick).toHaveBeenCalledWith( + expect.objectContaining({ + id: "1", + number: "CASE-2845", + title: "API Gateway timeout issues", + }), + ); + }); + + it("should render without onCaseClick (cards not clickable for navigation)", () => { + render(); + expect(screen.getByText("API Gateway timeout issues")).toBeInTheDocument(); + fireEvent.click(screen.getByText("API Gateway timeout issues")); + // No crash; behavior is just no navigation + }); }); diff --git a/apps/customer-portal/webapp/src/layouts/AppLayout.tsx b/apps/customer-portal/webapp/src/layouts/AppLayout.tsx index 90195426ad..c2b9dea778 100644 --- a/apps/customer-portal/webapp/src/layouts/AppLayout.tsx +++ b/apps/customer-portal/webapp/src/layouts/AppLayout.tsx @@ -50,6 +50,9 @@ export default function AppLayout({ children }: AppLayoutProps): JSX.Element { const { isVisible } = useLoader(); const isProjectHub = location.pathname === "/"; + const isCaseDetailsPage = /\/[^/]+\/support\/cases\/[^/]+$/.test( + location.pathname, + ); return ( <> @@ -106,7 +109,7 @@ export default function AppLayout({ children }: AppLayoutProps): JSX.Element { sx={{ flex: 1, overflow: "auto", - p: 3, + p: isCaseDetailsPage ? { px: 3, pb: 3, pt: 0 } : 3, }} > {children || ( diff --git a/apps/customer-portal/webapp/src/pages/SupportPage.tsx b/apps/customer-portal/webapp/src/pages/SupportPage.tsx index 462bcc764f..7e5c5d0830 100644 --- a/apps/customer-portal/webapp/src/pages/SupportPage.tsx +++ b/apps/customer-portal/webapp/src/pages/SupportPage.tsx @@ -110,7 +110,15 @@ export default function SupportPage(): JSX.Element { footerButtonLabel="View all cases" onFooterClick={() => navigate("cases")} > - + navigate(`/${projectId}/support/cases/${c.id}`) + : undefined + } + /> From 2e306fc4138a4652217bc676e62419fa63a341d6 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:43:20 +0530 Subject: [PATCH 34/43] Add CaseDetailsHeader component Add a new CaseDetailsHeader component for the support case details page. The component renders case number, a severity color dot, a status Chip (with icon and custom sx), and the case title. It accepts CaseDetailsHeaderProps (caseNumber, title, severityLabel, statusLabel, statusChipIcon, statusChipSx, isError, isLoading), shows CaseDetailsHeaderSkeleton while loading and ErrorIndicator on error, and uses getSeverityColor and formatValue utilities for consistent display. --- .../cases-table/__tests__/CasesTable.test.tsx | 34 +++++- .../case-details/header/CaseDetailsHeader.tsx | 114 ++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsHeader.tsx diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesTable.test.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesTable.test.tsx index 540968a0bc..cc6a161c81 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesTable.test.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesTable.test.tsx @@ -21,9 +21,11 @@ import useGetProjectCases from "@api/useGetProjectCases"; import useGetCasesFilters from "@api/useGetCasesFilters"; import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; +const mockNavigate = vi.fn(); + // Mock dependencies vi.mock("react-router", () => ({ - useNavigate: () => vi.fn(), + useNavigate: () => mockNavigate, })); vi.mock("@/api/useGetProjectCases"); @@ -87,7 +89,12 @@ vi.mock("../CasesTableHeader", () => ({ })); vi.mock("../CasesList", () => ({ - default: ({ data, onPageChange, onRowsPerPageChange }: any) => ( + default: ({ + data, + onPageChange, + onRowsPerPageChange, + onCaseClick, + }: any) => (
{data?.cases.length || 0} {data?.totalRecords || 0} @@ -95,6 +102,14 @@ vi.mock("../CasesList", () => ({ + {onCaseClick && ( + + )}
), })); @@ -331,6 +346,21 @@ describe("CasesTable", () => { }); }); + it("should navigate to case details when onCaseClick is triggered from list", () => { + render( + + + , + ); + + const caseTitleButton = screen.getByTestId("case-title-click"); + fireEvent.click(caseTitleButton); + + expect(mockNavigate).toHaveBeenCalledWith( + `/${mockProjectId}/support/cases/case-1`, + ); + }); + it("should propagate loading and error states to FilterPopover", () => { mockUseGetCasesFilters.mockReturnValue({ isFetching: true, diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsHeader.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsHeader.tsx new file mode 100644 index 0000000000..8aafe5fa0e --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsHeader.tsx @@ -0,0 +1,114 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { Box, Chip, Stack, Typography } from "@wso2/oxygen-ui"; +import { type ReactElement, type ReactNode, type JSX } from "react"; +import { getSeverityColor } from "@utils/casesTable"; +import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator"; +import { formatValue } from "@utils/support"; +import { CaseDetailsHeaderSkeleton } from "@case-details/CaseDetailsSkeleton"; + +export interface CaseDetailsHeaderProps { + caseNumber: string | null | undefined; + title: string | null | undefined; + severityLabel: string | null | undefined; + statusLabel: string | null | undefined; + statusChipIcon: ReactNode; + statusChipSx: Record; + isError: boolean; + isLoading?: boolean; +} + +/** + * Case details header: case number, severity, status chip, and title. + * + * @param {CaseDetailsHeaderProps} props - Header data and styling. + * @returns {JSX.Element} The header block. + */ +export default function CaseDetailsHeader({ + caseNumber, + title, + severityLabel, + statusLabel, + statusChipIcon, + statusChipSx, + isError, + isLoading = false, +}: CaseDetailsHeaderProps): JSX.Element { + if (isLoading) { + return ; + } + + return ( + + + {isError ? ( + + ) : ( + + {formatValue(caseNumber)} + + )} + {isError ? ( + + ) : ( + + + + {formatValue(severityLabel)} + + + )} + {isError ? ( + + ) : ( + + )} + + {isError ? ( + + ) : ( + + {formatValue(title)} + + )} + + ); +} From 02ccfdfe189caa8d047843f510d8e3a0d2056ef2 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:43:32 +0530 Subject: [PATCH 35/43] Add CaseDetailsContent component Introduce CaseDetailsContent: a new React component for the customer-portal that renders a sticky case header and tabbed case details. It accepts data, isLoading, isError, and onBack props; computes status/severity presentation (color, icon) using getStatusColor/resolveColorFromTheme/getStatusIconElement; derives engineer initials; and manages local state for active tab and focus mode. The component composes CaseDetailsBackButton, CaseDetailsHeader, CaseDetailsActionRow, CaseDetailsTabs, and CaseDetailsTabPanels and applies theme-aware styling. --- .../header/CaseDetailsContent.tsx | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx new file mode 100644 index 0000000000..a020d9939c --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx @@ -0,0 +1,140 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { Box, Paper, alpha, useTheme } from "@wso2/oxygen-ui"; +import { useMemo, useState, type JSX } from "react"; +import type { CaseDetails } from "@models/responses"; +import { getStatusColor } from "@utils/casesTable"; +import { resolveColorFromTheme, getStatusIconElement } from "@utils/support"; +import CaseDetailsBackButton from "@case-details/CaseDetailsBackButton"; +import CaseDetailsHeader from "@case-details/CaseDetailsHeader"; +import CaseDetailsActionRow from "@case-details/CaseDetailsActionRow"; +import CaseDetailsTabs from "@case-details/CaseDetailsTabs"; +import CaseDetailsTabPanels from "@case-details/CaseDetailsTabPanels"; + +export interface CaseDetailsContentProps { + data: CaseDetails | undefined; + isLoading: boolean; + isError: boolean; + onBack: () => void; +} + +/** + * CaseDetailsContent displays case header and details in a layout similar to the template. + * + * @param {CaseDetailsContentProps} props - Data, loading/error state, and callbacks. + * @returns {JSX.Element} The rendered case details content. + */ +export default function CaseDetailsContent({ + data, + isLoading, + isError, + onBack, +}: CaseDetailsContentProps): JSX.Element { + const theme = useTheme(); + const [activeTab, setActiveTab] = useState(0); + const [focusMode, setFocusMode] = useState(false); + + const statusLabel = data?.state?.label; + const severityLabel = data?.severity?.label; + const statusColorPath = getStatusColor(statusLabel ?? undefined); + const resolvedStatusColor = resolveColorFromTheme(statusColorPath, theme); + const statusChipIcon = getStatusIconElement(statusLabel, 12); + const statusChipSx = useMemo( + () => ({ + bgcolor: alpha(resolvedStatusColor, 0.1), + color: resolvedStatusColor, + height: 20, + fontSize: "0.75rem", + px: 0, + "& .MuiChip-icon": { + color: "inherit", + ml: "6px", + mr: "6px", + }, + "& .MuiChip-label": { + pl: 0, + pr: "6px", + }, + }), + [resolvedStatusColor], + ); + + const assignedEngineer = data?.assignedEngineer; + const engineerInitials = + assignedEngineer && typeof assignedEngineer === "string" + ? assignedEngineer + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2) + : "--"; + + return ( + + + + + + {!focusMode && ( + <> + + + + + )} + + setActiveTab(newValue)} + onFocusModeToggle={() => setFocusMode((prev) => !prev)} + /> + + + + + + ); +} From 24b1ef4fa5c6e054545c6a34c321ecba8f942544 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:43:45 +0530 Subject: [PATCH 36/43] Add CaseDetailsSkeleton and update Typography mock Add a new CaseDetailsSkeleton component (apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsSkeleton.tsx) providing header-only and full skeleton UI for case details (ID, severity, status chip, title, avatar/engineer row, tabs and panel placeholder). Update the test mock for Typography in CasesList.test.tsx to forward onClick and other props to the rendered span so click handlers and prop-based behavior work in tests. --- .../cases-table/__tests__/CasesList.test.tsx | 6 +- .../header/CaseDetailsSkeleton.tsx | 119 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsSkeleton.tsx diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx index 02063af842..53399e9dff 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/__tests__/CasesList.test.tsx @@ -35,7 +35,11 @@ vi.mock("@wso2/oxygen-ui", () => ({ {children} ), TableRow: ({ children }: any) => {children}, - Typography: ({ children }: any) => {children}, + Typography: ({ children, onClick, ...rest }: any) => ( + + {children} + + ), Chip: ({ label }: any) => {label}, IconButton: ({ children }: any) => , Paper: ({ children }: any) =>
{children}
, diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsSkeleton.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsSkeleton.tsx new file mode 100644 index 0000000000..700a2c3970 --- /dev/null +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsSkeleton.tsx @@ -0,0 +1,119 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { Box, Divider, Paper, Skeleton, Stack } from "@wso2/oxygen-ui"; +import type { JSX } from "react"; + +/** + * Header-only skeleton: case ID, severity, status chip, and title. + * Used by CaseDetailsHeader when loading and by CaseDetailsSkeleton. + * + * @returns {JSX.Element} The header skeleton block. + */ +export function CaseDetailsHeaderSkeleton(): JSX.Element { + return ( + + + + + + + + + + + + ); +} + +/** + * Skeleton placeholder matching the case details UI: case ID, severity, status chip, title, avatar and engineer row, tabs, panel. + * + * @returns {JSX.Element} The rendered skeleton. + */ +export default function CaseDetailsSkeleton(): JSX.Element { + return ( + + + + {/* Action row: avatar, engineer, manage status, buttons */} + + + + + + + Support Engineer + + + + + + + + + + + {/* Tabs */} + + {[1, 2, 3, 4, 5].map((i) => ( + + ))} + + + {/* Panel content - placeholder text only, no skeleton */} + + + Activity timeline will appear here. + + + + ); +} From 3c8ffb5796a6689cbb15482294722fc5033894e0 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:44:43 +0530 Subject: [PATCH 37/43] Add CaseDetailsTabs component Introduce a new CaseDetailsTabs React component for the support case details header. The controlled component renders tabs from CASE_DETAILS_TABS using WSO2 UI components, applies custom styling, and optionally shows a focus-mode toggle button with accessible aria-labels and icons. Props: value, onChange, optional focusMode and onFocusModeToggle. --- .../dashboard/cases-table/CasesList.tsx | 8 +- .../case-details/header/CaseDetailsTabs.tsx | 94 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsTabs.tsx diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx index c338d75648..f87d66bb5c 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx @@ -114,7 +114,13 @@ const CasesList = ({
- + void; + focusMode?: boolean; + onFocusModeToggle?: () => void; +} + +/** + * Tab bar for case details: Activity, Details, Attachments, Calls, Knowledge Base. + * + * @param {CaseDetailsTabsProps} props - Controlled value and onChange. + * @returns {JSX.Element} The tabs wrapper. + */ +export default function CaseDetailsTabs({ + value, + onChange, + focusMode = false, + onFocusModeToggle, +}: CaseDetailsTabsProps): JSX.Element { + return ( + + + {CASE_DETAILS_TABS.map(({ label, Icon }) => ( + } + iconPosition="start" + label={label} + /> + ))} + + {onFocusModeToggle && ( + + )} + + ); +} From 9fc16d97cc6af7f61a68e585109d1d6bed23211e Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:58:11 +0530 Subject: [PATCH 38/43] Tighten initials parsing, layout and loading Improve robustness and UX across case pages: - CaseDetailsContent: normalize assignedEngineer before extracting initials by trimming, splitting on whitespace and filtering empty parts to handle extra/multiple spaces. - AppLayout: adjust sx padding logic to spread conditional styles so padding keys are applied at the expected top-level shape (fixes style/typing inconsistencies). - AllCasesPage: change loading logic to show the global loader only for the initial load (first stats + cases response) instead of during background refetches or pagination; remove unused isFetching and rename the flag to isInitialPageLoading to make intent clear. - Tests: remove obsolete DetailRow.test.tsx. These changes fix edge cases in initials generation, prevent spurious global loaders during background fetches, and tidy layout styling behavior. --- .../webapp/src/pages/CaseDetailsPage.tsx | 12 +++++++++++- apps/customer-portal/webapp/src/utils/support.ts | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/customer-portal/webapp/src/pages/CaseDetailsPage.tsx b/apps/customer-portal/webapp/src/pages/CaseDetailsPage.tsx index f3e5112ae3..40b3b18287 100644 --- a/apps/customer-portal/webapp/src/pages/CaseDetailsPage.tsx +++ b/apps/customer-portal/webapp/src/pages/CaseDetailsPage.tsx @@ -54,8 +54,18 @@ export default function CaseDetailsPage(): JSX.Element { }, [showSkeletons, showLoader, hideLoader]); const hasShownErrorRef = useRef(false); + + // Reset so error can show again when navigating to another case or when error clears. + useEffect(() => { + hasShownErrorRef.current = false; + }, [caseId]); + useEffect(() => { - if (isError && !hasShownErrorRef.current) { + if (!isError) { + hasShownErrorRef.current = false; + return; + } + if (!hasShownErrorRef.current) { hasShownErrorRef.current = true; showError("case details"); } diff --git a/apps/customer-portal/webapp/src/utils/support.ts b/apps/customer-portal/webapp/src/utils/support.ts index b75fcf14c1..0cef9ad096 100644 --- a/apps/customer-portal/webapp/src/utils/support.ts +++ b/apps/customer-portal/webapp/src/utils/support.ts @@ -175,7 +175,7 @@ export function deriveFilterLabels(id: string): { * Returns the icon component for a given case status label. * * @param statusLabel - The case status label (e.g., "Open", "Working in Progress"). - * @returns {ComponentType} The icon component. + * @returns {ComponentType<{ size?: number }>} The icon component. */ export function getStatusIcon(statusLabel?: string): ComponentType<{ size?: number }> { const normalized = statusLabel?.toLowerCase() || ""; From e14a45b67c8b9ec1b2559f16823969468b9e0e14 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:58:31 +0530 Subject: [PATCH 39/43] Normalize engineer initials; remove test; fix padding CaseDetailsContent: trim and split assignedEngineer using /\s+/ and filter(Boolean) before mapping to initials so extra spaces or empty segments don't produce incorrect initials. AppLayout: adjust sx padding logic to spread a conditional object ({ px, pb, pt } vs { p }) so the p styling is applied consistently depending on isCaseDetailsPage. Tests: remove obsolete DetailRow.test.tsx. --- .../header/CaseDetailsContent.tsx | 4 +- .../header/__tests__/DetailRow.test.tsx | 72 ------------------- .../webapp/src/layouts/AppLayout.tsx | 4 +- 3 files changed, 6 insertions(+), 74 deletions(-) delete mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/DetailRow.test.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx index a020d9939c..4219f3c60f 100644 --- a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx @@ -77,7 +77,9 @@ export default function CaseDetailsContent({ const engineerInitials = assignedEngineer && typeof assignedEngineer === "string" ? assignedEngineer - .split(" ") + .trim() + .split(/\s+/) + .filter(Boolean) .map((n) => n[0]) .join("") .toUpperCase() diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/DetailRow.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/DetailRow.test.tsx deleted file mode 100644 index 7e844e9829..0000000000 --- a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/DetailRow.test.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import { render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; -import DetailRow from "@case-details/DetailRow"; -import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; - -vi.mock("@/components/common/error-indicator/ErrorIndicator", () => ({ - default: ({ entityName }: { entityName: string }) => ( - {entityName} - ), -})); - -vi.mock("@utils/support", () => ({ - formatValue: (v: unknown) => (v == null || v === "" ? "--" : String(v)), -})); - -function renderDetailRow(props: { - label: string; - value?: string | null; - isLoading?: boolean; - isError?: boolean; -}) { - return render( - - - , - ); -} - -describe("DetailRow", () => { - it("should render label and value when not loading and not error", () => { - renderDetailRow({ label: "Product", value: "Choreo" }); - expect(screen.getByText("Product")).toBeInTheDocument(); - expect(screen.getByText("Choreo")).toBeInTheDocument(); - }); - - it("should render -- for null value", () => { - renderDetailRow({ label: "Account", value: null }); - expect(screen.getByText("--")).toBeInTheDocument(); - }); - - it("should render error indicator when isError is true", () => { - renderDetailRow({ label: "Description", value: "Text", isError: true }); - expect(screen.getByTestId("error-indicator")).toBeInTheDocument(); - }); - - it("should not show value when isLoading is true", () => { - renderDetailRow({ label: "Created", value: "2026-01-01", isLoading: true }); - expect(screen.getByText("Created")).toBeInTheDocument(); - expect(screen.queryByText("2026-01-01")).not.toBeInTheDocument(); - }); -}); diff --git a/apps/customer-portal/webapp/src/layouts/AppLayout.tsx b/apps/customer-portal/webapp/src/layouts/AppLayout.tsx index c2b9dea778..f86b2a025b 100644 --- a/apps/customer-portal/webapp/src/layouts/AppLayout.tsx +++ b/apps/customer-portal/webapp/src/layouts/AppLayout.tsx @@ -109,7 +109,9 @@ export default function AppLayout({ children }: AppLayoutProps): JSX.Element { sx={{ flex: 1, overflow: "auto", - p: isCaseDetailsPage ? { px: 3, pb: 3, pt: 0 } : 3, + ...(isCaseDetailsPage + ? { px: 3, pb: 3, pt: 0 } + : { p: 3 }), }} > {children || ( From 5263ec801797127d77166bc7c7fbdaac437e4436 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 11:58:46 +0530 Subject: [PATCH 40/43] Show loader only for initial page load Stop treating background fetches as page-loading. Remove isCasesFetching from the page-loading calculation, rename isPageLoading to isInitialPageLoading, and update the effect/dependency list so the global loader is shown only until the first stats and cases responses arrive (not on refetches or fetchNextPage). This prevents the loader from appearing during background updates. --- apps/customer-portal/webapp/src/pages/AllCasesPage.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx b/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx index 3c4a37ed89..a666f7b480 100644 --- a/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx +++ b/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx @@ -89,14 +89,13 @@ export default function AllCasesPage(): JSX.Element { const { data, isLoading: isCasesQueryLoading, - isFetching: isCasesFetching, hasNextPage, fetchNextPage, } = useGetProjectCases(projectId || "", caseSearchRequest); const { showLoader, hideLoader } = useLoader(); - // Show skeletons until each section has a response (covers query disabled by auth + initial fetch). + // Show loader only for initial load (until first stats + cases response), not for background refetches or fetchNextPage. const hasStatsResponse = stats !== undefined; const hasCasesResponse = data !== undefined; const isStatsLoading = @@ -104,15 +103,15 @@ export default function AllCasesPage(): JSX.Element { const isCasesAreaLoading = isCasesQueryLoading || (!!projectId && !hasCasesResponse); - const isPageLoading = isStatsLoading || isCasesAreaLoading || isCasesFetching; + const isInitialPageLoading = isStatsLoading || isCasesAreaLoading; useEffect(() => { - if (isPageLoading) { + if (isInitialPageLoading) { showLoader(); return () => hideLoader(); } hideLoader(); - }, [isPageLoading, showLoader, hideLoader]); + }, [isInitialPageLoading, showLoader, hideLoader]); // Background-load all remaining pages so search/filters work on full dataset. useEffect(() => { From c6b6c68c650d41be0bd614ab5b40f924769f4a6f Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 12:01:27 +0530 Subject: [PATCH 41/43] Simplify CaseDetailsTabPanels props; remove DetailRow Refactor case details header: CaseDetailsTabPanels no longer receives case data, loading or error flags and only takes activeTab. Updated CaseDetailsContent to match by passing only activeTab. Removed the DetailRow component (deleted file) and cleaned up unused imports/props and JSDoc in CaseDetailsTabPanels. This simplifies the tab panel responsibilities and removes an unused/detail-level component. --- .../header/CaseDetailsContent.tsx | 6 +- .../header/CaseDetailsTabPanels.tsx | 6 +- .../support/case-details/header/DetailRow.tsx | 74 ------------------- 3 files changed, 2 insertions(+), 84 deletions(-) delete mode 100644 apps/customer-portal/webapp/src/components/support/case-details/header/DetailRow.tsx diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx index 4219f3c60f..2087dba71b 100644 --- a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsContent.tsx @@ -131,11 +131,7 @@ export default function CaseDetailsContent({ /> - + ); diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsTabPanels.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsTabPanels.tsx index b77fe849b5..6438ac544a 100644 --- a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsTabPanels.tsx +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsTabPanels.tsx @@ -16,19 +16,15 @@ import { Box, Typography } from "@wso2/oxygen-ui"; import { type JSX } from "react"; -import type { CaseDetails } from "@models/responses"; export interface CaseDetailsTabPanelsProps { activeTab: number; - data: CaseDetails | undefined; - isError: boolean; - isLoading?: boolean; } /** * Renders the active tab panel for case details (Activity, Details, Attachments, Calls, Knowledge Base). * - * @param {CaseDetailsTabPanelsProps} props - Active tab index, case data, and error state. + * @param {CaseDetailsTabPanelsProps} props - Active tab index. * @returns {JSX.Element | null} The panel content. */ export default function CaseDetailsTabPanels({ diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/DetailRow.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/DetailRow.tsx deleted file mode 100644 index f44cdd0929..0000000000 --- a/apps/customer-portal/webapp/src/components/support/case-details/header/DetailRow.tsx +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import ErrorIndicator from "@/components/common/error-indicator/ErrorIndicator"; -import { Box, Typography } from "@wso2/oxygen-ui"; -import { type JSX } from "react"; -import { formatValue } from "@utils/support"; - -export interface DetailRowProps { - label: string; - value: string | null | undefined; - isLoading: boolean; - isError: boolean; -} - -/** - * Single label-value row for case details (e.g. Description, Created, Product). - * - * @param {DetailRowProps} props - Label, value, and loading/error state. - * @returns {JSX.Element} The row. - */ -export default function DetailRow({ - label, - value, - isLoading, - isError, -}: DetailRowProps): JSX.Element { - return ( - - - {label} - - {isError ? ( - - ) : isLoading ? ( - - ) : ( - - {formatValue(value)} - - )} - - ); -} From 82a980f0fb4bf6556aba18229561bdc2eed05f46 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 12:02:02 +0530 Subject: [PATCH 42/43] Refactor BackButton sx type and test props Replace the inline sx prop type with SxProps in CaseDetailsBackButton for proper typing from @wso2/oxygen-ui. Update the CaseDetailsTabPanels test to only pass the required activeTab prop, removing unnecessary/undefined props to match the component's expected signature. --- .../support/case-details/header/CaseDetailsBackButton.tsx | 4 ++-- .../header/__tests__/CaseDetailsTabPanels.test.tsx | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsBackButton.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsBackButton.tsx index 5026bea851..e1069f3e54 100644 --- a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsBackButton.tsx +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsBackButton.tsx @@ -14,13 +14,13 @@ // specific language governing permissions and limitations // under the License. -import { Button } from "@wso2/oxygen-ui"; +import { Button, type SxProps, type Theme } from "@wso2/oxygen-ui"; import { ArrowLeft } from "@wso2/oxygen-ui-icons-react"; import { type JSX } from "react"; export interface CaseDetailsBackButtonProps { onClick: () => void; - sx?: { mb?: number; ml?: number; alignSelf?: string }; + sx?: SxProps; } /** diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabPanels.test.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabPanels.test.tsx index dcb366a56e..31d6e2db54 100644 --- a/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabPanels.test.tsx +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/__tests__/CaseDetailsTabPanels.test.tsx @@ -22,11 +22,7 @@ import { ThemeProvider, createTheme } from "@wso2/oxygen-ui"; function renderTabPanels(activeTab: number) { return render( - + , ); } From 87800fe0cb4137d05b114e8fa0720e66af7ad3c5 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Thu, 12 Feb 2026 12:06:27 +0530 Subject: [PATCH 43/43] Move case status actions to support constants Extract CaseStatusAction, CaseStatusPaletteIntent and CASE_STATUS_ACTIONS from CaseDetailsActionRow into supportConstants. Update component to import the actions and the palette intent type, remove duplicate local definitions and unused icon imports, and keep styling function referencing the exported intent type. This centralizes action config and icon imports for reuse and reduces duplication. --- .../header/CaseDetailsActionRow.tsx | 31 +++++-------------- .../webapp/src/constants/supportConstants.ts | 21 +++++++++++++ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx index f7b50b96ae..56e6800589 100644 --- a/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx +++ b/apps/customer-portal/webapp/src/components/support/case-details/header/CaseDetailsActionRow.tsx @@ -27,37 +27,20 @@ import { useTheme, type Theme, } from "@wso2/oxygen-ui"; -import { - CircleCheck, - CirclePause, - CircleX, - CirclePlay, - TriangleAlert, -} from "@wso2/oxygen-ui-icons-react"; -import { type JSX, type ComponentType } from "react"; +import { CirclePlay } from "@wso2/oxygen-ui-icons-react"; +import { type JSX } from "react"; import ErrorIndicator from "@components/common/error-indicator/ErrorIndicator"; +import { + CASE_STATUS_ACTIONS, + type CaseStatusPaletteIntent, +} from "@constants/supportConstants"; import { formatValue } from "@utils/support"; -type PaletteIntent = "error" | "warning" | "success" | "info"; - -interface CaseStatusAction { - label: string; - Icon: ComponentType<{ size?: number }>; - paletteIntent: PaletteIntent; -} - -const CASE_STATUS_ACTIONS: CaseStatusAction[] = [ - { label: "Escalate Case", Icon: TriangleAlert, paletteIntent: "error" }, - { label: "Waiting on WSO2", Icon: CirclePause, paletteIntent: "warning" }, - { label: "Mark as Resolved", Icon: CircleCheck, paletteIntent: "success" }, - { label: "Close Case", Icon: CircleX, paletteIntent: "info" }, -]; - const ACTION_BUTTON_ICON_SIZE = 12; function getActionButtonSx( theme: Theme, - intent: PaletteIntent, + intent: CaseStatusPaletteIntent, ): Record { const light = theme.palette[intent].light; return { diff --git a/apps/customer-portal/webapp/src/constants/supportConstants.ts b/apps/customer-portal/webapp/src/constants/supportConstants.ts index acc7228f72..7724ba9d90 100644 --- a/apps/customer-portal/webapp/src/constants/supportConstants.ts +++ b/apps/customer-portal/webapp/src/constants/supportConstants.ts @@ -20,7 +20,9 @@ import { Bot, CircleAlert, CircleCheck, + CirclePause, CircleQuestionMark, + CircleX, Clock, FileText, Info, @@ -28,6 +30,7 @@ import { MessageSquare, Paperclip, Phone, + TriangleAlert, TrendingUp, Zap, } from "@wso2/oxygen-ui-icons-react"; @@ -198,6 +201,24 @@ export const CASE_DETAILS_TABS: CaseDetailsTabConfig[] = [ { label: "Knowledge Base (0)", Icon: BookOpen }, ]; +//Palette intent for case status action buttons. +export type CaseStatusPaletteIntent = "error" | "warning" | "success" | "info"; + +// Case status action (e.g. Escalate, Mark as Resolved) for the action row. +export interface CaseStatusAction { + label: string; + Icon: ComponentType<{ size?: number }>; + paletteIntent: CaseStatusPaletteIntent; +} + +// Case status actions shown in the case details action row. +export const CASE_STATUS_ACTIONS: CaseStatusAction[] = [ + { label: "Escalate Case", Icon: TriangleAlert, paletteIntent: "error" }, + { label: "Waiting on WSO2", Icon: CirclePause, paletteIntent: "warning" }, + { label: "Mark as Resolved", Icon: CircleCheck, paletteIntent: "success" }, + { label: "Close Case", Icon: CircleX, paletteIntent: "info" }, +]; + // Number of outstanding cases to show on support overview cards. export const SUPPORT_OVERVIEW_CASES_LIMIT = 5;