Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5adfce4
Add .coderabbit.yaml file
Rashmika998 Jan 13, 2026
626649a
format code
Rashmika998 Jan 13, 2026
ea173f1
Update base branches
Rashmika998 Jan 13, 2026
5ca71e0
Merge pull request #12 from Rashmika998/coderabbit-patch-1-1
suhand Jan 13, 2026
a435012
Add ProjectCard component
dileepapeiris Jan 31, 2026
c9662ea
Add ProjectCardActions component
dileepapeiris Jan 31, 2026
57d63af
Add project card date & status utils
dileepapeiris Jan 31, 2026
24d42df
Add tests for projectCard utilities
dileepapeiris Jan 31, 2026
3547899
ProjectSwitcher: use project id instead of key
dileepapeiris Jan 31, 2026
e198ab0
Use absolute import for UserProfile
dileepapeiris Jan 31, 2026
94e4f05
Use project id instead of key in Header
dileepapeiris Jan 31, 2026
0f64b75
Type results as SearchProjectsResponse
dileepapeiris Jan 31, 2026
9968e3f
Use absolute import in useSearchProjects test
dileepapeiris Jan 31, 2026
d78e366
Use path alias for Brand import in test
dileepapeiris Jan 31, 2026
c046f8a
Use project id in Header tests
dileepapeiris Jan 31, 2026
2c8cff0
Add ProjectCardSkeleton component
dileepapeiris Jan 31, 2026
5728393
Add ProjectCardStats component
dileepapeiris Jan 31, 2026
42dc95f
Add ProjectCardBadges component
dileepapeiris Jan 31, 2026
d58001e
Add ProjectCardInfo component
dileepapeiris Jan 31, 2026
cc9e9ad
Update mock projects, user, and options
dileepapeiris Jan 31, 2026
5efce1d
Wrap Outlet with Box for padding and scrolling
dileepapeiris Jan 31, 2026
f1283cc
Use absolute import for SubscriptionWidget
dileepapeiris Jan 31, 2026
0d215cb
Use alias import and mock Box in AppLayout test
dileepapeiris Jan 31, 2026
7c74781
Use alias import in SubscriptionWidget test
dileepapeiris Jan 31, 2026
c2d6fe7
Add mock utility functions for customer portal
dileepapeiris Jan 31, 2026
d64510c
Render responsive ProjectCard grid with skeletons
dileepapeiris Jan 31, 2026
e334ac9
Add tests for ProjectHub page
dileepapeiris Jan 31, 2026
43e8e38
Add Logger utils tests
dileepapeiris Jan 31, 2026
3299e62
Use absolute import for SideBar in tests
dileepapeiris Jan 31, 2026
35b082d
Add tests for ProjectCardStats component
dileepapeiris Jan 31, 2026
b94ec2b
Add ProjectCardSkeleton unit tests
dileepapeiris Jan 31, 2026
1105062
Add ProjectCardInfo unit tests
dileepapeiris Jan 31, 2026
8193f1e
Trim trailing spaces in ProjectCardSkeleton test
dileepapeiris Jan 31, 2026
43d746f
Add tests for ProjectCardBadges
dileepapeiris Jan 31, 2026
a591a82
Add ProjectCardActions tests
dileepapeiris Jan 31, 2026
dd9452a
Add ProjectCard component tests
dileepapeiris Jan 31, 2026
bfe489a
Use project id in ProjectSwitcher tests
dileepapeiris Jan 31, 2026
8334d56
Merge branch 'wso2-open-operations:main' into feat/add-project-cards
dileepapeiris Jan 31, 2026
1c86519
Move ProjectHub logging into useEffect hooks
dileepapeiris Jan 31, 2026
a9adeff
Add comments and fix sidebar mock in tests
dileepapeiris Jan 31, 2026
811f125
Enhance Header tests for project switching
dileepapeiris Jan 31, 2026
6f21e96
Clear mockOnProjectChange before each test
dileepapeiris Jan 31, 2026
9833a3a
Avoid fetching on error and fix project selection
dileepapeiris Jan 31, 2026
0597cd6
Enable CSS handling in Vitest config
dileepapeiris Jan 31, 2026
5f82109
Wrap logger assertions with waitFor in tests
dileepapeiris Jan 31, 2026
22571ba
Merge branch 'customer-portal-milestone-1' into feat/add-project-cards
dileepapeiris Feb 2, 2026
96363f9
Merge branch 'customer-portal-milestone-1' into feat/add-project-cards
Rashmika998 Feb 3, 2026
dd817bf
Apply suggestion from @Rashmika998
Rashmika998 Feb 3, 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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import useSearchProjects from "../useSearchProjects";
import useSearchProjects from "@/api/useSearchProjects";
import { mockProjects } from "@/models/mockData";
import type { ReactNode } from "react";

