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
a75a00c
Add Header component for customer portal webapp
dileepapeiris Jan 29, 2026
3c7662d
Add Brand component for header
dileepapeiris Jan 29, 2026
40de400
Add Actions component for header UI
dileepapeiris Jan 29, 2026
892c08c
Add ProjectSwitcher component to header
dileepapeiris Jan 29, 2026
8ecfd9f
Add SearchBar component to header
dileepapeiris Jan 29, 2026
6b8c442
Add UserProfile component to header
dileepapeiris Jan 29, 2026
3c94fd9
Add Footer component to customer portal webapp
dileepapeiris Jan 29, 2026
9a02f2d
Add unit tests for Footer component
dileepapeiris Jan 29, 2026
d0949a5
Add response models for projects and user profile
dileepapeiris Jan 29, 2026
b0335d2
Add request models for project search and pagination
dileepapeiris Jan 29, 2026
fff5fc7
Add mock data for projects and user profile
dileepapeiris Jan 29, 2026
c32aea8
Add ProjectHub page component
dileepapeiris Jan 29, 2026
b12113f
Add ProjectPage component for project details view
dileepapeiris Jan 29, 2026
ce6029e
Add AppLayout component for main application layout
dileepapeiris Jan 29, 2026
56a3923
Update Vitest config with CSS and server options
dileepapeiris Jan 29, 2026
53f7cbe
Add comments to useLogger hook for clarity
dileepapeiris Jan 29, 2026
7ede012
Add JSDoc return annotation to LoggerContext
dileepapeiris Jan 29, 2026
a27297c
Update import path for app layout constants
dileepapeiris Jan 29, 2026
016af56
Update import path for app layout constants
dileepapeiris Jan 29, 2026
5347f76
Add app layout constants for navigation and URLs
dileepapeiris Jan 29, 2026
075317a
Add API query keys constants file
dileepapeiris Jan 29, 2026
ad7c40e
Implement routing structure for customer portal app
dileepapeiris Jan 29, 2026
c84861b
Reorder import of App component
dileepapeiris Jan 29, 2026
1c1e36f
Add SideBar component for customer portal
dileepapeiris Jan 29, 2026
0cd799f
Add SubscriptionWidget component
dileepapeiris Jan 29, 2026
427f8e8
Fix import path for JOIN_COMMUNITY_URL constant
dileepapeiris Jan 29, 2026
839fa84
Update pnpm lockfile with new dependencies
dileepapeiris Jan 29, 2026
941ba55
Add @asgardeo/react-router and react-router dependencies
dileepapeiris Jan 29, 2026
0f4abc3
Remove fixed icon size in sidebar items
dileepapeiris Jan 29, 2026
e693a01
Add tests for SubscriptionWidget component
dileepapeiris Jan 29, 2026
f99754f
Add unit tests for SideBar component
dileepapeiris Jan 29, 2026
1cd7f78
Fix project switch navigation subpath handling
dileepapeiris Jan 29, 2026
b35bf82
Add tests for UserProfile component
dileepapeiris Jan 29, 2026
30fbb8d
Add unit test for SearchBar component
dileepapeiris Jan 29, 2026
1e5f475
Add tests for ProjectSwitcher component
dileepapeiris Jan 29, 2026
8cc5c38
Add unit tests for Header component
dileepapeiris Jan 29, 2026
ada11c8
Add unit test for Brand component
dileepapeiris Jan 29, 2026
0a8e05a
Add unit tests for Header Actions component
dileepapeiris Jan 29, 2026
633f3c0
Add useSearchProjects hook for infinite project queries
dileepapeiris Jan 29, 2026
e05923a
Add tests for useSearchProjects hook
dileepapeiris Jan 29, 2026
08c0157
Add tests for AppLayout component
dileepapeiris Jan 29, 2026
eda5fda
Merge branch 'customer-portal-milestone-1' into customer-portal-miles…
dileepapeiris Jan 29, 2026
f9edffc
Refactor logging and update mock user email
dileepapeiris Jan 29, 2026
233bcca
Merge branch 'customer-portal-milestone-1' of https://github.com/dile…
dileepapeiris Jan 29, 2026
7ebfe44
Update test mocks for Footer and UserProfile components
dileepapeiris Jan 29, 2026
22fbed8
Update mock user data in mockData.ts
dileepapeiris Jan 30, 2026
d1a93cd
Add explicit type to search projects results
dileepapeiris Jan 30, 2026
b670bcd
Merge branch 'customer-portal-milestone-1' into customer-portal-miles…
dileepapeiris Feb 2, 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
2 changes: 2 additions & 0 deletions apps/customer-portal/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
},
"dependencies": {
"@asgardeo/auth-react": "^5.4.3",
"@asgardeo/react-router": "^1.0.31",
"@tanstack/react-query": "^5.90.20",
"@tanstack/react-query-devtools": "^5.91.2",
"@wso2/oxygen-ui": "^0.3.0",
"@wso2/oxygen-ui-charts-react": "^0.3.0",
"@wso2/oxygen-ui-icons-react": "^0.3.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router": "^7.1.5",
"zod": "^4.3.6"
},
"devDependencies": {
Expand Down
1,036 changes: 1,023 additions & 13 deletions apps/customer-portal/webapp/pnpm-lock.yaml

Large diffs are not rendered by default.

71 changes: 67 additions & 4 deletions apps/customer-portal/webapp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,71 @@
// specific language governing permissions and limitations
// under the License.

function App() {
return <></>;
}
import type { JSX } from "react";
import { BrowserRouter, Route, Routes, Navigate } from "react-router";
import ProjectHub from "@/pages/ProjectHub";
import ProjectPage from "@/pages/ProjectPage";
import AppLayout from "@/layouts/AppLayout";

export default function App(): JSX.Element {
return (
<BrowserRouter>
<Routes>
{/* AppLayout component */}
<Route element={<AppLayout />}>
{/* ProjectHub Page */}
<Route path="/" element={<ProjectHub />} />

export default App;
{/* Project Specific Routes */}
<Route path="/:projectId">
{/* Dashboard */}
<Route index element={<Navigate to="dashboard" replace />} />
<Route
path="dashboard"
element={<ProjectPage title="Dashboard" />}
/>
{/* Project Details */}
<Route
path="project-details"
element={<ProjectPage title="Project Details" />}
/>
{/* Support */}
<Route path="support" element={<ProjectPage title="Support" />} />
{/* Updates */}
<Route path="updates" element={<ProjectPage title="Updates" />} />
{/* SecurityCenter */}
<Route
path="security-center"
element={<ProjectPage title="Security Center" />}
/>
{/* Engagements */}
<Route
path="engagements"
element={<ProjectPage title="Engagements" />}
/>
{/* LegalContracts */}
<Route
path="legal-contracts"
element={<ProjectPage title="Legal Contracts" />}
/>
{/* Community */}
<Route
path="community"
element={<ProjectPage title="Community" />}
/>
{/* Announcements */}
<Route
path="announcements"
element={<ProjectPage title="Announcements" />}
/>
{/* Settings */}
<Route path="settings" element={<ProjectPage title="Settings" />} />
</Route>
</Route>

{/* Fallback */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
);
}
2 changes: 1 addition & 1 deletion apps/customer-portal/webapp/src/AppWithConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import type { JSX } from "react";
import { OxygenUIThemeProvider } from "@wso2/oxygen-ui";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import App from "@/App";
import { AuthProvider } from "@asgardeo/auth-react";
import { authConfig } from "@/config/authConfig";
import { themeConfig } from "@/config/themeConfig";
import { loggerConfig } from "@/config/loggerConfig";
import LoggerProvider from "@/context/logger/LoggerProvider";
import App from "@/App";

const queryClient: QueryClient = new QueryClient();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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 } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import useSearchProjects from "../useSearchProjects";
import { mockProjects } from "@/models/mockData";
import type { ReactNode } from "react";

// 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("useSearchProjects", () => {
it("should return paginated mock projects in pages on query execution", async () => {
const { result } = renderHook(
() => useSearchProjects({ pagination: { offset: 0, limit: 2 } }),
{
wrapper: createWrapper(),
},
);

// Wait for the initial page to finish
await waitFor(() => expect(result.current.isSuccess).toBe(true));

const response = result.current.data;
expect(response).toBeDefined();
expect(response?.pages).toHaveLength(1);
expect(response?.pages[0].projects).toHaveLength(2);
expect(response?.pages[0].totalRecords).toBe(mockProjects.length);
expect(response?.pages[0].projects[0].name).toBe(mockProjects[0].name);
expect(response?.pages[0].offset).toBe(0);
expect(response?.pages[0].limit).toBe(2);
});

it("should handle default pagination if none provided", async () => {
const { result } = renderHook(() => useSearchProjects(), {
wrapper: createWrapper(),
});

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

const response = result.current.data;
expect(response?.pages[0].offset).toBe(0);
expect(response?.pages[0].limit).toBe(10);
});
});
88 changes: 88 additions & 0 deletions apps/customer-portal/webapp/src/api/useSearchProjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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 {
useInfiniteQuery,
type UseInfiniteQueryResult,
type InfiniteData,
} from "@tanstack/react-query";
import { useLogger } from "@/hooks/useLogger";
import { mockProjects } from "@/models/mockData";
import { ApiQueryKeys } from "@/constants/apiQueryKeys";
import type { SearchProjectsRequest } from "@/models/requests";
import type { SearchProjectsResponse } from "@/models/responses";

