Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
330922d
Add @case-details alias to Vite config
dileepapeiris Feb 12, 2026
0e6733b
Format renderHook invocation in test
dileepapeiris Feb 12, 2026
2f21b54
Refactor test formatting and type annotations
dileepapeiris Feb 12, 2026
e03b012
Reformat useGetProductUpdatesStats tests
dileepapeiris Feb 12, 2026
540e4e0
Add case details route under support cases
dileepapeiris Feb 12, 2026
cf34a17
Format enabled condition in useGetCaseAttachments
dileepapeiris Feb 12, 2026
263a3ac
Add CaseDetailsSkeleton header test
dileepapeiris Feb 12, 2026
f6056ab
Add tests for CaseDetailsContent header
dileepapeiris Feb 12, 2026
816d216
Refactor test typings and window config casts
dileepapeiris Feb 12, 2026
b37cad2
Add optional onCaseClick handler to AllCasesList
dileepapeiris Feb 12, 2026
312fe44
Remove project prefix from case details API
dileepapeiris Feb 12, 2026
ed67c8a
Add CaseDetailField unit tests
dileepapeiris Feb 12, 2026
3d35c21
Compact logger.debug message
dileepapeiris Feb 12, 2026
a5b799b
Add tests for CaseDetailsActionRow component
dileepapeiris Feb 12, 2026
e2ffdd2
Add tests for CaseDetailsBackButton
dileepapeiris Feb 12, 2026
c77784e
Add CaseDetailsPage loading state tests
dileepapeiris Feb 12, 2026
9bd397c
Add JSDoc for mock case classification
dileepapeiris Feb 12, 2026
4ce43c9
Add case details tabs config & icons
dileepapeiris Feb 12, 2026
1b74093
Add DetailRow component for case details
dileepapeiris Feb 12, 2026
d5d0948
Add CaseDetailsTabPanels component
dileepapeiris Feb 12, 2026
49dd303
Add CaseDetailsBackButton component
dileepapeiris Feb 12, 2026
94dbae9
Add tests for CaseDetailsHeader component
dileepapeiris Feb 12, 2026
bb98862
Add tests for CaseDetailsTabPanels
dileepapeiris Feb 12, 2026
d6519de
Add @case-details TS path aliases
dileepapeiris Feb 12, 2026
2117035
Add formatValue and getStatusIconElement helpers
dileepapeiris Feb 12, 2026
c66cefd
Add tests for CaseDetailsTabs component
dileepapeiris Feb 12, 2026
69840f8
Add tests for DetailRow component
dileepapeiris Feb 12, 2026
2dee80d
Add CaseDetailField component
dileepapeiris Feb 12, 2026
3a0f4b3
Add CaseDetailsActionRow component
dileepapeiris Feb 12, 2026
9adea53
Add tests for formatValue utility
dileepapeiris Feb 12, 2026
c64406b
Add CaseDetailsPage component
dileepapeiris Feb 12, 2026
20b7de6
Show loader during fetch and add case click
dileepapeiris Feb 12, 2026
7644207
Make case titles clickable and adjust layout
dileepapeiris Feb 12, 2026
2e306fc
Add CaseDetailsHeader component
dileepapeiris Feb 12, 2026
02ccfdf
Add CaseDetailsContent component
dileepapeiris Feb 12, 2026
24b1ef4
Add CaseDetailsSkeleton and update Typography mock
dileepapeiris Feb 12, 2026
3c8ffb5
Add CaseDetailsTabs component
dileepapeiris Feb 12, 2026
7541eb8
Merge branch 'customer-portal-milestone-1' into feat/add-specific-cas…
dileepapeiris Feb 12, 2026
9fc16d9
Tighten initials parsing, layout and loading
dileepapeiris Feb 12, 2026
e14a45b
Normalize engineer initials; remove test; fix padding
dileepapeiris Feb 12, 2026
5263ec8
Show loader only for initial page load
dileepapeiris Feb 12, 2026
c6b6c68
Simplify CaseDetailsTabPanels props; remove DetailRow
dileepapeiris Feb 12, 2026
82a980f
Refactor BackButton sx type and test props
dileepapeiris Feb 12, 2026
87800fe
Move case status actions to support constants
dileepapeiris Feb 12, 2026
463aacc
Merge branch 'customer-portal-milestone-1' into feat/add-specific-cas…
dileepapeiris Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/customer-portal/webapp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -57,7 +58,10 @@ export default function App(): JSX.Element {
{/* Support */}
<Route path="support">
<Route index element={<SupportPage />} />
<Route path="cases" element={<AllCasesPage />} />
<Route path="cases">
<Route index element={<AllCasesPage />} />
<Route path=":caseId" element={<CaseDetailsPage />} />
</Route>
<Route path="chat">
<Route index element={<NoveraChatPage />} />
<Route path="create-case" element={<CreateCasePage />} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ vi.mock("@hooks/useLogger", () => ({
}));

vi.mock("@constants/apiConstants", async (importOriginal) => {
const actual = (await importOriginal()) as { ApiQueryKeys: Record<string, string> };
const actual = (await importOriginal()) as {
ApiQueryKeys: Record<string, string>;
};
return {
...actual,
API_MOCK_DELAY: 0,
Expand Down Expand Up @@ -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",
};

Expand Down Expand Up @@ -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",
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ vi.mock("@hooks/useLogger", () => ({
}));

vi.mock("@constants/apiConstants", async (importOriginal) => {
const actual = (await importOriginal()) as { ApiQueryKeys: Record<string, string> };
const actual = (await importOriginal()) as {
ApiQueryKeys: Record<string, string>;
};
return {
...actual,
API_MOCK_DELAY: 0,
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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",
};

Expand All @@ -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));

Expand All @@ -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",
};

Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ vi.mock("@hooks/useLogger", () => ({
}));

vi.mock("@constants/apiConstants", async (importOriginal) => {
const actual = (await importOriginal()) as { ApiQueryKeys: Record<string, string> };
const actual = (await importOriginal()) as {
ApiQueryKeys: Record<string, string>;
};
return {
...actual,
API_MOCK_DELAY: 0,
Expand Down Expand Up @@ -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",
};

Expand Down Expand Up @@ -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",
};

Expand Down
3 changes: 1 addition & 2 deletions apps/customer-portal/webapp/src/api/useGetCaseAttachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
2 changes: 1 addition & 1 deletion apps/customer-portal/webapp/src/api/useGetCaseDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Comment thread
dileepapeiris marked this conversation as resolved.
method: "GET",
headers: addApiHeaders(idToken),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ export function useGetProductUpdateLevels(): UseQueryResult<
return useQuery<ProductUpdateLevelsResponse, Error>({
queryKey: [ApiQueryKeys.PRODUCT_UPDATE_LEVELS, isMockEnabled],
queryFn: async (): Promise<ProductUpdateLevelsResponse> => {
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -44,6 +44,8 @@ interface CasesListProps {
rowsPerPage: number;
onPageChange: (event: unknown, newPage: number) => void;
onRowsPerPageChange: (event: ChangeEvent<HTMLInputElement>) => void;
/** When provided, case title is clickable and navigates to case details. */
onCaseClick?: (caseItem: CaseListItem) => void;
}

const CasesList = ({
Expand All @@ -54,6 +56,7 @@ const CasesList = ({
rowsPerPage,
onPageChange,
onRowsPerPageChange,
onCaseClick,
}: CasesListProps): JSX.Element => {
return (
<>
Expand Down Expand Up @@ -111,17 +114,46 @@ const CasesList = ({
</Box>
</TableCell>
<TableCell>
<Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 0.25,
}}
>
<Typography
component="span"
variant="body2"
color="text.primary"
role={onCaseClick ? "button" : undefined}
tabIndex={onCaseClick ? 0 : undefined}
onKeyDown={
onCaseClick
? (e) => {
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 || "--"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ vi.mock("@wso2/oxygen-ui", () => ({
<thead data-testid="table-head">{children}</thead>
),
TableRow: ({ children }: any) => <tr data-testid="table-row">{children}</tr>,
Typography: ({ children }: any) => <span>{children}</span>,
Typography: ({ children, onClick, ...rest }: any) => (
<span onClick={onClick} {...rest}>
{children}
</span>
),
Chip: ({ label }: any) => <span data-testid="chip">{label}</span>,
IconButton: ({ children }: any) => <button>{children}</button>,
Paper: ({ children }: any) => <div>{children}</div>,
Expand Down Expand Up @@ -193,6 +197,49 @@ describe("CasesList", () => {
expect(mockOnRowsPerPageChange).toHaveBeenCalled();
});

it("should call onCaseClick with case when case title is clicked", () => {
const mockOnCaseClick = vi.fn();
render(
<CasesList
isLoading={false}
data={mockData}
page={0}
rowsPerPage={10}
onPageChange={mockOnPageChange}
onRowsPerPageChange={mockOnRowsPerPageChange}
onCaseClick={mockOnCaseClick}
/>,
);

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(
<CasesList
isLoading={false}
data={mockData}
page={0}
rowsPerPage={10}
onPageChange={mockOnPageChange}
onRowsPerPageChange={mockOnRowsPerPageChange}
/>,
);

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: [
Expand Down
Loading