Expand Down
10 changes: 8 additions & 2 deletions apps/customer-portal/webapp/src/api/useSearchProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export default function useSearchProjects(
const logger = useLogger();
const limit = fetchAll ? 100 : searchData.pagination?.limit || 10;

// A stable key for the "all projects" query ensures cache sharing
/**
* A stable key for the "all projects" query ensures cache sharing
*/
const queryKey = fetchAll
? [ApiQueryKeys.PROJECTS, "all"]
: [ApiQueryKeys.PROJECTS, searchData];
Expand All @@ -60,7 +62,11 @@ export default function useSearchProjects(
`Fetching projects... offset: ${pageParam}, limit: ${limit}, fetchAll: ${fetchAll}`,
);

// Simulate a network delay
/**
* Mock behavior: simulate network latency for the in-memory `mockProjects` data.
* This is intended only for development/demo use and should be removed or
* replaced when wiring this hook to the real backend API.
*/
await new Promise((resolve) => setTimeout(resolve, 800));

const offset = typeof pageParam === "number" ? pageParam : 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
import { Users } from "@wso2/oxygen-ui-icons-react";
import type { JSX } from "react";
import { JOIN_COMMUNITY_URL } from "@/constants/appLayoutConstants";
import UserProfile from "./UserProfile";
import UserProfile from "@/components/header/UserProfile";

/**
* Actions component for the header.
Expand Down
45 changes: 28 additions & 17 deletions apps/customer-portal/webapp/src/components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import { useNavigate, useLocation, useParams } from "react-router";
import useSearchProjects from "@/api/useSearchProjects";
import { useLogger } from "@/hooks/useLogger";
import type { ProjectListItem } from "@/models/responses";
import Brand from "./Brand";
import Actions from "./Actions";
import SearchBar from "./SearchBar";
import ProjectSwitcher from "./ProjectSwitcher";
import Brand from "@/components/header/Brand";
import Actions from "@/components/header/Actions";
import SearchBar from "@/components/header/SearchBar";
import ProjectSwitcher from "@/components/header/ProjectSwitcher";

/**
* Props for the Header component.
Expand Down Expand Up @@ -74,16 +74,17 @@ export default function Header({ onToggleSidebar }: HeaderProps): JSX.Element {
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isError,
} = useSearchProjects({}, true);

/**
* Fetch next page of projects if available.
*/
useEffect(() => {
if (hasNextPage && !isFetchingNextPage) {
if (hasNextPage && !isFetchingNextPage && !isError) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
}, [hasNextPage, isFetchingNextPage, isError, fetchNextPage]);

/**
* Flatten the projects response.
Expand All @@ -94,7 +95,7 @@ export default function Header({ onToggleSidebar }: HeaderProps): JSX.Element {
/**
* Find the project from the URL parameters.
*/
const projectFromUrl = projects.find((project) => project.key === projectId);
const projectFromUrl = projects.find((project) => project.id === projectId);

/**
* State for the selected project.
Expand All @@ -111,25 +112,35 @@ export default function Header({ onToggleSidebar }: HeaderProps): JSX.Element {
/**
* Find the project from the URL parameters.
*/
const project = projects.find((project) => project.key === projectId);
const project = projects.find((p) => p.id === projectId);
/**
* Set the selected project if it is different from the current selected project.
* If no matching project is found, clear the selection.
*/
if (project && project.key !== selectedProject?.key) {
setProject(project);
if (project) {
if (project.id !== selectedProject?.id) {
setProject(project);
}
} else {
setProject(undefined);
}
} else if (selectedProject) {
/**
* If projectId is missing (e.g., on the project hub), clear the selection.
*/
setProject(undefined);
}
}, [projectId, selectedProject?.key, projects]);
}, [projectId, selectedProject?.id, projects]);

