[Customer Portal][FE][Web] Project Structure Refactor and Project Switcher Enhancements - #80
Conversation
Introduce a new Footer component that wraps @wso2/oxygen-ui's Footer and uses app layout constants for company name, terms, and privacy URLs. Add unit tests for the Footer, and new tests for Header subcomponents (Actions and Brand). Tests mock @wso2/oxygen-ui and icon packages to verify rendered links, theme toggle, user profile, logo, and product title.
Refactor import paths to match reorganized component folders: Header/Footer/SideBar imports moved to components/common and ProjectCard/ProjectCardSkeleton imports moved to components/projectHub. Updated corresponding tests' mocks to use the new paths. No functional changes, just path updates to reflect project structure.
Introduce modular ProjectCard UI pieces for the customer portal: ProjectCardBadges, ProjectCardInfo, ProjectCardStats, and ProjectCardSkeleton. Each component is a TypeScript React component using @wso2/oxygen-ui primitives and typed props (JSX). Badges renders project key and status (uses getStatusColor), Info shows title and truncated subtitle, Stats displays open cases, active chats and formatted date (uses formatProjectDate) with icons, and Skeleton provides a loading placeholder matching the card layout. These additions split the ProjectCard into reusable parts and improve consistency of layout and loading state.
Delete unit tests for ProjectCard and its subcomponents. Removed files: - apps/customer-portal/webapp/src/components/projectCard/__tests__/ProjectCard.test.tsx - apps/customer-portal/webapp/src/components/projectCard/__tests__/ProjectCardActions.test.tsx - apps/customer-portal/webapp/src/components/projectCard/__tests__/ProjectCardBadges.test.tsx - apps/customer-portal/webapp/src/components/projectCard/__tests__/ProjectCardInfo.test.tsx These tests contained mocks for @wso2/oxygen-ui, react-router, and internal utilities and have been removed (likely as part of test cleanup or refactor).
Delete four ProjectCard components (ProjectCardActions, ProjectCardBadges, ProjectCardInfo, ProjectCardSkeleton) from apps/customer-portal/webapp/src/components/projectCard. Remove associated UI pieces; update any imports/usages referencing these files.
Introduce unit tests for Header, ProjectSwitcher, SearchBar, and UserProfile components under apps/customer-portal/webapp/src/components/common/header/__tests__/. Tests cover rendering, project selection/navigation, sidebar toggle behavior, fetchNextPage invocation handling, and error cases. The tests include extensive mocking for @wso2/oxygen-ui, icons, react-router hooks, logger hook, projects API, and internal subcomponents to isolate behavior.
Delete SideBar.tsx and SubscriptionWidget.tsx along with their unit tests from apps/customer-portal/webapp/src/components/sideNavBar. These files were removed as part of a sidebar cleanup/refactor to consolidate or replace the side navigation functionality.
Delete Footer implementation and its tests from apps/customer-portal/webapp, and remove header test files. Removed files: Footer.tsx, footer/Footer.test.tsx, header/__tests__/Actions.test.tsx, header/__tests__/Brand.test.tsx as part of a component/test cleanup.
Delete legacy header implementation from the customer-portal webapp. Removed files: Actions.tsx, Brand.tsx, Header.tsx, ProjectSwitcher.tsx, and SearchBar.tsx from apps/customer-portal/webapp/src/components/header. These deletions remove the previous header, brand, actions, project switcher, and search bar components (presumably to be replaced by a refactored header implementation).
Delete ProjectCard, ProjectCardStats and UserProfile components and their associated tests (ProjectCardSkeleton.test.tsx, ProjectCardStats.test.tsx) from apps/customer-portal/webapp/src/components. Cleans up obsolete project-card and header UI pieces and corresponding unit tests.
Introduce a new SubscriptionWidget component for the side nav (uses oxygen-ui components and Crown icon; returns null when collapsed). Update Header tests: adjust the mocked ProjectSwitcher to accept an isLoading prop (adds data-loading attribute) and add a test to verify Header passes isLoading when project search is loading. Remove legacy tests under components/header/__tests__ that were replaced/relocated to the common/header test suite.
Add four new header components for the customer portal: Brand, Actions, Header, and ProjectSwitcher. Brand renders the WSO2 logo and title; Actions provides the Join Community button, theme toggle, divider, and user profile. ProjectSwitcher renders a ComplexSelect with a loading skeleton and handles project selection. Header composes these parts, fetches projects via useGetProjects, keeps selected project state in sync with the URL, and navigates to the selected project's subpath.
Introduce Header components SearchBar and UserProfile and add unit tests for the sidebar. SearchBar uses @wso2/oxygen-ui SearchBar with a small size and placeholder; UserProfile wraps @wso2/oxygen-ui UserMenu, uses mockUser, useNavigate for logout navigation and useLogger for click handlers. Add tests for SideBar and SubscriptionWidget that mock @wso2/oxygen-ui, icons and react-router to assert navigation items render, links include projectId, and subscription widget behavior when collapsed or expanded. All new files include the project Apache 2.0 license header.
Introduce a new SideBar component for the customer portal using @wso2/oxygen-ui and react-router. The component supports collapsed state, expanded menus, selection/expand callbacks, and renders navigation items from APP_SHELL_NAV_ITEMS plus a footer with SubscriptionWidget and Settings link. Add unit tests for ProjectCard and its subcomponents (ProjectCardActions, ProjectCardBadges, ProjectCardInfo) using Vitest and React Testing Library. Tests mock @wso2/oxygen-ui, icons, utilities, and react-router to verify rendering, props, behavior (including navigation and callback handling), and visual chip color logic.
Introduce a new ProjectCard component and unit tests. Adds ProjectCard.tsx which renders project badges, info, stats and actions, accepts explicit props (id, projectKey, title, subtitle, date, optional status/openCases/activeChats and onViewDashboard), uses mock defaults for status/counts, and navigates to a dashboard route when clicked. Also adds tests: ProjectCardSkeleton.test.tsx (verifies skeleton layout and placeholders, mocks @wso2/oxygen-ui) and ProjectCardStats.test.tsx (verifies counts, formatted date and icons, mocks UI icons and utils). Includes license headers in new files.
Introduce a new ProjectCardActions component at apps/customer-portal/webapp/src/components/projectHub/projectCard/ProjectCardActions.tsx. The component renders card action area using Form.CardActions and Stack from @wso2/oxygen-ui and includes a full-width outlined secondary Button labeled "View Dashboard" with an ArrowRight icon. File includes license header and basic styling (spacing, padding) for consistent layout within project cards.
📝 WalkthroughWalkthroughThis PR introduces a complete customer portal UI layout with header, footer, and sidebar navigation components, a project hub page with project card cards, and comprehensive test coverage. All changes are new component additions integrating Oxygen UI library. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Use project.key as the secondary text in ProjectSwitcher (replacing project.description). Update tests to mock MenuItem.Text to render both primary and secondary and assert that both project.name and project.key are present (using non-exact matching).
There was a problem hiding this comment.
Pull request overview
This PR refactors the customer portal project structure by centralizing common layout components (header, footer, sideNavBar) into a src/components/common/ directory and organizing feature-specific components (projectCard) into src/components/projectHub/. It implements comprehensive routing with React Router 7, adds a ProjectSwitcher component with skeleton loading states, and creates several new pages including ProjectHub, ProjectPage, and SupportPage with their corresponding API hooks and mock data infrastructure.
Changes:
- Centralized common layout components into
src/components/common/directory structure - Implemented React Router 7 for application routing with project-specific and hub pages
- Added ProjectSwitcher component with skeleton loading states and project name/key display
- Created support statistics infrastructure with API hooks, mock data, and UI components
Reviewed changes
Copilot reviewed 59 out of 60 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Added CSS support and inline dependencies for Oxygen UI components in test environment |
| src/utils/projectCard.ts | New utility functions for date formatting and status color mapping |
| src/utils/tests/projectCard.test.ts | Comprehensive tests for projectCard utilities |
| src/utils/tests/logger.test.ts | New tests for Logger functionality with various log levels |
| src/pages/* | New page components: SupportPage, ProjectPage, ProjectHub with tests |
| src/models/* | New type definitions for requests/responses and mock data infrastructure |
| src/layouts/AppLayout.tsx | Main layout component with conditional sidebar rendering |
| src/components/common/header/* | Header components including ProjectSwitcher, Brand, Actions, SearchBar, UserProfile |
| src/components/common/footer/* | Footer component with company info and policy links |
| src/components/common/sideNavBar/* | Sidebar navigation and subscription widget |
| src/components/projectHub/projectCard/* | ProjectCard components with badges, info, stats, actions, and skeleton |
| src/components/support/* | CasesOverviewStats component for displaying support metrics |
| src/constants/* | Application constants for API keys, layout navigation, and support config |
| src/api/* | API hooks for fetching projects and support statistics |
| src/App.tsx | Main routing configuration with React Router 7 |
| package.json | Added react-router and @asgardeo/react-router dependencies |
Files not reviewed (1)
- apps/customer-portal/webapp/pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@apps/customer-portal/webapp/src/api/useGetProjects.ts`:
- Around line 59-60: The pagination offset from callers
(searchData.pagination?.offset) is being ignored; update useGetProjects.ts so
initialPageParam and the queryFn honor that requested offset: set
initialPageParam to searchData.pagination?.offset ?? 0 (instead of hardcoded 0)
and ensure the queryFn signature for pageParam uses that value (e.g., defaulting
to the incoming pageParam and passing it through to the underlying search API),
so requests start at the requested non-zero offset; reference symbols:
initialPageParam, queryFn, and searchData.pagination?.offset.
In
`@apps/customer-portal/webapp/src/components/projectHub/projectCard/ProjectCard.tsx`:
- Around line 78-86: The component ProjectCard currently calls getMockStatus,
getMockOpenCases, and getMockActiveChats directly in the props default values
which re-runs on every render and changes those values; fix this by moving mock
generation to a stable initializer — either call the mock generators once
outside the component (module-level constants) or initialize them inside
ProjectCard using a stable hook (useMemo or useRef/useState with an empty
dependency array) and then use those stable values as the final
status/openCases/activeChats when real props are not provided; update references
to getMockStatus, getMockOpenCases, and getMockActiveChats to use the stabilized
values.
In `@apps/customer-portal/webapp/src/layouts/__tests__/AppLayout.test.tsx`:
- Around line 28-31: The test's mockShellState uses expandedMenus as an array
but the component expects a record/map; update mockShellState.expandedMenus to
match the real contract (e.g., an object/Record<string, boolean> or empty {}),
so tests reflect the actual shape consumed by AppLayout and any functions
referencing expandedMenus (search for mockShellState and expandedMenus in the
test file to change it).
In `@apps/customer-portal/webapp/src/pages/ProjectHub.tsx`:
- Around line 39-96: The infinite-query returned by useGetProjects({}, true) is
not being drained, so only the first page is rendered; call the query's
pagination helpers (e.g., fetchNextPage while hasNextPage is true) to auto-fetch
remaining pages and append them to projectsResponse.pages. Add logic in a
useEffect that depends on isLoading, hasNextPage, and fetchNextPage (or
implement a helper like drainAllPages) to sequentially or concurrently
fetchNextPage until hasNextPage is false, then rely on
projectsResponse?.pages.flatMap(...) to render the full list. Ensure you
reference the existing variables fetchNextPage and hasNextPage from the
useGetProjects hook and guard against repeated calls using isLoading or a local
draining flag.
In `@apps/customer-portal/webapp/src/pages/ProjectPage.tsx`:
- Around line 37-52: ProjectPage currently reads projectId from useParams
without guarding against undefined; update the component (ProjectPage) to handle
a missing projectId by providing a safe fallback or early return: check the
value returned from useParams({ projectId }) and if undefined render a sensible
fallback UI (e.g., "Unknown project" message, skeleton/loader, or redirect)
instead of rendering <strong>{projectId}</strong>, ensuring all usages in the
JSX use the guarded value.
In `@apps/customer-portal/webapp/src/utils/projectCard.ts`:
- Around line 24-42: formatProjectDate uses local-time Date and getDate(),
causing timezone shifts; update it to parse only the date portion (e.g., extract
the YYYY-MM-DD from the input) and construct a UTC-stable date or use Date.UTC,
then format using getUTCDate(), getUTCMonth()/toLocaleString with UTC options or
map month names manually. Ensure the same fallbacks remain (return "" for empty
input and original string on parse error) and keep the exported function name
formatProjectDate unchanged.
🧹 Nitpick comments (8)
apps/customer-portal/webapp/src/components/common/header/SearchBar.tsx (1)
26-35: Remove unnecessary React fragment.The fragment wrapper is redundant since only a single element is returned.
♻️ Suggested simplification
export default function SearchBar(): JSX.Element { return ( - <> - {/* header search bar */} - <SearchBarUI - size="small" - placeholder="Search cases, tickets, or users" - sx={{ minWidth: 400 }} - /> - </> + <SearchBarUI + size="small" + placeholder="Search cases, tickets, or users" + sx={{ minWidth: 400 }} + /> ); }apps/customer-portal/webapp/src/models/mockFunctions.ts (2)
30-34: Non-deterministic mock functions may cause flaky tests.Using
Math.random()without seeding produces different values each run, which can lead to non-reproducible test failures. The JSDoc comments describe these as "deterministic random" but they are not deterministic.For test stability, consider either:
- Accepting a seed parameter for reproducibility
- Returning fixed values for unit tests
- Using the existing mock arrays consistently (as done for the first three functions)
63-69: Inconsistent approach compared to other mock functions.
getMockProjectSupportStatsuses inlineMath.random() * Nranges while the other three functions sample from predefined arrays (mockActiveChatsOptions, etc.). Consider using predefined arrays for consistency, or document why different approaches are used.♻️ Consistent approach using predefined sampling
+const mockTotalCasesOptions: number[] = [0, 10, 25, 50]; +const mockResolvedChatsOptions: number[] = [0, 5, 15, 30]; +const mockSessionChatsOptions: number[] = [0, 3, 10, 20]; export const getMockProjectSupportStats = (): ProjectSupportStats => { return { - activeChats: Math.floor(Math.random() * 10), - resolvedChats: Math.floor(Math.random() * 30), - sessionChats: Math.floor(Math.random() * 20), - totalCases: Math.floor(Math.random() * 50), + activeChats: getMockActiveChats(), + resolvedChats: mockResolvedChatsOptions[Math.floor(Math.random() * mockResolvedChatsOptions.length)], + sessionChats: mockSessionChatsOptions[Math.floor(Math.random() * mockSessionChatsOptions.length)], + totalCases: mockTotalCasesOptions[Math.floor(Math.random() * mockTotalCasesOptions.length)], }; };apps/customer-portal/webapp/src/context/logger/LoggerContext.tsx (1)
22-29: Incorrect JSDoc@returnsusage for a constant.The
@returnstag is intended for documenting function return values, not constants.LoggerContextis a constant assigned viacreateContext(), so@returnsis semantically incorrect here. The type is already explicitly declared on line 27.📝 Suggested fix
/** * Context object for the Logger service. - * - * `@returns` {Context<ILogger | null>} The LoggerContext. */ const LoggerContext: Context<ILogger | null> = createContext<ILogger | null>(apps/customer-portal/webapp/src/components/common/header/UserProfile.tsx (1)
17-47: Verify router import and mock-user wiring before release.
Line 19 importsuseNavigatefromreact-router, which is uncommon in web apps that usereact-router-dom/BrowserRouter; please confirm this is intentional and works with your router setup. Also confirmmockUser(Line 43) is only a temporary placeholder and will be replaced with real user context before release.apps/customer-portal/webapp/src/components/support/CasesOverviewStats.tsx (1)
55-95: Avoid double-casting Skeleton intoStatCard.value.
Line 86 forces aSkeletoninto anumberviaas unknown as number, which bypasses type safety and may break ifStatCardformats/assumes numeric values. Please verifyStatCardsupports ReactNode values; otherwise render a dedicated loading branch or update the component API accordingly. (Also consider usingstat.keyas thekeyinstead of the array index to avoid reorder issues.)apps/customer-portal/webapp/src/api/__tests__/useGetProjectSupportStats.test.tsx (1)
17-88: Consider fake timers to avoid the real 800 ms delay.
These tests currently wait for the hook’s real timeout, which can slow down the suite. Using Vitest fake timers would keep the tests fast and deterministic.♻️ Possible test refactor
import { beforeEach, describe, expect, it, vi } from "vitest"; @@ beforeEach(() => { + vi.useFakeTimers(); queryClient = new QueryClient({ @@ mockLogger.debug.mockClear(); mockLogger.error.mockClear(); }); + afterEach(() => { + vi.useRealTimers(); + }); @@ it("should return data after fetching", async () => { @@ - await waitFor(() => expect(result.current.isSuccess).toBe(true)); + await vi.runAllTimersAsync(); + await waitFor(() => expect(result.current.isSuccess).toBe(true));apps/customer-portal/webapp/src/components/common/sideNavBar/SideBar.tsx (1)
64-65: Derive the active item via route matching for nested paths.Using
pathname.split("/").pop()breaks highlighting when nested routes (e.g.,/support/cases) or query fragments are added. Consider matching againstAPP_SHELL_NAV_ITEMSwithmatchPathso the active menu stays stable.Suggested update
-import { useLocation, useParams, Link as NavigateLink } from "react-router"; +import { + useLocation, + useParams, + Link as NavigateLink, + matchPath, +} from "react-router"; -const activeItem = location.pathname.split("/").pop() || "dashboard"; +const activeItem = + APP_SHELL_NAV_ITEMS.find((item) => + matchPath(`/:projectId/${item.path}/*`, location.pathname), + )?.id ?? + (matchPath(`/:projectId/settings`, location.pathname) ? "settings" : "dashboard");
Call fetchNextPage in a useEffect to drain all pages when useGetProjects indicates more pages are available. Adds hasNextPage, isFetchingNextPage and fetchNextPage to the hook destructuring and guards the auto-fetch so it only runs when not currently fetching and there is no error. Also adds a unit test to verify fetchNextPage is invoked when hasNextPage is true.
Change mockShellState.expandedMenus from an array to an object in apps/customer-portal/webapp/src/layouts/__tests__/AppLayout.test.tsx to match the expected shell state shape and avoid type inconsistencies in tests.
Stop calling getMock* functions in the component signature and import useMemo to memoize mock values inside the component. Compute resolvedStatus/resolvedOpenCases/resolvedActiveChats with nullish coalescing and pass those resolved values to ProjectCardBadges and ProjectCardStats. This ensures mock defaults aren't regenerated on every render and preserves optional prop behavior.
Update the warning message in Header.tsx to use uppercase 'ID' instead of 'id' for clarity and consistency when a project is not found during switching.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@apps/customer-portal/webapp/src/components/common/header/Header.tsx`:
- Around line 69-88: The Project fetch can fail and currently Header.tsx passes
only isLoading and projects to ProjectSwitcher, causing an empty dropdown on
error; update the Header component to surface errors by passing isError (and
optionally error object from useGetProjects) into <ProjectSwitcher> and update
the auto-pagination useEffect to avoid calling fetchNextPage when isError is
true (keep the current guard) or render an inline error message in Header when
isError is true; specifically modify the hook usage (useGetProjects) destructure
error/isError and pass isError (and error) into ProjectSwitcher, or render an
error UI in Header before ProjectSwitcher when isError is true so users see an
error state instead of an empty list.
- Around line 164-191: The Header currently hardcodes HeaderUI.Toggle
collapsed={false}; update the Header component to accept the real sidebar
collapsed state from its parent (prop name e.g. sidebarCollapsed or collapsed)
and pass that value into HeaderUI.Toggle instead of false; modify the Header
function signature to include the new prop, wire it where Header is instantiated
(AppLayout/shellState.sidebarCollapsed) and ensure the existing onToggleSidebar
handler is still passed to HeaderUI.Toggle so the toggle icon reflects the
actual sidebarCollapsed state.
🧹 Nitpick comments (1)
apps/customer-portal/webapp/src/components/common/header/Header.tsx (1)
93-96: Consider removing explicituseMemosince React Compiler handles memoization.The customer-portal webapp has React Compiler enabled, which automatically optimizes re-renders. This
useMemois not harmful but is redundant.Based on learnings: "The customer-portal webapp has React Compiler (babel-plugin-react-compiler v1.0.0) enabled via the Vite React plugin configuration. Manual memoization using useCallback, useMemo, or React.memo is unnecessary as the compiler handles these optimizations automatically."
♻️ Optional: Remove manual memoization
- const projects = useMemo( - () => projectsResponse?.pages.flatMap((page) => page.projects) || [], - [projectsResponse?.pages], - ); + const projects = + projectsResponse?.pages.flatMap((page) => page.projects) || [];
…o feat/add-project-selector-dropdown
7702e75
into
wso2-open-operations:customer-portal-milestone-1
Purpose
This PR standardizes the project's component architecture by centralizing shared layout elements and organizing feature-specific components. Additionally, it enhances the Header navigation by implementing a robust Project Switcher dropdown with advanced loading states and detailed project information.
ScreenRecordings and screenshots
Screen.Recording.2026-01-31.at.22.14.13.mov
Projects Selector
**Light mode: **

**Dark mode: **

**Loading Skeletons: **
**Light mode: **

**Dark mode: **

** Animations**
Screen.Recording.2026-01-31.at.22.14.13.mov
Goals
Approach
header,footer, andsideNavBarfolders fromsrc/components/to a newsrc/components/common/directory.projectCardfromsrc/components/tosrc/components/projectHub/projectCard.srcdirectory.Skeletoncomponent into theProjectSwitcherthat mimics the exact button dimensions, border-radius, and typography height to ensure zero layout shift during data fetching.FolderOpenicon to remain visible even during the loading state, providing immediate visual feedback upon page refresh.User stories
Changes
src/components/common/*,src/components/projectHub/*ProjectSwitcher.tsx,ProjectSwitcher.test.tsxHeader.tsx,Actions.tsxmockData.ts,responses.tsAutomation tests
ProjectSwitchercorrectly renders both the Project Name and Key.Skeletoncomponent is rendered when theisLoadingprop is true.common/directory structure.Summary by CodeRabbit
Release Notes