/**
* Custom hook to search projects.
* This hook uses an infinite query to fetch projects in pages.
*
* @param {SearchProjectsRequest} searchData - The search and pagination parameters.
* @param {boolean} fetchAll - If true, treats this as a shared "all projects" query.
* @returns {UseInfiniteQueryResult<InfiniteData<SearchProjectsResponse>, Error>} The infinite query result object.
*/
export default function useSearchProjects(
searchData: SearchProjectsRequest = {},
fetchAll: boolean = false,
): UseInfiniteQueryResult<InfiniteData<SearchProjectsResponse>, Error> {
const logger = useLogger();
const limit = fetchAll ? 100 : searchData.pagination?.limit || 10;

// A stable key for the "all projects" query ensures cache sharing
const queryKey = fetchAll
? [ApiQueryKeys.PROJECTS, "all"]
: [ApiQueryKeys.PROJECTS, searchData];

return useInfiniteQuery<SearchProjectsResponse, Error>({
getPreviousPageParam: (firstPage) => {
const prevOffset = firstPage.offset - limit;
return prevOffset >= 0 ? prevOffset : undefined;
},
getNextPageParam: (lastPage) => {
const nextOffset = lastPage.offset + limit;
return nextOffset < lastPage.totalRecords ? nextOffset : undefined;
},
initialPageParam: 0,
queryFn: async ({ pageParam = 0 }): Promise<SearchProjectsResponse> => {
logger.debug(
`Fetching projects... offset: ${pageParam}, limit: ${limit}, fetchAll: ${fetchAll}`,
);

// Simulate a network delay
await new Promise((resolve) => setTimeout(resolve, 800));

const offset = typeof pageParam === "number" ? pageParam : 0;

const results: SearchProjectsResponse = {
limit,
offset,
projects: mockProjects.slice(offset, offset + limit).map((project) => ({
createdOn: project.createdOn,
description: project.description,
id: project.id,
key: project.key,
name: project.name,
})),
totalRecords: mockProjects.length,
};

logger.debug("Projects fetched successfully", results);

return results;
},
queryKey,
staleTime: Infinity,
});
}
38 changes: 38 additions & 0 deletions apps/customer-portal/webapp/src/components/footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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 {
COMPANY_NAME,
PRIVACY_POLICY_URL,
TERMS_OF_SERVICE_URL,
} from "@/constants/appLayoutConstants";
import { Footer as FooterUI } from "@wso2/oxygen-ui";
import type { JSX } from "react";

