-
Notifications
You must be signed in to change notification settings - Fork 49
[Customer Portal][FE][Web] Add Project Hub, UI Shell, and Routing Infrastructure #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Rashmika998
merged 48 commits into
wso2-open-operations:customer-portal-milestone-1
from
dileepapeiris:customer-portal-milestone-1
Feb 3, 2026
Merged
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 3c7662d
Add Brand component for header
dileepapeiris 40de400
Add Actions component for header UI
dileepapeiris 892c08c
Add ProjectSwitcher component to header
dileepapeiris 8ecfd9f
Add SearchBar component to header
dileepapeiris 6b8c442
Add UserProfile component to header
dileepapeiris 3c94fd9
Add Footer component to customer portal webapp
dileepapeiris 9a02f2d
Add unit tests for Footer component
dileepapeiris d0949a5
Add response models for projects and user profile
dileepapeiris b0335d2
Add request models for project search and pagination
dileepapeiris fff5fc7
Add mock data for projects and user profile
dileepapeiris c32aea8
Add ProjectHub page component
dileepapeiris b12113f
Add ProjectPage component for project details view
dileepapeiris ce6029e
Add AppLayout component for main application layout
dileepapeiris 56a3923
Update Vitest config with CSS and server options
dileepapeiris 53f7cbe
Add comments to useLogger hook for clarity
dileepapeiris 7ede012
Add JSDoc return annotation to LoggerContext
dileepapeiris a27297c
Update import path for app layout constants
dileepapeiris 016af56
Update import path for app layout constants
dileepapeiris 5347f76
Add app layout constants for navigation and URLs
dileepapeiris 075317a
Add API query keys constants file
dileepapeiris ad7c40e
Implement routing structure for customer portal app
dileepapeiris c84861b
Reorder import of App component
dileepapeiris 1c1e36f
Add SideBar component for customer portal
dileepapeiris 0cd799f
Add SubscriptionWidget component
dileepapeiris 427f8e8
Fix import path for JOIN_COMMUNITY_URL constant
dileepapeiris 839fa84
Update pnpm lockfile with new dependencies
dileepapeiris 941ba55
Add @asgardeo/react-router and react-router dependencies
dileepapeiris 0f4abc3
Remove fixed icon size in sidebar items
dileepapeiris e693a01
Add tests for SubscriptionWidget component
dileepapeiris f99754f
Add unit tests for SideBar component
dileepapeiris 1cd7f78
Fix project switch navigation subpath handling
dileepapeiris b35bf82
Add tests for UserProfile component
dileepapeiris 30fbb8d
Add unit test for SearchBar component
dileepapeiris 1e5f475
Add tests for ProjectSwitcher component
dileepapeiris 8cc5c38
Add unit tests for Header component
dileepapeiris ada11c8
Add unit test for Brand component
dileepapeiris 0a8e05a
Add unit tests for Header Actions component
dileepapeiris 633f3c0
Add useSearchProjects hook for infinite project queries
dileepapeiris e05923a
Add tests for useSearchProjects hook
dileepapeiris 08c0157
Add tests for AppLayout component
dileepapeiris eda5fda
Merge branch 'customer-portal-milestone-1' into customer-portal-miles…
dileepapeiris f9edffc
Refactor logging and update mock user email
dileepapeiris 233bcca
Merge branch 'customer-portal-milestone-1' of https://github.com/dile…
dileepapeiris 7ebfe44
Update test mocks for Footer and UserProfile components
dileepapeiris 22fbed8
Update mock user data in mockData.ts
dileepapeiris d1a93cd
Add explicit type to search projects results
dileepapeiris b670bcd
Merge branch 'customer-portal-milestone-1' into customer-portal-miles…
dileepapeiris File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
apps/customer-portal/webapp/src/api/__tests__/useSearchProjects.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
38
apps/customer-portal/webapp/src/components/footer/Footer.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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} | ||
| /> | ||
| ); | ||
| } |
62 changes: 62 additions & 0 deletions
62
apps/customer-portal/webapp/src/components/footer/__tests__/Footer.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ), | ||
| })); | ||
|
|
||
| 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); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.