/**
* Handles the project change.
*
* @param {string} projectKey - Key of the project to switch to.
* @param {string} projectId - ID of the project to switch to.
*/
const handleProjectChange = (projectKey: string) => {
const project = projects.find((p) => p.key === projectKey);
const handleProjectChange = (id: string) => {
const project = projects.find((p) => p.id === id);
if (project) {
logger.debug(`Switching to project: ${project.name} (${project.key})`);
logger.debug(`Switching to project: ${project.name} (${project.id})`);
/**
* Set the selected project.
*/
Expand All @@ -141,9 +152,9 @@ export default function Header({ onToggleSidebar }: HeaderProps): JSX.Element {
/**
* Navigate to the new project.
*/
navigate(`/${project.key}/${subPath || "dashboard"}`);
navigate(`/${project.id}/${subPath || "dashboard"}`);
} else {
logger.warn(`Project with key ${projectKey} not found for switching`);
logger.warn(`Project with ID: ${id} not found for switching`);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ interface ProjectSwitcherProps {
/**
* Callback function to handle project change.
*/
onProjectChange: (projectKey: string) => void;
onProjectChange: (projectId: string) => void;
}

/**
Expand All @@ -52,15 +52,15 @@ export default function ProjectSwitcher({
<HeaderUI.Switchers showDivider={false}>
{/* project switcher select */}
<ComplexSelect
value={selectedProject?.key || ""}
value={selectedProject?.id || ""}
onChange={(event: any) => onProjectChange(event.target.value)}
size="small"
sx={{ minWidth: 200 }}
renderValue={(selected) => {
/**
* Find the project from the URL parameters.
*/
const project = projects.find((project) => project.key === selected);
const project = projects.find((project) => project.id === selected);
/**
* Return the project name and icon.
*/
Expand All @@ -77,7 +77,7 @@ export default function ProjectSwitcher({
<ComplexSelect.ListHeader>Switch Project</ComplexSelect.ListHeader>
{/* project switcher list items */}
{projects.map((project) => (
<ComplexSelect.MenuItem key={project.key} value={project.key}>
<ComplexSelect.MenuItem key={project.id} value={project.id}>
<ComplexSelect.MenuItem.Text
primary={project.name}
secondary={project.description}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import Brand from "../Brand";
import Brand from "@/components/header/Brand";

// Mock @wso2/oxygen-ui
vi.mock("@wso2/oxygen-ui", () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,20 @@ vi.mock("@/hooks/useLogger", () => ({
}),
}));

const mockFetchNextPage = vi.fn();
const mockUseSearchProjects = vi.fn(() => ({
data: {
pages: [{ projects: mockProjects }],
},
fetchNextPage: mockFetchNextPage,
hasNextPage: false,
isFetchingNextPage: false,
isError: false,
})) as any;

vi.mock("@/api/useSearchProjects", () => ({
default: vi.fn(() => ({
data: {
pages: [{ projects: mockProjects }],
},
fetchNextPage: vi.fn(),
hasNextPage: false,
isFetchingNextPage: false,
})),
default: (searchData: any, fetchAll: any) =>
mockUseSearchProjects(searchData, fetchAll),
}));

// Mock sub-components
Expand All @@ -128,14 +133,19 @@ vi.mock("../SearchBar", () => ({
}));

vi.mock("../ProjectSwitcher", () => ({
default: ({ onProjectChange }: any) => (
<div data-testid="project-switcher">
default: ({ projects, selectedProject, onProjectChange }: any) => (
<div
data-testid="project-switcher"
data-selected-id={selectedProject?.id || ""}
>
<select
data-testid="project-select"
value={selectedProject?.id || ""}
onChange={(e) => onProjectChange(e.target.value)}
>
{mockProjects.map((p: any) => (
<option key={p.key} value={p.key}>
<option value="">None</option>
{projects.map((p: any) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
Expand All @@ -156,6 +166,15 @@ describe("Header", () => {
vi.clearAllMocks();
mockLocation.pathname = "/";
mockParams.projectId = "";
mockUseSearchProjects.mockReturnValue({
data: {
pages: [{ projects: mockProjects }],
},
fetchNextPage: mockFetchNextPage,
hasNextPage: false,
isFetchingNextPage: false,
isError: false,
});
});

it("should render the brand component", () => {
Expand Down Expand Up @@ -196,13 +215,62 @@ describe("Header", () => {
render(<Header onToggleSidebar={mockOnToggleSidebar} />);

const select = screen.getByTestId("project-select");
fireEvent.change(select, { target: { value: mockProjects[1].key } });
fireEvent.change(select, { target: { value: mockProjects[1].id } });

expect(mockNavigate).toHaveBeenCalledWith(
`/${mockProjects[1].key}/dashboard`,
`/${mockProjects[1].id}/dashboard`,
);
});

it("should clear selection when projectId is invalid", () => {
mockLocation.pathname = "/invalid-project/dashboard";
mockParams.projectId = "invalid-project";

render(<Header onToggleSidebar={mockOnToggleSidebar} />);

const switcher = screen.getByTestId("project-switcher");
expect(switcher).toHaveAttribute("data-selected-id", "");
});

it("should clear selection when projectId is missing (hub page)", () => {
mockLocation.pathname = "/";
mockParams.projectId = "";

render(<Header onToggleSidebar={mockOnToggleSidebar} />);

// Switcher is not rendered on Hub, but we can verify the state if it was
// For coverage of the useEffect clearing logic, we check that it handles missing projectId
expect(screen.queryByTestId("project-switcher")).toBeNull();
});

it("should call fetchNextPage if hasNextPage is true", () => {
mockUseSearchProjects.mockReturnValue({
data: { pages: [{ projects: mockProjects }] },
fetchNextPage: mockFetchNextPage,
hasNextPage: true,
isFetchingNextPage: false,
isError: false,
});

render(<Header onToggleSidebar={mockOnToggleSidebar} />);

expect(mockFetchNextPage).toHaveBeenCalled();
});

it("should NOT call fetchNextPage if isError is true", () => {
mockUseSearchProjects.mockReturnValue({
data: { pages: [{ projects: mockProjects }] },
fetchNextPage: mockFetchNextPage,
hasNextPage: true,
isFetchingNextPage: false,
isError: true,
});

render(<Header onToggleSidebar={mockOnToggleSidebar} />);

expect(mockFetchNextPage).not.toHaveBeenCalled();
});

it("should render Actions", () => {
render(<Header onToggleSidebar={mockOnToggleSidebar} />);
expect(screen.getByTestId("actions")).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// under the License.

import { render, screen, fireEvent } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ProjectSwitcher from "@/components/header/ProjectSwitcher";
import { mockProjects } from "@/models/mockData";

Expand Down Expand Up @@ -55,6 +55,10 @@ vi.mock("@wso2/oxygen-ui-icons-react", () => ({
describe("ProjectSwitcher", () => {
const mockOnProjectChange = vi.fn();

beforeEach(() => {
mockOnProjectChange.mockClear();
});

it("should render projects in the dropdown", () => {
render(
<ProjectSwitcher
Expand All @@ -80,8 +84,8 @@ describe("ProjectSwitcher", () => {
);

const select = screen.getByTestId("project-select");
fireEvent.change(select, { target: { value: mockProjects[1].key } });
fireEvent.change(select, { target: { value: mockProjects[1].id } });

expect(mockOnProjectChange).toHaveBeenCalledWith(mockProjects[1].key);
expect(mockOnProjectChange).toHaveBeenCalledWith(mockProjects[1].id);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
Loading