Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
e86be54
Add TabBar unit tests
dileepapeiris Feb 3, 2026
9517f85
Add TabBar component
dileepapeiris Feb 3, 2026
e39b2d9
Add project details tab constants
dileepapeiris Feb 3, 2026
3ac9473
Render ProjectDetails for project-details route
dileepapeiris Feb 3, 2026
b5a9610
Add ProjectDetails page with tabs
dileepapeiris Feb 3, 2026
150cdd8
Add tests for useGetProjectDetails hook
dileepapeiris Feb 3, 2026
8c74949
Add tests for useGetProjectStat hook
dileepapeiris Feb 3, 2026
c2b3603
Add useGetProjectDetails React Query hook
dileepapeiris Feb 3, 2026
d31566d
Add tests for projectStats utils
dileepapeiris Feb 3, 2026
9685fa1
Add projectStats utils for dates & chip colors
dileepapeiris Feb 3, 2026
35a47af
Render project overview with data and loader
dileepapeiris Feb 3, 2026
2353f6f
Use project details in CreateCasePage
dileepapeiris Feb 3, 2026
0b6ce7c
CreateCasePage tests: mock project hook/icons
dileepapeiris Feb 3, 2026
f3856c6
Add useGetProjectStat hook for project stats
dileepapeiris Feb 3, 2026
08c04ed
Active filters: use warning color; drop prop
dileepapeiris Feb 3, 2026
b657812
Add project details and stats interfaces
dileepapeiris Feb 3, 2026
947d4c2
Add tests for ProjectName component
dileepapeiris Feb 3, 2026
2321a5b
Add tests for ProjectHeader
dileepapeiris Feb 3, 2026
0fe28a9
Add ProjectInformationCard unit tests
dileepapeiris Feb 3, 2026
effdcaf
Add tests for ProjectMetadata component
dileepapeiris Feb 3, 2026
0b75e8a
Add tests for ProjectDescription component
dileepapeiris Feb 3, 2026
891074f
Add ContactInfoCard unit tests
dileepapeiris Feb 3, 2026
cb2c4a1
Add ContactRow unit tests
dileepapeiris Feb 3, 2026
4bc13df
Add ProjectMetadata component
dileepapeiris Feb 3, 2026
a3554f3
Enhance CaseDetailsSection tests and mocks
dileepapeiris Feb 3, 2026
14507de
Make basic info fields read-only with edit mode
dileepapeiris Feb 3, 2026
2579789
Use outlined variant for AI Generated chip
dileepapeiris Feb 3, 2026
80de4c0
Add RecentActivityCard component
dileepapeiris Feb 3, 2026
a4abcc9
Add getMockProjectStats mock and import
dileepapeiris Feb 3, 2026
05c9fbd
Add mockProjectDetails with subscription data
dileepapeiris Feb 3, 2026
db2bcd0
Pass sidebar state to Outlet via context
dileepapeiris Feb 3, 2026
39386fd
Memoize loader show/hide handlers with useCallback
dileepapeiris Feb 3, 2026
ac17843
Mock loader context and add loader tests
dileepapeiris Feb 3, 2026
8619fea
Add project details constants and helpers
dileepapeiris Feb 3, 2026
535632c
Add ContactRow component for contacts
dileepapeiris Feb 3, 2026
fb6d233
Add RecentActivityCard unit tests
dileepapeiris Feb 3, 2026
1985e8b
Remove projectDetailsConstants.ts
dileepapeiris Feb 3, 2026
d437c88
Add ProjectName component
dileepapeiris Feb 3, 2026
a3ba21a
Add edit toggle to CaseDetailsSection
dileepapeiris Feb 3, 2026
2dc6b46
Add project-related API query keys
dileepapeiris Feb 3, 2026
6593525
Use primary color for Create Case button
dileepapeiris Feb 3, 2026
d6e94be
Use shared Message type in ChatMessageList
dileepapeiris Feb 3, 2026
188c60b
Import shared Message type
dileepapeiris Feb 3, 2026
201c4c0
Update BasicInformationSection tests
dileepapeiris Feb 3, 2026
035ae65
Add ProjectStatisticsCard component
dileepapeiris Feb 3, 2026
9c4271c
Add tests for ProjectStatisticsCard
dileepapeiris Feb 3, 2026
4a3fb69
Add SubscriptionDetails component
dileepapeiris Feb 3, 2026
b75ce61
Add ProjectInformationCard component
dileepapeiris Feb 3, 2026
4e4eb2d
Add ProjectHeader component
dileepapeiris Feb 3, 2026
abe4ce5
Add ProjectDescription component
dileepapeiris Feb 3, 2026
352822e
Add SubscriptionDetails component tests
dileepapeiris Feb 3, 2026
c1343c4
Add ContactInfoCard component
dileepapeiris Feb 3, 2026
d5f6e73
Calculate subscription progress and adjust UI
dileepapeiris Feb 3, 2026
b585705
Add calculateProgress util to projectStats
dileepapeiris Feb 3, 2026
7c83622
Add calculateProgress tests to projectStats
dileepapeiris Feb 3, 2026
438d4ca
Log project/stats errors in useEffect
dileepapeiris Feb 3, 2026
92750fd
Update SubscriptionDetails test progress mock
dileepapeiris Feb 3, 2026
20f64a4
Count loader usages to avoid premature hide
dileepapeiris Feb 3, 2026
b68fcb0
Add ref-counting test for LoaderContext
dileepapeiris Feb 3, 2026
dde934c
Add ProjectDetails unit tests
dileepapeiris Feb 3, 2026
656c6fc
Add aria-label to edit icon
dileepapeiris Feb 3, 2026
272a853
Set project metadata labels to medium weight
dileepapeiris Feb 3, 2026
49ee7b2
Refactor loading effect and update test mock
dileepapeiris Feb 3, 2026
c51a855
Add test to hide loader after loading
dileepapeiris Feb 3, 2026
7cabe9e
Remove redundant projectId prop
dileepapeiris Feb 3, 2026
d2ae486
Merge branch 'customer-portal-milestone-1' into feat/add-project-over…
Rashmika998 Feb 4, 2026
a23380e
Use SubscriptionStatus enum in subscription utils
dileepapeiris Feb 4, 2026
cca40fb
Add SubscriptionStatus enum and type
dileepapeiris Feb 4, 2026
730c9e2
Use SubscriptionStatus enum for status checks
dileepapeiris Feb 4, 2026
6612b30
Use SubscriptionStatus enum in tests
dileepapeiris Feb 4, 2026
d201b8d
Remove outdated comment in SubscriptionDetails test
dileepapeiris Feb 4, 2026
435123b
Use project detail constants
dileepapeiris Feb 4, 2026
f6c7899
Remove SubscriptionStatus enum/type
dileepapeiris Feb 4, 2026
11585e2
Use project/case constants in mockData
dileepapeiris Feb 4, 2026
524b1f7
Add project-related constants and types
dileepapeiris Feb 4, 2026
e185aef
Use SUBSCRIPTION_STATUS constant
dileepapeiris Feb 4, 2026
1dc1296
Use SUBSCRIPTION_STATUS constant in tests
dileepapeiris Feb 4, 2026
97b5599
Remove unused props from Header and Card mock
dileepapeiris Feb 4, 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: 2 additions & 4 deletions apps/customer-portal/webapp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { JSX } from "react";
import { BrowserRouter, Route, Routes, Navigate } from "react-router";
Comment thread
dileepapeiris marked this conversation as resolved.
import ProjectHub from "@/pages/ProjectHub";
import ProjectPage from "@/pages/ProjectPage";
import ProjectDetails from "@/pages/ProjectDetails";
import DashboardPage from "@/pages/DashboardPage";
import SupportPage from "@/pages/SupportPage";
import NoveraChatPage from "@/pages/NoveraChatPage";
Expand All @@ -41,10 +42,7 @@ export default function App(): JSX.Element {
<Route index element={<Navigate to="dashboard" replace />} />
<Route path="dashboard" element={<DashboardPage />} />
{/* Project Details */}
<Route
path="project-details"
element={<ProjectPage title="Project Details" />}
/>
<Route path="project-details" element={<ProjectDetails />} />
{/* Support */}
<Route path="support">
<Route index element={<SupportPage />} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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 { renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import useGetProjectDetails from "@/api/useGetProjectDetails";
import { mockProjectDetails } from "@/models/mockData";
import type { ReactNode } from "react";

vi.mock("@/constants/apiConstants", async (importOriginal) => {
const actual = (await importOriginal()) as any;
return {
...actual,
API_MOCK_DELAY: 0,
};
});

// Mock useLogger
vi.mock("@/hooks/useLogger", () => ({
useLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));

const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

describe("useGetProjectDetails", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("should return project details for a valid project ID", async () => {
const validProjectId = mockProjectDetails[0].id;

const { result } = renderHook(() => useGetProjectDetails(validProjectId), {
wrapper: createWrapper(),
});

await waitFor(() => expect(result.current.isSuccess).toBe(true));

const response = result.current.data;
expect(response).toBeDefined();
expect(response?.id).toBe(validProjectId);
expect(response?.name).toBe(mockProjectDetails[0].name);
});

it("should error for an invalid project ID", async () => {
const invalidProjectId = "invalid-id";

const { result } = renderHook(
() => useGetProjectDetails(invalidProjectId),
{
wrapper: createWrapper(),
},
);

await waitFor(() => expect(result.current.isError).toBe(true));

expect(result.current.error).toBeDefined();
expect(result.current.error?.message).toContain(
"Project with ID invalid-id not found",
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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 { renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useGetProjectStat } from "@/api/useGetProjectStat";
import { mockProjects } from "@/models/mockData";
import type { ReactNode } from "react";

vi.mock("@/constants/apiConstants", async (importOriginal) => {
const actual = (await importOriginal()) as any;
return {
...actual,
API_MOCK_DELAY: 0,
};
});

// Mock useLogger
vi.mock("@/hooks/useLogger", () => ({
useLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));

const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

describe("useGetProjectStat", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("should return project stats for a valid project ID", async () => {
const validProjectId = mockProjects[0].id;

const { result } = renderHook(() => useGetProjectStat(validProjectId), {
wrapper: createWrapper(),
});

await waitFor(() => expect(result.current.isSuccess).toBe(true));

const response = result.current.data;
expect(response).toBeDefined();
expect(response?.projectStats).toBeDefined();
expect(response?.recentActivity).toBeDefined();
});

it("should error for an invalid project ID", async () => {
const invalidProjectId = "invalid-id";

const { result } = renderHook(() => useGetProjectStat(invalidProjectId), {
wrapper: createWrapper(),
});

await waitFor(() => expect(result.current.isError).toBe(true));

expect(result.current.error).toBeDefined();
expect(result.current.error?.message).toContain(
"Project stats not found for ID: invalid-id",
);
});
});
60 changes: 60 additions & 0 deletions apps/customer-portal/webapp/src/api/useGetProjectDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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 { useQuery, type UseQueryResult } from "@tanstack/react-query";
import { useLogger } from "@/hooks/useLogger";
import { mockProjectDetails } from "@/models/mockData";
import { ApiQueryKeys, API_MOCK_DELAY } from "@/constants/apiConstants";
import type { ProjectDetails } from "@/models/responses";

/**
* Custom hook to fetch detailed project information by ID.
*
* @param {string} projectId - The ID of the project to fetch details for.
* @returns {UseQueryResult<ProjectDetails, Error>} The query result object.
*/
export default function useGetProjectDetails(
projectId: string,
): UseQueryResult<ProjectDetails, Error> {
const logger = useLogger();

return useQuery<ProjectDetails, Error>({
queryKey: [ApiQueryKeys.PROJECT_DETAILS, projectId],
queryFn: async (): Promise<ProjectDetails> => {
logger.debug(`Fetching project details for project ID: ${projectId}`);

// Mock behavior: simulate network latency.
await new Promise((resolve) => setTimeout(resolve, API_MOCK_DELAY));

// Find project by ID from mock data.
const project = mockProjectDetails.find((p) => p.id === projectId);

if (!project) {
logger.error(`Project not found for ID: ${projectId}`);
throw new Error(`Project with ID ${projectId} not found`);
}

logger.debug(
`Project details fetched successfully for project ID: ${projectId}`,
project,
);

return project;
},
enabled: !!projectId,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
61 changes: 61 additions & 0 deletions apps/customer-portal/webapp/src/api/useGetProjectStat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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 { useQuery, type UseQueryResult } from "@tanstack/react-query";
import { getMockProjectStats } from "@/models/mockFunctions";
import { mockProjects } from "@/models/mockData";
import { useLogger } from "@/hooks/useLogger";
import { ApiQueryKeys, API_MOCK_DELAY } from "@/constants/apiConstants";
import type { ProjectStatsResponse } from "@/models/responses";

/**
* Custom hook to fetch project statistics.
*
* @param {string} projectId - The ID of the project.
* @returns {UseQueryResult<ProjectStatsResponse, Error>} The query result object.
*/
export function useGetProjectStat(
projectId: string,
): UseQueryResult<ProjectStatsResponse, Error> {
const logger = useLogger();

return useQuery<ProjectStatsResponse, Error>({
queryKey: [ApiQueryKeys.PROJECT_STATS, projectId],
queryFn: async (): Promise<ProjectStatsResponse> => {
logger.debug(`Fetching project stats for project ID: ${projectId}`);

// Mock behavior: simulate network latency.
await new Promise((resolve) => setTimeout(resolve, API_MOCK_DELAY));

// Validate project ID
const projectExists = mockProjects.some((p) => p.id === projectId);
if (!projectExists) {
throw new Error(`Project stats not found for ID: ${projectId}`);
}

const stats: ProjectStatsResponse = getMockProjectStats();

logger.debug(
`Project stats fetched successfully for project ID: ${projectId}`,
stats,
);

return stats;
},
enabled: !!projectId,
staleTime: 5 * 60 * 1000,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const ActiveFilters = <T extends Record<string, any>>({
icon={hasOptions ? <ChevronDown size={14} /> : undefined}
size="small"
variant="outlined"
color={isActive ? "primary" : "default"}
color={isActive ? "warning" : "default"}
sx={{
"& .MuiChip-label": { order: 1, pr: 1 },
"& .MuiChip-icon": { order: 2, mr: 0.5, ml: 0 },
Expand All @@ -121,7 +121,7 @@ const ActiveFilters = <T extends Record<string, any>>({
const optionValue =
typeof option === "string" ? option : option.value;
const isSelected =
appliedFilters[activeField.id] === optionLabel || // Check against display label which is currently stored in appliesFilters map (need to verify this assumption from CasesTable)
appliedFilters[activeField.id] === optionLabel ||
appliedFilters[activeField.id] === optionValue;

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ interface HeaderProps {
* @param {HeaderProps} props - The props for the component.
* @returns {JSX.Element} The Header component.
*/
export default function Header({
onToggleSidebar,
collapsed = false,
}: HeaderProps): JSX.Element {
export default function Header({ onToggleSidebar }: HeaderProps): JSX.Element {
const navigate = useNavigate();
const location = useLocation();
const logger = useLogger();
Expand Down
Loading