/**
* Footer component.
*
* @returns {JSX.Element} The Footer component.
*/
export default function Footer(): JSX.Element {
return (
<FooterUI
companyName={COMPANY_NAME}
termsUrl={TERMS_OF_SERVICE_URL}
privacyUrl={PRIVACY_POLICY_URL}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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 Footer from "@/components/footer/Footer";
import {
COMPANY_NAME,
PRIVACY_POLICY_URL,
TERMS_OF_SERVICE_URL,
} from "@/constants/appLayoutConstants";

// Mock @wso2/oxygen-ui
vi.mock("@wso2/oxygen-ui", () => ({
Footer: ({
companyName,
termsUrl,
privacyUrl,
}: {
companyName: string;
termsUrl: string;
privacyUrl: string;
}) => (
<div data-testid="footer-ui">
<span>{companyName}</span>
<a href={termsUrl}>Terms of Service</a>
<a href={privacyUrl}>Privacy Policy</a>
</div>
),
}));
Comment thread
dileepapeiris marked this conversation as resolved.

describe("Footer", () => {
it("should render the company name", () => {
render(<Footer />);
expect(screen.getByText(new RegExp(COMPANY_NAME, "i"))).toBeDefined();
});

it("should render the terms of service link with correct URL", () => {
render(<Footer />);
const termsLink = screen.getByRole("link", { name: /terms/i });
expect(termsLink).toHaveAttribute("href", TERMS_OF_SERVICE_URL);
});

it("should render the privacy policy link with correct URL", () => {
render(<Footer />);
const privacyLink = screen.getByRole("link", { name: /privacy/i });
expect(privacyLink).toHaveAttribute("href", PRIVACY_POLICY_URL);
});
});
Loading