[Customer Portal][FE][Web] Implement Project Details Page and Modular Overview Components - #88
Conversation
Add comprehensive unit tests for the TabBar component (apps/customer-portal/webapp/src/components/common/tabBar/__tests__/TabBar.test.tsx). Tests use Vitest and React Testing Library, mock @wso2/oxygen-ui and a sample icon, and cover rendering, ARIA roles/aria-selected, icons, count badges (including string/zero/custom color), click interaction (onTabChange), combined feature scenarios, and edge cases (single/empty/long labels and nonexistent activeTab).
Introduce a new TabBar React component that renders a styled tab list using @wso2/oxygen-ui Card, Button and Box. Defines TabOption and TabBarProps interfaces, supports icons, count badges, active state styling, accessible roles (tablist/tab) and onTabChange callbacks. Exports the component as default.
Introduce PROJECT_DETAILS_TABS constant with three TabOption entries (overview, deployments, time-tracking) including associated icons (Info, Server, Clock). Adds a typed constants file at apps/customer-portal/webapp/src/constants/projectDetailsConstants.ts and includes the project license header.
Replace usage of ProjectPage (with title prop) by the new ProjectDetails component for the "project-details" route in App.tsx. Added import for ProjectDetails and updated the Route element to render it directly.
Introduce ProjectDetails React component that provides a tabbed project page layout. It uses PROJECT_DETAILS_TABS and a TabBar component, manages activeTab state (defaulting to "overview") and renders placeholder content for "overview", "deployments", and "time-tracking". Uses WSO2 oxygen-ui Box and Typography for layout and styling. (Note: useParams is imported but not yet used.)
📝 WalkthroughWalkthroughReplaces the placeholder App with a Router-backed shell: adds LoaderProvider, AppLayout, nested project routes and pages (dashboard, project details, chat, create-case), many new UI components, React Query hooks, mock data/generators, utils, and extensive unit tests. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant RouterApp as "App (Router)"
participant LoaderProv as "LoaderProvider"
participant AppShell as "AppLayout"
participant Page as "DashboardPage"
participant Hook as "useGetDashboardMockStats"
participant MockData as "mockFunctions"
Client->>RouterApp: navigate to "/:projectId/dashboard"
RouterApp->>LoaderProv: wrap routes
LoaderProv->>AppShell: render shell (header/sidebar/outlet)
AppShell->>Page: mount DashboardPage via Outlet
Page->>LoaderProv: showLoader()
Page->>Hook: fetch dashboard stats (projectId)
Hook->>MockData: generate/read mock stats (API_MOCK_DELAY)
MockData-->>Hook: return stats
Hook-->>Page: data received
Page->>LoaderProv: hideLoader()
Page->>AppShell: render charts and CasesTable
Client->>Page: click "Get Support"
Page->>RouterApp: navigate to "/:projectId/support"
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 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 |
There was a problem hiding this comment.
Pull request overview
This PR builds out the initial customer portal UI for project-level navigation and support, including a shared layout, routing, dashboard, and support experiences, all backed by mock data and React Query hooks.
Changes:
- Introduces a top-level
Approuter with anAppLayoutusing Oxygen UI’sAppShell, global notification banner, linear loader context, header, sidebar, and footer. - Adds project-level pages and components (Project Hub cards, Dashboard with stats/charts/cases table, Support page with support stats and Novera AI chat entry points, and case-creation layout), along with supporting models, constants, and mock-driven API hooks.
- Expands configuration and utilities (auth, API, logger, theme, notification banner, project/case utilities) and adds extensive Vitest coverage for the new components, hooks, and contexts, plus test-focused Vitest/vite tuning.
Reviewed changes
Copilot reviewed 138 out of 139 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/customer-portal/webapp/vite.config.ts | Updates Vitest config to inline Oxygen UI/chart dependencies and enable CSS for tests. |
| apps/customer-portal/webapp/src/utils/projectCard.ts | Adds helpers to format project dates and map project “health” statuses to theme color tokens. |
| apps/customer-portal/webapp/src/utils/logger.ts | Minor comment style clean-up for the logger implementation. |
| apps/customer-portal/webapp/src/utils/casesTable.ts | Adds color-mapping utilities for case priority and status used in the dashboard cases table. |
| apps/customer-portal/webapp/src/utils/tests/projectCard.test.ts | Unit tests for formatProjectDate and project status color mapping. |
| apps/customer-portal/webapp/src/utils/tests/logger.test.ts | Unit tests for logger level filtering, formatting, and log level parsing. |
| apps/customer-portal/webapp/src/utils/tests/casesTable.test.ts | Unit tests for priority and status color helpers used by the cases table. |
| apps/customer-portal/webapp/src/pages/SupportPage.tsx | New support page that loads project support stats, logs load/error states, renders stat cards and the Novera chat banner, and shows an error message on failure. |
| apps/customer-portal/webapp/src/pages/ProjectPage.tsx | Simple generic project sub-page component that displays a title and project ID. |
| apps/customer-portal/webapp/src/pages/ProjectDetails.tsx | Adds a project details page with a tabbed layout (overview/deployments/time-tracking) using the shared TabBar. |
| apps/customer-portal/webapp/src/models/responses.ts | Defines typed response models for projects, user profile, support/case stats, trend data, dashboard stats, and case lists. |
| apps/customer-portal/webapp/src/models/requests.ts | Defines typed request models for project search and case search (filters, pagination, sorting). |
| apps/customer-portal/webapp/src/layouts/AppLayout.tsx | New application layout using Oxygen UI AppShell, global notification banner, sidebar, header, footer, and linear loader integration around routed content. |
| apps/customer-portal/webapp/src/hooks/useLogger.ts | Simplifies logger hook documentation while keeping error guard when context is missing. |
| apps/customer-portal/webapp/src/context/logger/tests/LoggerContext.test.tsx | Updates copyright header style in logger context tests. |
| apps/customer-portal/webapp/src/context/logger/LoggerProvider.tsx | Removes redundant comment noise around logger provider props. |
| apps/customer-portal/webapp/src/context/logger/LoggerContext.tsx | Extends JSDoc on logger context with explicit type annotation in the comment. |
| apps/customer-portal/webapp/src/context/linearLoader/tests/LoaderContext.test.tsx | Adds tests verifying loader visibility toggling, error when used outside provider, and child rendering. |
| apps/customer-portal/webapp/src/context/linearLoader/LoaderContext.tsx | Implements a simple context/provider and hook for a global linear progress loader. |
| apps/customer-portal/webapp/src/constants/supportConstants.ts | Configures support stat cards (icons, colors, labels, keys) for the support page metrics. |
| apps/customer-portal/webapp/src/constants/projectDetailsConstants.ts | Defines tab metadata (ids, labels, icons) for the project details page. |
| apps/customer-portal/webapp/src/constants/appLayoutConstants.ts | Introduces sidebar navigation items and layout-related constants (URLs, company name). |
| apps/customer-portal/webapp/src/constants/apiConstants.ts | Adds a global mock delay and centralizes React Query key constants. |
| apps/customer-portal/webapp/src/config/themeConfig.ts | Converts theme configuration comments to line comments; retains theme selection based on env. |
| apps/customer-portal/webapp/src/config/notificationBannerConfig.ts | Adds typed config for a global notification banner sourced from environment variables. |
| apps/customer-portal/webapp/src/config/loggerConfig.ts | Minor comment style update for logger configuration. |
| apps/customer-portal/webapp/src/config/authConfig.ts | Minor comment style update for Asgardeo auth configuration. |
| apps/customer-portal/webapp/src/config/apiConfig.ts | Minor comment style updates for API base URL and config; preserves runtime env validation. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/tests/EscalationBanner.test.tsx | Tests EscalationBanner rendering, visibility toggling, and callback invocation. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/tests/ChatMessageList.test.tsx | Tests that the message list renders provided messages. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/tests/ChatMessageBubble.test.tsx | Tests user vs bot message rendering and presence of bot avatar icon. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/tests/ChatInput.test.tsx | Tests chat input behavior, send button, enter key, and escalation banner interactions. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/tests/ChatHeader.test.tsx | Tests that the chat header back button calls the provided callback. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/EscalationBanner.tsx | Implements an inline escalation banner prompting case creation from a chat. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/ChatMessageList.tsx | Renders a scrollable list of chat message bubbles with an end-of-list ref. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/ChatMessageBubble.tsx | Renders a single message bubble with different layout and avatar for user vs bot. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/ChatInput.tsx | Renders the chat input field, send button, and optional escalation banner with enter-key handling. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/ChatHeader.tsx | Renders the Novera chat header with back navigation and assistant branding. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatBanner/tests/NoveraChatBanner.test.tsx | Tests banner rendering and navigation to the chat route on button click. |
| apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatBanner/NoveraChatBanner.tsx | “Start New Chat” banner to route users into the Novera AI chat flow. |
| apps/customer-portal/webapp/src/components/support/casesOverviewStats/tests/CasesOverviewStatCard.test.tsx | Tests loading vs loaded rendering for support stat cards, including icons and labels. |
| apps/customer-portal/webapp/src/components/support/casesOverviewStats/CasesOverviewStatCard.tsx | Renders a grid of summary StatCards for project support stats with optional skeletons. |
| apps/customer-portal/webapp/src/components/support/caseCreationLayout/tests/ConversationSummary.test.tsx | Tests conversation summary sidebar behavior for loading, loaded, and missing metadata. |
| apps/customer-portal/webapp/src/components/support/caseCreationLayout/tests/CaseCreationHeader.test.tsx | Tests header titles, labels, and back button behavior on the case creation page. |
| apps/customer-portal/webapp/src/components/support/caseCreationLayout/tests/AIInfoCard.test.tsx | Tests the informational AI auto-population card content and icon. |
| apps/customer-portal/webapp/src/components/support/caseCreationLayout/ConversationSummary.tsx | Sidebar summarizing conversation metrics with skeleton loading and an informational tip. |
| apps/customer-portal/webapp/src/components/support/caseCreationLayout/CaseCreationHeader.tsx | Header section for reviewing AI-populated case details, with back navigation and “AI Generated” chip. |
| apps/customer-portal/webapp/src/components/support/caseCreationLayout/AIInfoCard.tsx | Small explanatory card indicating fields were auto-populated from the Novera conversation. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/tests/ProjectCardStats.test.tsx | Tests the stats section of a project card (counts, date formatting, icons). |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/tests/ProjectCardSkeleton.test.tsx | Tests that the project card skeleton renders expected structural placeholders. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/tests/ProjectCardInfo.test.tsx | Tests project card info block title/subtitle rendering. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/tests/ProjectCardBadges.test.tsx | Tests project card badges (key and status) and status color mapping. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/tests/ProjectCardActions.test.tsx | Tests project card actions calling the view-dashboard callback and stopping propagation. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/tests/ProjectCard.test.tsx | Integration-style tests for the full project card, navigation, and override callback. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/ProjectCardStats.tsx | Stats subcomponent for a project card showing open cases, active chats, and created date. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/ProjectCardSkeleton.tsx | Skeleton project card to show while project data is loading. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/ProjectCardInfo.tsx | Subcomponent rendering project card title and truncated description. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/ProjectCardBadges.tsx | Subcomponent rendering project key and status chips using getStatusColor. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/ProjectCardActions.tsx | Subcomponent rendering the “View Dashboard” button and wiring its click handler. |
| apps/customer-portal/webapp/src/components/projectHub/projectCard/ProjectCard.tsx | Full project card component combining badges, info, stats, and actions with mock fallbacks and navigation. |
| apps/customer-portal/webapp/src/components/dashboard/stats/tests/TrendIndicator.test.tsx | Tests trend indicator rendering for loading, up, and down trends. |
| apps/customer-portal/webapp/src/components/dashboard/stats/TrendIndicator.tsx | Trend pill component for showing up/down indicators and “vs last month” text. |
| apps/customer-portal/webapp/src/components/dashboard/stats/StatCard.tsx | Custom dashboard stat card component with icon, trend indicator, value, and tooltip. |
| apps/customer-portal/webapp/src/components/dashboard/charts/tests/ChartLegend.test.tsx | Tests that the chart legend renders items and colors correctly and handles empty data. |
| apps/customer-portal/webapp/src/components/dashboard/charts/tests/ChartLayout.test.tsx | Tests that layout renders all chart placeholders and passes loading state. |
| apps/customer-portal/webapp/src/components/dashboard/charts/tests/CasesTrendChart.test.tsx | Tests that the cases trend chart renders its title, skeleton state, chart, and legend. |
| apps/customer-portal/webapp/src/components/dashboard/charts/ChartLegend.tsx | Reusable legend component for charts showing colored dots and labels. |
| apps/customer-portal/webapp/src/components/dashboard/charts/ChartLayout.tsx | Layout that arranges outstanding incidents, active cases, and cases trend charts. |
| apps/customer-portal/webapp/src/components/dashboard/charts/CasesTrendChart.tsx | Stacked bar chart of case trends over time with legend and loading skeleton. |
| apps/customer-portal/webapp/src/components/dashboard/charts/ActiveCasesChart.tsx | Donut chart for distribution of active cases plus total center label and legend. |
| apps/customer-portal/webapp/src/components/dashboard/casesTable/tests/CasesTableHeader.test.tsx | Tests header copy, button callbacks, and active filter rendering for the cases table. |
| apps/customer-portal/webapp/src/components/dashboard/casesTable/CasesTableSkeleton.tsx | Row skeletons for the cases table during data loading. |
| apps/customer-portal/webapp/src/components/dashboard/casesTable/CasesTableHeader.tsx | Header for cases table with title, active filters display, and filter/create buttons. |
| apps/customer-portal/webapp/src/components/common/tabBar/TabBar.tsx | Generic tab bar component implemented with Oxygen UI Card + Button. |
| apps/customer-portal/webapp/src/components/common/sideNavBar/tests/SubscriptionWidget.test.tsx | Tests subscription widget visibility vs collapsed state and contents. |
| apps/customer-portal/webapp/src/components/common/sideNavBar/SubscriptionWidget.tsx | Sidebar subscription widget with short text and “View Details” button. |
| apps/customer-portal/webapp/src/components/common/sideNavBar/SideBar.tsx | Wrapper around Oxygen UI Sidebar to render navigation items, subscription widget, and settings link. |
| apps/customer-portal/webapp/src/components/common/notificationBanner/tests/GlobalNotificationBanner.test.tsx | Tests global notification banner visibility sync and dismissal. |
| apps/customer-portal/webapp/src/components/common/notificationBanner/GlobalNotificationBanner.tsx | Global notification banner component that respects config and supports dismissal. |
| apps/customer-portal/webapp/src/components/common/header/tests/UserProfile.test.tsx | Tests that user profile renders mock user name and email via UserMenu. |
| apps/customer-portal/webapp/src/components/common/header/tests/SearchBar.test.tsx | Tests that search bar renders with expected placeholder. |
| apps/customer-portal/webapp/src/components/common/header/tests/ProjectSwitcher.test.tsx | Tests project switcher behavior including loading skeleton and change callback. |
| apps/customer-portal/webapp/src/components/common/header/tests/Brand.test.tsx | Tests brand logo and “Customer Portal” title rendering. |
| apps/customer-portal/webapp/src/components/common/header/tests/Actions.test.tsx | Tests header action area (join community link, theme toggle, and user profile). |
| apps/customer-portal/webapp/src/components/common/header/UserProfile.tsx | Header user profile component wiring UserMenu to mock user and navigation/logger callbacks. |
| apps/customer-portal/webapp/src/components/common/header/SearchBar.tsx | Wrapper around Oxygen UI SearchBar with fixed placeholder and sizing. |
| apps/customer-portal/webapp/src/components/common/header/ProjectSwitcher.tsx | Header project switcher using ComplexSelect and a loading skeleton state. |
| apps/customer-portal/webapp/src/components/common/header/Brand.tsx | Header brand block with WSO2 logo and product name. |
| apps/customer-portal/webapp/src/components/common/header/Actions.tsx | Header action area with “Join our community” link, theme toggle, and user profile. |
| apps/customer-portal/webapp/src/components/common/footer/tests/Footer.test.tsx | Tests footer links and company name wiring from constants. |
| apps/customer-portal/webapp/src/components/common/footer/Footer.tsx | Footer wrapper passing company name and legal URLs to Oxygen UI Footer. |
| apps/customer-portal/webapp/src/api/useGetProjects.ts | React Query infinite-query hook to load projects from mock data with paging and optional “fetch all” mode. |
| apps/customer-portal/webapp/src/api/useGetProjectSupportStats.ts | React Query hook to load project support stats from mock functions with logging and mock delay. |
| apps/customer-portal/webapp/src/api/useGetProjectCasesStats.ts | React Query hook to load per-project case statistics, with logging and mock delay. |
| apps/customer-portal/webapp/src/api/useGetProjectCases.ts | React Query hook to search project cases using mock case data, filters, and pagination. |
| apps/customer-portal/webapp/src/api/useGetDashboardMockStats.ts | React Query hook exposing mock dashboard stats (trends and trend-series) per project. |
| apps/customer-portal/webapp/src/api/useGetCaseCreationDetails.ts | React Query hook to fetch mock case-creation metadata and conversation summary. |
| apps/customer-portal/webapp/src/api/tests/useGetProjects.test.tsx | Tests paging, default pagination, and fetchAll behavior of useGetProjects. |
| apps/customer-portal/webapp/src/api/tests/useGetProjectSupportStats.test.tsx | Tests success, logging, and disabled behavior for useGetProjectSupportStats. |
| apps/customer-portal/webapp/src/api/tests/useGetProjectCasesStats.test.tsx | Tests success and disabled behavior for useGetProjectCasesStats. |
| apps/customer-portal/webapp/src/api/tests/useGetDashboardMockStats.test.tsx | Tests success and disabled behavior for useGetDashboardMockStats. |
| apps/customer-portal/webapp/src/api/tests/useGetCaseCreationDetails.test.tsx | Tests successful and failing flows for the case creation metadata hook. |
| apps/customer-portal/webapp/src/App.tsx | Defines the top-level router, wraps the app with BrowserRouter and LoaderProvider, and wires all project and support routes into AppLayout. |
| apps/customer-portal/webapp/package.json | Updates dependencies (adds React Router core, Asgardeo router, bumps Oxygen UI charts, and reverts vite to a standard version without pnpm override). |
| apps/customer-portal/webapp/.env.example | Documents new environment variables for controlling the global maintenance banner. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 4
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🤖 Fix all issues with AI agents
In
`@apps/customer-portal/webapp/src/components/support/casesOverviewStats/CasesOverviewStatCard.tsx`:
- Around line 74-82: The component imports and uses the wrong StatCard and
unsafe casting: in CasesOverviewStatCard replace the external StatCard import
from `@wso2/oxygen-ui` with the local StatCard from
"@/components/dashboard/stats/StatCard", remove the unsafe cast of Skeleton to
number, and pass the loading state directly via the StatCard's isLoading prop
(e.g., value={stats?.[stat.key] ?? 0} and isLoading={isLoading}) so the local
StatCard handles skeleton rendering internally; update any prop types
accordingly in the CasesOverviewStatCard component so no unknown/number cast is
needed.
In
`@apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/ChatMessageList.tsx`:
- Around line 21-26: Move the duplicated Message interface into the shared
models module and export it, then replace the local interface declarations with
an import of Message in each component: remove the Message declaration from
ChatMessageList.tsx, ChatMessageBubble.tsx, and NoveraChatPage.tsx and add a
single import { Message } from the shared models file; ensure the exported
interface keeps the same fields (id: string, text: string, sender: "user" |
"bot", timestamp: Date) and update any prop/type usages (e.g., props or state
typed as Message[]) to reference the imported Message type.
In `@apps/customer-portal/webapp/src/pages/CreateCasePage.tsx`:
- Around line 56-63: The effect in CreateCasePage re-runs because showLoader and
hideLoader are recreated each render; update LoaderProvider by importing
useCallback and wrap the exported functions (showLoader and hideLoader) with
useCallback (e.g., memoize setIsVisible handlers) so their references remain
stable, then the useEffect([isLoading, showLoader, hideLoader]) will no longer
trigger unnecessarily.
In `@apps/customer-portal/webapp/src/utils/projectCard.ts`:
- Around line 24-40: formatProjectDate is timezone‑sensitive because it uses
local getters; make the output deterministic by normalizing to UTC: when
creating the date (in formatProjectDate) keep parsing the ISO string but then
use UTC-based accessors (e.g., getUTCDate and getUTCFullYear) or format the
month via Intl.DateTimeFormat('en-US', { month: 'short', timeZone: 'UTC' }) on
the same Date instance (refer to the date variable) so the returned string
always represents the UTC date regardless of the user's timezone.
🟡 Minor comments (17)
apps/customer-portal/webapp/src/pages/ProjectPage.tsx-31-43 (1)
31-43:⚠️ Potential issue | 🟡 MinorAdd runtime safety check for missing
projectIdfor defensive robustness.While the routing structure (
/:projectId/*) ensuresprojectIdis present when this component renders, React Router'suseParamsAPI types all params asstring | undefinedfor safety. Adding a runtime check aligns with React Router best practices.Instead of using a placeholder, prefer early return or error handling:
♻️ Recommended approach
export default function ProjectPage({ title }: ProjectPageProps): JSX.Element { const { projectId } = useParams<{ projectId: string }>(); + if (!projectId) return null; // or <ErrorBoundary /> / <NotFound /> return ( <Box> {/* project page title */} <Typography variant="h4" gutterBottom> {title} </Typography> {/* project page subtitle */} <Typography variant="body1" color="text.secondary"> - Displaying content for Project: <strong>{projectId}</strong> + Displaying content for Project: <strong>{projectId}</strong> </Typography> </Box> ); }Alternatively, the suggested fallback value works but early return is clearer about handling the edge case explicitly.
apps/customer-portal/webapp/src/components/common/tabBar/TabBar.tsx-28-47 (1)
28-47:⚠️ Potential issue | 🟡 MinorUnused
classNameprop in TabBarProps.The
classNameprop is declared inTabBarProps(line 32) but never destructured or applied to the component. This makes the public API misleading—consumers cannot style the tab bar container via this prop.Proposed fix
-const TabBar = ({ tabs, activeTab, onTabChange }: TabBarProps): JSX.Element => { +const TabBar = ({ + tabs, + activeTab, + onTabChange, + className, +}: TabBarProps): JSX.Element => { return ( <Card role="tablist" + className={className} sx={{apps/customer-portal/webapp/src/context/logger/LoggerContext.tsx-22-26 (1)
22-26:⚠️ Potential issue | 🟡 MinorUse
@typeinstead of@typesfor JSDoc.
@typesis not a standard JSDoc tag and will be ignored by tooling. The correct tag is@type.🔧 Proposed fix
- * `@types` {Context<ILogger | null>} The LoggerContext. + * `@type` {Context<ILogger | null>} The LoggerContext.apps/customer-portal/webapp/src/utils/__tests__/projectCard.test.ts-22-35 (1)
22-35:⚠️ Potential issue | 🟡 MinorTest expectation is timezone-dependent and will fail in western timezones.
The input
"2026-01-29T11:28:40+05:30"represents 29 Jan 2026 11:28:40 IST. JavaScript parses this as UTC (05:58:40Z on 29 Jan), thengetDate()andgetFullYear()return values in the local machine timezone. In PST (UTC-8), this becomes 28 Jan 21:58:40, causing the test to expect day "28" instead of "29".Either:
- Use a fixed
timeZoneparameter in date formatting (e.g.,date.toLocaleString("en-US", { month: "short", timeZone: "UTC" })) and align the test to that timezone, or- Mock the system timezone in tests to ensure consistent results across environments.
apps/customer-portal/webapp/src/utils/casesTable.ts-40-47 (1)
40-47:⚠️ Potential issue | 🟡 MinorGuard
getStatusColoragainst empty/undefined labels.
label.toLowerCase()will throw if status is missing; mirror the defensive pattern used ingetPriorityColor.🛠️ Suggested fix
-export const getStatusColor = (label: string): string => { - const normalized = label.toLowerCase(); +export const getStatusColor = (label?: string): string => { + const normalized = label?.toLowerCase() || ""; if (normalized.includes("open")) return "primary.main"; if (normalized.includes("awaiting")) return "info.main"; if (normalized.includes("progress")) return "warning.main"; if (normalized.includes("resolved") || normalized.includes("closed")) return "success.main"; return "text.secondary"; };apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/ChatInput.tsx-52-63 (1)
52-63:⚠️ Potential issue | 🟡 MinorPrevent default on Enter regardless of input content.
This avoids accidental form submits if ChatInput is embedded in a
<form>.🛠️ Suggested fix
onKeyDown={(e) => { - if (e.key === "Enter") { - if (!inputValue.trim()) return; - e.preventDefault(); - onSend(); - } + if (e.key !== "Enter") return; + e.preventDefault(); + if (!inputValue.trim()) return; + onSend(); }}apps/customer-portal/webapp/src/constants/dashboardConstants.ts-159-164 (1)
159-164:⚠️ Potential issue | 🟡 MinorInconsistent casing in deployment option values.
"Development"uses PascalCase while"production"uses lowercase. This inconsistency may cause issues with filtering if backend expects consistent casing.🔧 Suggested fix
options: [ { label: "Development", value: "Development" }, - { label: "Production", value: "production" }, + { label: "Production", value: "Production" }, { label: "QA", value: "QA" }, { label: "Staging", value: "Staging" }, ],apps/customer-portal/webapp/src/components/common/filterPanel/ActiveFilters.tsx-46-106 (1)
46-106:⚠️ Potential issue | 🟡 MinorHandle falsy-but-valid filter values consistently.
Boolean(value)andvalue || field.labeltreat0/falseas inactive, which can hide valid filters and block deletion.🛠️ Suggested fix
- const activeFiltersCount = - Object.values(appliedFilters).filter(Boolean).length; + const hasFilterValue = (value: unknown): boolean => { + if (Array.isArray(value)) { + return value.length > 0; + } + return value !== undefined && value !== null && value !== ""; + }; + + const activeFiltersCount = + Object.values(appliedFilters).filter(hasFilterValue).length; @@ - const isActive = Boolean(value); + const isActive = hasFilterValue(value); + const displayLabel = isActive ? String(value) : field.label; @@ - label={value || field.label} + label={displayLabel}apps/customer-portal/webapp/src/components/dashboard/charts/ChartLayout.tsx-50-55 (1)
50-55:⚠️ Potential issue | 🟡 MinorFix JSDoc param types to match the actual prop shapes.
The current@paramentries saynumberfor object props, which is misleading.📝 Suggested doc fix
- * `@param` {number} props.outstandingIncidents - Number of outstanding incidents. - * `@param` {number} props.activeCases - Number of active cases. - * `@param` {CasesTrendData[]} props.casesTrend - Array of trend data for cases. + * `@param` {Object} props.outstandingIncidents - Breakdown of outstanding incidents. + * `@param` {Object} props.activeCases - Breakdown of active cases. + * `@param` {Array<{ name: string; TypeA: number; TypeB: number; TypeC: number; TypeD: number }>} props.casesTrend - Trend data for cases.apps/customer-portal/webapp/src/pages/__tests__/CreateCasePage.test.tsx-125-197 (1)
125-197:⚠️ Potential issue | 🟡 MinorReset mocked hook return values per test to avoid order coupling.
The error-case override can leak into other tests if execution order changes. Add a default mock return inbeforeEach.Suggested fix
+const defaultCaseCreationDetails = { + projects: ["Production Environment-Main"], + products: ["WSO2 API Manager - v4.2.0"], + deploymentTypes: ["Production"], + issueTypes: ["Partial Outage"], + severityLevels: [ + { id: "S1", label: "S1", description: "Desc 1" }, + { id: "S2", label: "S2", description: "Desc 2" }, + ], + conversationSummary: { + messagesExchanged: 8, + troubleshootingAttempts: "2 steps completed", + kbArticlesReviewed: "3 articles suggested", + }, +}; + vi.mock("@/api/useGetCaseCreationDetails", () => ({ useGetCaseCreationDetails: vi.fn(() => ({ - data: { - projects: ["Production Environment-Main"], - products: ["WSO2 API Manager - v4.2.0"], - deploymentTypes: ["Production"], - issueTypes: ["Partial Outage"], - severityLevels: [ - { id: "S1", label: "S1", description: "Desc 1" }, - { id: "S2", label: "S2", description: "Desc 2" }, - ], - conversationSummary: { - messagesExchanged: 8, - troubleshootingAttempts: "2 steps completed", - kbArticlesReviewed: "3 articles suggested", - }, - }, - isLoading: false, + data: defaultCaseCreationDetails, + isLoading: false, + isError: false, })), })); describe("CreateCasePage", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useGetCaseCreationDetails).mockReturnValue({ + data: defaultCaseCreationDetails, + isLoading: false, + isError: false, + } as any); + });apps/customer-portal/webapp/src/components/dashboard/stats/TrendIndicator.tsx-62-73 (1)
62-73:⚠️ Potential issue | 🟡 MinorTrend color ignores non-success values.
If
trend.coloris"info"or"warning", the current logic still renderstext.secondary, so the UI won’t reflect the provided color.🎨 Suggested fix to honor all allowed colors
- color: - trend.direction === "down" - ? "error.main" - : trend.color === "success" - ? "success.main" - : "text.secondary", + color: + trend.direction === "down" + ? "error.main" + : `${trend.color}.main`,apps/customer-portal/webapp/src/components/support/caseCreationLayout/ConversationSummary.tsx-96-110 (1)
96-110:⚠️ Potential issue | 🟡 MinorInteractive element missing click handler.
The "View full conversation" text is styled as a clickable link (cursor: pointer, hover underline) but lacks an
onClickhandler. This could confuse users who expect it to be functional.Consider adding a handler or TODO
<Typography variant="body2" + onClick={isLoading ? undefined : () => { /* TODO: implement */ }} sx={{ color: "primary.main", cursor: isLoading ? "default" : "pointer",Or use a
Buttoncomponent withvariant="text"for better semantics if this will be interactive.apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx-95-106 (1)
95-106:⚠️ Potential issue | 🟡 MinorPotential message ID collision with rapid sends.
Using
Date.now() + 1for bot message IDs could collide if the user sends messages faster than 1ms apart. Consider using a more robust ID generation approach.Proposed fix using crypto.randomUUID
const timeoutId = window.setTimeout(() => { const botMessage: Message = { - id: (Date.now() + 1).toString(), + id: crypto.randomUUID(), text: getNoveraResponse(), sender: "bot", timestamp: new Date(), };apps/customer-portal/webapp/src/pages/CreateCasePage.tsx-71-71 (1)
71-71:⚠️ Potential issue | 🟡 MinorHardcoded array index for severity selection is fragile.
Using
metadata.severityLevels?.[1]?.idassumes the second severity level is always the appropriate default. If the array order changes or has fewer than 2 elements, this could select an unexpected value or undefined.Consider selecting by a known ID or using a default constant.
Suggested approach
- setSeverity(metadata.severityLevels?.[1]?.id || ""); + // Select a sensible default severity by ID, or fall back to first available + const defaultSeverity = metadata.severityLevels?.find( + (level) => level.id === "S2" + )?.id ?? metadata.severityLevels?.[0]?.id ?? ""; + setSeverity(defaultSeverity);apps/customer-portal/webapp/src/components/common/sideNavBar/SideBar.tsx-63-76 (1)
63-76:⚠️ Potential issue | 🟡 MinorHandle undefined
projectIdin link construction.When
projectIdis undefined, the link will be/undefined/${item.path}, which leads to broken navigation. Consider guarding against this or providing a fallback.🛡️ Proposed fix
{APP_SHELL_NAV_ITEMS.map((item) => ( <Link key={item.id} component={NavigateLink} - to={`/${projectId}/${item.path}`} + to={projectId ? `/${projectId}/${item.path}` : `/${item.path}`} color="inherit" underline="none" >Alternatively, if navigation should be disabled without a project context, consider conditionally rendering or disabling the links.
apps/customer-portal/webapp/src/components/common/sideNavBar/SideBar.tsx-85-97 (1)
85-97:⚠️ Potential issue | 🟡 MinorSame undefined
projectIdissue in Settings link.The Settings link has the same potential issue with undefined
projectId.🛡️ Proposed fix
<Link component={NavigateLink} - to={`/${projectId}/settings`} + to={projectId ? `/${projectId}/settings` : "/settings"} color="inherit" underline="none" >apps/customer-portal/webapp/src/components/common/header/Header.tsx-128-133 (1)
128-133:⚠️ Potential issue | 🟡 MinorThe
collapsedprop is not being passed toHeaderUI.Toggle.The component receives a
collapsedprop (line 41) but line 132 hardcodescollapsed={false}. This means the sidebar toggle won't reflect the actual collapsed state.🐛 Proposed fix
<HeaderUI> {!isProjectHub && ( /* header sidebar toggle */ - <HeaderUI.Toggle collapsed={false} onToggle={onToggleSidebar} /> + <HeaderUI.Toggle collapsed={collapsed} onToggle={onToggleSidebar} /> )}
🧹 Nitpick comments (49)
apps/customer-portal/webapp/src/pages/ProjectDetails.tsx (2)
28-30: Derive initial tab from constants to avoid drift.Line 29 hard-codes
"overview"; this can get out of sync if tab IDs change. Prefer bootstrapping fromPROJECT_DETAILS_TABS.♻️ Proposed fix
-export default function ProjectDetails(): JSX.Element { - const [activeTab, setActiveTab] = useState<string>("overview"); +export default function ProjectDetails(): JSX.Element { + const [activeTab, setActiveTab] = useState<string>( + PROJECT_DETAILS_TABS[0]?.id ?? "overview", + );
31-59: Avoid rendering nothing on unexpected tab IDs.Line 58 returns
null, which yields a blank content area ifactiveTabis invalid (e.g., deep link or future tab changes). Consider a small fallback.🛠️ Suggested fallback
case "time-tracking": return ( <Box sx={{ p: 3, textAlign: "center" }}> <Typography variant="h6" color="text.secondary"> Time Tracking (Coming Soon) </Typography> </Box> ); default: - return null; + return ( + <Box sx={{ p: 3, textAlign: "center" }}> + <Typography variant="h6" color="text.secondary"> + Coming Soon + </Typography> + </Box> + ); } };apps/customer-portal/webapp/src/components/common/tabBar/__tests__/TabBar.test.tsx (1)
108-113: Test doesn’t verify the custom className.The test name says it checks className, but it only asserts the tablist exists. Consider asserting the class itself (especially if TabBar forwards it).
🧪 Suggested assertion
- expect(screen.getByRole("tablist")).toBeInTheDocument(); + const tablist = screen.getByRole("tablist"); + expect(tablist).toBeInTheDocument(); + expect(tablist).toHaveClass("custom-class");apps/customer-portal/webapp/src/context/linearLoader/LoaderContext.tsx (1)
33-45: Consider reference-counting to avoid premature hides.
If multiple operations callshowLoader/hideLoader, a boolean can hide while another is still in flight. A simple counter avoids that.♻️ Suggested reference-counted loader
- const [isVisible, setIsVisible] = useState(false); - - const showLoader = () => setIsVisible(true); - const hideLoader = () => setIsVisible(false); + const [visibleCount, setVisibleCount] = useState(0); + const isVisible = visibleCount > 0; + + const showLoader = () => setVisibleCount((c) => c + 1); + const hideLoader = () => setVisibleCount((c) => Math.max(0, c - 1));apps/customer-portal/webapp/src/config/notificationBannerConfig.ts (1)
27-35: Validate env-providedseverityto avoid unsupported values.
import.meta.envis runtime data; an unexpected string will bypass the union and could break UI styling. Consider normalizing to the allowed set with a safe fallback.♻️ Suggested normalization
-export const notificationBannerConfig: NotificationBannerConfig = { +const rawSeverity = + import.meta.env.CUSTOMER_PORTAL_MAINTENANCE_BANNER_SEVERITY || "info"; +const severity: NotificationBannerConfig["severity"] = + rawSeverity === "info" || + rawSeverity === "warning" || + rawSeverity === "error" || + rawSeverity === "success" + ? rawSeverity + : "info"; + +export const notificationBannerConfig: NotificationBannerConfig = { actionLabel: import.meta.env.CUSTOMER_PORTAL_MAINTENANCE_BANNER_ACTION_LABEL, actionUrl: import.meta.env.CUSTOMER_PORTAL_MAINTENANCE_BANNER_ACTION_URL, message: import.meta.env.CUSTOMER_PORTAL_MAINTENANCE_BANNER_MESSAGE || "", - severity: - import.meta.env.CUSTOMER_PORTAL_MAINTENANCE_BANNER_SEVERITY || "info", + severity, title: import.meta.env.CUSTOMER_PORTAL_MAINTENANCE_BANNER_TITLE || "", visible: import.meta.env.CUSTOMER_PORTAL_MAINTENANCE_BANNER_VISIBLE === "true", };apps/customer-portal/webapp/src/components/support/caseCreationLayout/CaseDetailsSection.tsx (1)
33-44: Typemetadatato avoidanyleakage.Tightening the metadata shape prevents silent runtime issues and improves IDE help.
♻️ Suggested typing
-interface CaseDetailsSectionProps { +interface CaseCreationMetadata { + issueTypes: string[]; + severityLevels: Array<{ + id: string; + label: string; + description: string; + }>; +} + +interface CaseDetailsSectionProps { title: string; setTitle: (value: string) => void; description: string; setDescription: (value: string) => void; issueType: string; setIssueType: (value: string) => void; severity: string; setSeverity: (value: string) => void; - metadata: any; + metadata?: CaseCreationMetadata; isLoading: boolean; }apps/customer-portal/webapp/src/components/common/filterPanel/FilterPopover.tsx (2)
85-91: Avoidas anytype assertion inhandleReset.The reset logic uses
as anywhich bypasses type safety. Consider using a properly typed approach.♻️ Suggested improvement
const handleReset = () => { - const resetState = fields.reduce((acc, field) => { - acc[field.id] = ""; - return acc; - }, {} as any); + const resetState = fields.reduce( + (acc, field) => { + acc[field.id as keyof T] = "" as T[keyof T]; + return acc; + }, + {} as T, + ); setTempFilters(resetState); };
123-125: Duplicate comment.Line 124 duplicates the comment on line 123.
🧹 Remove duplicate comment
<Box sx={{ display: "flex", flexDirection: "column", gap: 3, pt: 2 }}> {/* filter popover fields */} - {/* filter popover fields */} {fields.map((field) =>apps/customer-portal/webapp/src/constants/dashboardConstants.ts (1)
40-40: Consider stronger typing foriconproperty.Using
anyfor the icon type loses type safety. Consider using the icon component type from the library.♻️ Suggested improvement
+import type { ComponentType, SVGProps } from "react"; + export interface StatConfigItem { id: Exclude<keyof DashboardMockStats, "casesTrend">; label: string; - icon: any; + icon: ComponentType<SVGProps<SVGSVGElement>>; iconColor: StatCardColor; tooltipText: string; }apps/customer-portal/webapp/src/components/dashboard/charts/CasesTrendChart.tsx (1)
98-104: Hardcodedvalue: 0in legend data is a workaround.The
ChartLegendcomponent expects avalueproperty, but it's not used for display in the legend. Passingvalue: 0works but is semantically misleading. Consider makingvalueoptional inChartLegendPropsif it's not always needed.This would require updating
ChartLegend.tsx:interface ChartLegendProps { data: Array<{ name: string; value?: number; color: string }>; }apps/customer-portal/webapp/src/components/common/filterPanel/__tests__/FilterPopover.test.tsx (1)
258-275: Reset test verifies UI state but doesn't verify callback behavior.The reset test only checks that the UI value changes to empty string. Consider also verifying whether
onSearchoronCloseshould be called (or explicitly not called) after reset, depending on intended behavior.apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatBanner/__tests__/NoveraChatBanner.test.tsx (1)
21-26: Consider clearingmockNavigatebetween tests.The mock function is not cleared between tests. If additional tests are added later, stale call data could cause flaky assertions.
🧹 Proposed fix
const mockNavigate = vi.fn(); // Mock react-router vi.mock("react-router", () => ({ useNavigate: () => mockNavigate, })); + +beforeEach(() => { + mockNavigate.mockClear(); +});apps/customer-portal/webapp/src/components/support/noveraAIAssistant/noveraChatPage/__tests__/ChatMessageList.test.tsx (1)
35-46: Test coverage is minimal; consider adding edge cases.The current test only verifies that message text renders. Consider adding tests for:
- Empty messages array
- Different sender types (bot vs user) rendering differently
- The
messagesEndRefscroll behaviorAlso, using
anyfor the messages array loses type safety. IfChatMessagetype is exported, prefer using it.🧪 Example additional test
it("should render empty state when no messages", () => { const ref: any = { current: null }; render(<ChatMessageList messages={[]} messagesEndRef={ref} />); // Verify container renders without errors expect(screen.getByTestId("box")).toBeInTheDocument(); });apps/customer-portal/webapp/src/components/common/header/__tests__/ProjectSwitcher.test.tsx (1)
108-120: Consider adding test for undefinedselectedProject.The component handles
selectedProject?.id || ""with optional chaining. A test verifying behavior whenselectedProjectis undefined would increase confidence in the fallback rendering (shows "Select Project" placeholder).🧪 Example test
it("should render placeholder when no project is selected", () => { render( <ProjectSwitcher projects={mockProjects} selectedProject={undefined} onProjectChange={mockOnProjectChange} />, ); expect(screen.getByTestId("project-select")).toHaveValue(""); });apps/customer-portal/webapp/src/components/support/caseCreationLayout/__tests__/BasicInformationSection.test.tsx (1)
75-84: Consider clearing mock functions between tests.The mock functions (
setProject,setProduct,setDeployment) indefaultPropsare shared across all tests without being cleared. This could cause test pollution if one test's assertions depend on call counts or previous calls from other tests.♻️ Suggested improvement
+import { beforeEach } from "vitest"; describe("BasicInformationSection", () => { const mockMetadata = { projects: ["Project 1", "Project 2"], products: ["Product 1", "Product 2"], deploymentTypes: ["Dev", "Prod"], }; - const defaultProps = { + let defaultProps: ReturnType<typeof createDefaultProps>; + + const createDefaultProps = () => ({ project: "Project 1", setProject: vi.fn(), product: "Product 1", setProduct: vi.fn(), deployment: "Dev", setDeployment: vi.fn(), metadata: mockMetadata, isLoading: false, - }; + }); + + beforeEach(() => { + defaultProps = createDefaultProps(); + });apps/customer-portal/webapp/src/components/support/caseCreationLayout/__tests__/CaseDetailsSection.test.tsx (1)
114-125: Consider clearing mock functions between tests.Similar to
BasicInformationSection.test.tsx, the mock functions indefaultPropsare shared across tests without being reset. UsebeforeEachto create fresh mocks for each test.apps/customer-portal/webapp/src/components/dashboard/casesTable/__tests__/CasesList.test.tsx (1)
110-111: Consider clearing mock functions between tests.
mockOnPageChangeandmockOnRowsPerPageChangeare declared at module level and not cleared between tests. This could lead to accumulated call counts affecting assertions.♻️ Suggested improvement
+import { beforeEach } from "vitest"; - const mockOnPageChange = vi.fn(); - const mockOnRowsPerPageChange = vi.fn(); + let mockOnPageChange: ReturnType<typeof vi.fn>; + let mockOnRowsPerPageChange: ReturnType<typeof vi.fn>; + + beforeEach(() => { + mockOnPageChange = vi.fn(); + mockOnRowsPerPageChange = vi.fn(); + });apps/customer-portal/webapp/src/components/common/footer/__tests__/Footer.test.tsx (1)
46-49: Use consistent assertion methods.Line 48 uses
toBeDefined()while the other tests usetoHaveAttribute(). For DOM element presence checks,toBeInTheDocument()is more idiomatic with@testing-library/react.♻️ Suggested fix
it("should render the company name", () => { render(<Footer />); - expect(screen.getByText(new RegExp(COMPANY_NAME, "i"))).toBeDefined(); + expect(screen.getByText(new RegExp(COMPANY_NAME, "i"))).toBeInTheDocument(); });apps/customer-portal/webapp/src/pages/ProjectHub.tsx (1)
64-159: Consider extracting the repeated card wrapper styles.The Box
sxobject is duplicated in both loading and loaded grids; a shared constant would reduce maintenance overhead.♻️ Suggested refactor
+ const cardWrapperSx = { + flex: { + xs: "1 1 100%", + sm: "0 1 calc(50% - 24px)", + md: "0 1 calc(33.33% - 24px)", + lg: "0 1 calc(25% - 24px)", + xl: "0 1 calc(20% - 24px)", + }, + maxWidth: { + xs: "100%", + sm: 400, + }, + minWidth: 300, + }; ... - <Box - key={index} - sx={{ - flex: { - xs: "1 1 100%", - sm: "0 1 calc(50% - 24px)", - md: "0 1 calc(33.33% - 24px)", - lg: "0 1 calc(25% - 24px)", - xl: "0 1 calc(20% - 24px)", - }, - maxWidth: { - xs: "100%", - sm: 400, - }, - minWidth: 300, - }} - > + <Box key={index} sx={cardWrapperSx}> ... - <Box - key={project.id} - sx={{ - flex: { - xs: "1 1 100%", - sm: "0 1 calc(50% - 24px)", - md: "0 1 calc(33.33% - 24px)", - lg: "0 1 calc(25% - 24px)", - xl: "0 1 calc(20% - 24px)", - }, - maxWidth: { - xs: "100%", - sm: 400, - }, - minWidth: 300, - }} - > + <Box key={project.id} sx={cardWrapperSx}>apps/customer-portal/webapp/src/components/support/casesOverviewStats/__tests__/CasesOverviewStatCard.test.tsx (1)
30-45: Dead code in StatCard mock.The conditional logic checking for
"Skeleton" in value(lines 31-34, 41) will never match because the component passes a React element (<Skeleton />) asvalue, not an object with a "Skeleton" property. TheValueSkeletonvariable is never truthy. The mock still works correctly because React elements passed asvalueare rendered directly in the JSX.Consider simplifying:
Suggested simplification
StatCard: ({ label, value, icon }: any) => { - const ValueSkeleton = - value && typeof value === "object" && "Skeleton" in value - ? (value as any).Skeleton - : null; - return ( <div data-testid="oxygen-stat-card"> <div data-testid="stat-card-icon">{icon}</div> <span>{label}</span> - <div data-testid="stat-card-value"> - {ValueSkeleton ? <ValueSkeleton variant="text" /> : value} - </div> + <div data-testid="stat-card-value">{value}</div> </div> ); },apps/customer-portal/webapp/src/api/__tests__/useGetProjects.test.tsx (1)
89-99: Test title mentions query key verification but only asserts limit.The test is titled "should use 'all' query key and larger limit when fetchAll is true" but only verifies the limit (100). The query key aspect isn't directly testable without inspecting QueryClient internals or spying on the query function.
Consider either updating the title to match what's tested, or adding a comment explaining why query key verification is omitted.
Suggested title update
- it("should use 'all' query key and larger limit when fetchAll is true", async () => { + it("should use larger limit (100) when fetchAll is true", async () => {apps/customer-portal/webapp/src/api/useGetProjectCases.ts (1)
57-63: Consider adding a TODO for production cleanup.Per learnings, this fallback to
mockCases.slice(0, limit)whenfilteredCasesis empty is intentional for demo purposes. However, this behavior will need to be removed when integrating with the real API, as it would mask genuine "no data" scenarios.Consider adding a TODO comment to document this:
+ // TODO: Remove fallback to mockCases when real API is integrated. + // This fallback exists only for demo purposes to always show data. const response: CaseSearchResponse = { cases: pagedCases.length > 0 ? pagedCases : mockCases.slice(0, limit),apps/customer-portal/webapp/src/components/common/header/__tests__/Actions.test.tsx (1)
47-62: Remove unused icon mocks.The Actions component only uses the
Usersicon, but this mock includes many unused icons (Briefcase, FileText, FolderOpen, Headset, Home, Megaphone, RefreshCw, Shield, Settings). Consider simplifying:♻️ Suggested cleanup
// Mock icons vi.mock("@wso2/oxygen-ui-icons-react", () => ({ - Briefcase: mockIcon("Briefcase"), - FileText: mockIcon("FileText"), - FolderOpen: mockIcon("FolderOpen"), - Headset: mockIcon("Headset"), - Home: mockIcon("Home"), - Megaphone: mockIcon("Megaphone"), - RefreshCw: mockIcon("RefreshCw"), - Shield: mockIcon("Shield"), Users: () => <svg data-testid="icon-Users" />, - Settings: mockIcon("Settings"), });apps/customer-portal/webapp/src/pages/__tests__/NoveraChatPage.test.tsx (2)
114-114: Consider a more robust selector for the send button.Using
screen.getByTestId("send-icon").parentElement!with a non-null assertion is fragile. If the DOM structure changes, this will throw a runtime error rather than a clear test failure.Consider adding a
data-testidto the IconButton mock or using a more descriptive approach:♻️ Suggested improvement
- IconButton: ({ children, onClick, disabled }: any) => ( - <button onClick={onClick} disabled={disabled}> + IconButton: ({ children, onClick, disabled, "aria-label": ariaLabel }: any) => ( + <button onClick={onClick} disabled={disabled} aria-label={ariaLabel}> {children} </button> ),Then in tests:
- const sendButton = screen.getByTestId("send-icon").parentElement!; + const sendButton = screen.getByRole("button", { name: /send/i });
116-132: Comments show incorrect message counts.The comments don't account for the initial bot message. With the initial bot message, the actual counts are:
- After 1st user message: 2 messages (1 bot + 1 user)
- After 2nd user message: 3 messages
- After 3rd user message: 4 messages
- After 4th user message: 5 messages → triggers escalation (> 4)
The test logic is correct, but the comments are misleading.
apps/customer-portal/webapp/src/components/dashboard/casesTable/CasesTableHeader.tsx (2)
33-33: Remove unusedprojectIdprop.The
projectIdprop is declared in the interface and destructured but never used in the component. Either remove it or implement its intended usage.♻️ Suggested fix
interface CasesTableHeaderProps { activeFiltersCount: number; appliedFilters: Record<string, string>; filterFields: ActiveFilterConfig[]; onRemoveFilter: (field: string) => void; onClearAll: () => void; onUpdateFilter: (field: string, value: any) => void; onFilterClick: () => void; onCreateCase: () => void; - projectId: string; } const CasesTableHeader = ({ activeFiltersCount, appliedFilters, filterFields, onRemoveFilter, onClearAll, onUpdateFilter, onFilterClick, onCreateCase, -}: CasesTableHeaderProps): JSX.Element => { +}: Omit<CasesTableHeaderProps, 'projectId'>): JSX.Element => {Or if the prop will be needed later, add a TODO comment.
Also applies to: 45-45
95-97: "All cases" button has no click handler.This button appears to be a placeholder without functionality. Consider either implementing the handler or adding a TODO comment to track this.
- <Button variant="outlined" size="small" color="warning"> + {/* TODO: Implement navigation to all cases view */} + <Button variant="outlined" size="small" color="warning" disabled> All cases </Button>apps/customer-portal/webapp/src/components/support/caseCreationLayout/BasicInformationSection.tsx (1)
30-38: Typemetadatato avoidany.Using a concrete type here improves safety and auto-complete for the select options.
🧩 Suggested typing update
import { Pencil, Sparkles } from "@wso2/oxygen-ui-icons-react"; import type { JSX } from "react"; +import type { CaseCreationMetadata } from "@/models/mockData"; @@ - metadata: any; + metadata: CaseCreationMetadata | undefined;apps/customer-portal/webapp/src/components/dashboard/casesTable/__tests__/CasesTableHeader.test.tsx (1)
45-56: Consider addingbeforeEachto reset mock functions.The
mockPropscallbacks (onCreateCase,onFilterClick, etc.) are not reset between tests. If tests run in a specific order and one test checks call counts, it could lead to flaky results.Proposed fix
+import { beforeEach, describe, expect, it, vi } from "vitest"; + +beforeEach(() => { + vi.clearAllMocks(); +}); + describe("CasesTableHeader", () => { const mockProps = {apps/customer-portal/webapp/src/components/common/sideNavBar/SubscriptionWidget.tsx (2)
65-72: Placeholder text appears incomplete.The text "Information about" / "subscription" seems like placeholder content. Consider replacing with meaningful copy or adding a TODO comment if this is intentional.
39-79: Remove unnecessary Fragment wrapper.The Fragment (
<>...</>) wrapping the Paper is unnecessary since there's only one root element.Proposed fix
- return ( - <> - {/* subscription widget container */} - <Paper + return ( + <Paper sx={{ p: 1.5, m: 1.5, border: "1px solid", borderColor: "divider", }} > ... - </Paper> - </> - ); + </Paper> + );apps/customer-portal/webapp/src/components/support/caseCreationLayout/ConversationSummary.tsx (1)
27-27: Type imported from mockData module.
CaseCreationMetadatais imported from@/models/mockData. If this type will be used in production, consider moving it to a dedicated types/models file to avoid coupling production code to mock data modules.apps/customer-portal/webapp/src/components/dashboard/stats/StatCard.tsx (2)
83-87: Redundant type assertion.The
iconColor as StatCardColorcast is unnecessary sinceiconColoris already typed asStatCardColorin the props interface (line 37).Suggested simplification
bgcolor: alpha( - theme.palette[iconColor as StatCardColor].light, + theme.palette[iconColor].light, 0.1, ), - color: theme.palette[iconColor as StatCardColor].light, + color: theme.palette[iconColor].light,
116-118: Consider wrapping the icon in a span for Tooltip compatibility.Some icon components don't forward refs, which can cause MUI/Oxygen Tooltip to log a warning. Wrapping in a
<span>ensures proper ref forwarding.Suggested fix
<Tooltip title={tooltipText} arrow placement="bottom"> - <Info size={14} /> + <span style={{ display: "inline-flex" }}> + <Info size={14} /> + </span> </Tooltip>apps/customer-portal/webapp/src/pages/CreateCasePage.tsx (1)
92-94: Form submission handler is a no-op.The
handleSubmitfunction only prevents default behavior without implementing actual case creation logic. This is acceptable for a WIP page, but consider adding a TODO comment for clarity.Would you like me to open an issue to track implementing the case submission logic?
apps/customer-portal/webapp/src/components/dashboard/casesTable/CasesTable.tsx (2)
33-33: Consider adding a typed filter state interface.Using
Record<string, any>for filters loses type safety. A dedicated interface would provide better autocomplete and catch errors at compile time.Suggested type definition
+interface CaseFilters { + deploymentId?: string; + severityId?: string; + statusId?: string; + caseTypes?: string; +} + const CasesTable = ({ projectId }: CasesTableProps): JSX.Element => { const navigate = useNavigate(); - const [filters, setFilters] = useState<Record<string, any>>({}); + const [filters, setFilters] = useState<CaseFilters>({});
101-105: Consider memoizing or simplifying activeFilterFields.This mapping creates a new array on every render. If
CasesTableHeaderonly needsid,label, andoptions, consider either:
- Passing
FILTER_FIELDSdirectly if the extratypeproperty is harmless- Memoizing with
useMemoif transformation is necessaryOption 1: Pass FILTER_FIELDS directly
<CasesTableHeader activeFiltersCount={Object.keys(filters).length} appliedFilters={mappedAppliedFilters} - filterFields={activeFilterFields} + filterFields={FILTER_FIELDS}Option 2: Memoize the transformation
- const activeFilterFields = FILTER_FIELDS.map((field) => ({ - id: field.id, - label: field.label, - options: field.options, - })); + const activeFilterFields = useMemo( + () => + FILTER_FIELDS.map((field) => ({ + id: field.id, + label: field.label, + options: field.options, + })), + [], + );apps/customer-portal/webapp/src/api/useGetProjects.ts (1)
44-46: Consider cache key collision with "default" fallback.When
searchDatais undefined, the queryKey falls back to"default", which could cause unintended cache sharing between different components callinguseGetProjects()without parameters. This may lead to stale or incorrect data being served.Consider using a more explicit fallback or requiring
searchData:const queryKey = fetchAll ? [ApiQueryKeys.PROJECTS, "all"] - : [ApiQueryKeys.PROJECTS, searchData ?? "default"]; + : [ApiQueryKeys.PROJECTS, searchData ?? {}];apps/customer-portal/webapp/src/components/common/header/ProjectSwitcher.tsx (2)
74-74: Avoidanytype for event handler.Using
anybypasses TypeScript's type checking. Consider using the proper event type from the UI library or a more specific type.♻️ Proposed fix
- onChange={(event: any) => onProjectChange(event.target.value)} + onChange={(event: React.ChangeEvent<{ value: unknown }>) => + onProjectChange(event.target.value as string) + }Or if the library provides a specific type, use that instead.
69-101: Consider handling empty projects array.When
projectsis empty andisLoadingis false, the dropdown renders with no items and shows "Select Project". Consider showing an informative message or disabling the dropdown when no projects are available.apps/customer-portal/webapp/src/pages/DashboardPage.tsx (3)
17-17: Unused import:LinearProgress.
LinearProgressis imported but not used in this component since the global loader context handles the loading indicator.♻️ Proposed fix
-import { Box, Button, Grid, LinearProgress, Typography } from "@wso2/oxygen-ui"; +import { Box, Button, Grid, Typography } from "@wso2/oxygen-ui";
157-157: TODO: Error component for chart loading failures.This TODO indicates incomplete error handling for charts. Tracking this for follow-up.
Would you like me to open an issue to track implementing a dedicated error component for chart loading failures?
117-155: Consider extracting stat value resolution to a utility function.The switch statement for mapping stat IDs to values is verbose. A mapping object or utility function would improve maintainability.
♻️ Suggested refactor
const getStatValue = (statId: string, casesStats: ProjectCasesStats | undefined): string | number => { if (!casesStats) return 0; const valueMap: Record<string, string | number> = { totalCases: casesStats.totalCases, openCases: casesStats.openCases, resolvedCases: casesStats.resolvedCases.total, avgResponseTime: `${casesStats.averageResponseTime}h`, }; return valueMap[statId] ?? 0; };apps/customer-portal/webapp/src/components/dashboard/casesTable/CasesList.tsx (2)
85-89: Fragile date/time parsing.Splitting on space assumes a specific date format (e.g.,
"2024-01-15 10:30:00"). If the format varies or includes timezone info, this will break. Consider using a date parsing library or utility function.♻️ Proposed improvement
// Utility function for safer date parsing const parseDateTime = (dateString: string) => { const date = new Date(dateString); return { date: date.toLocaleDateString(), time: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), }; };
114-118: TODO: Contact column placeholder.The Contact column currently shows "TODO". This should be tracked for completion.
Would you like me to open an issue to track implementing the Contact column?
apps/customer-portal/webapp/src/components/projectHub/projectCard/ProjectCard.tsx (1)
71-76: Mock values generated even when props are provided.The mock functions are called unconditionally via
useMemo, even whenstatus,openCases, andactiveChatsare provided as props. While the empty dependency arrays prevent re-execution, the initial generation is still wasteful.♻️ Proposed lazy initialization
- const mockStatus = useMemo(() => getMockStatus(), []); - const mockOpenCases = useMemo(() => getMockOpenCases(), []); - const mockActiveChats = useMemo(() => getMockActiveChats(), []); - const resolvedStatus = status ?? mockStatus; - const resolvedOpenCases = openCases ?? mockOpenCases; - const resolvedActiveChats = activeChats ?? mockActiveChats; + const resolvedStatus = useMemo( + () => status ?? getMockStatus(), + [status] + ); + const resolvedOpenCases = useMemo( + () => openCases ?? getMockOpenCases(), + [openCases] + ); + const resolvedActiveChats = useMemo( + () => activeChats ?? getMockActiveChats(), + [activeChats] + );Note: This changes behavior slightly - mock values would regenerate if props change from defined to undefined. If stable mock values are required across re-renders, the current approach is acceptable for demo purposes.
apps/customer-portal/webapp/src/models/responses.ts (1)
96-102: Consider more descriptive property names forcasesTrend.The properties
TypeA,TypeB,TypeC,TypeDare generic. If these represent specific case categories (e.g.,incidents,questions,bugs,features), consider using descriptive names for better code readability and maintainability.apps/customer-portal/webapp/src/models/mockData.ts (1)
90-96: Nitpick: Consider lowercase email for consistency.Email addresses are conventionally lowercase. While not functionally incorrect,
john@example.comwould be more realistic.apps/customer-portal/webapp/src/models/mockFunctions.ts (1)
163-180: Verify trend color semantics for decreasing metrics.The trend colors appear semantically inverted:
openCasesdecreasing withcolor: "error"— typically fewer open cases is positive ("success")avgResponseTimedecreasing withcolor: "error"— typically faster response times is positive ("success")If this is intentional design (e.g., highlighting any change as noteworthy), please disregard. Otherwise, consider:
♻️ Proposed fix for semantic colors
openCases: { value: 42, - trend: { value: "5%", direction: "down", color: "error" }, + trend: { value: "5%", direction: "down", color: "success" }, }, resolvedCases: { value: 114, trend: { value: "8%", direction: "up", color: "success" }, }, avgResponseTime: { value: "4.5h", - trend: { value: "0.5h", direction: "down", color: "error" }, + trend: { value: "0.5h", direction: "down", color: "success" }, },
Introduce a new test suite for the useGetProjectDetails hook using Vitest and @testing-library/react. Adds a QueryClientProvider wrapper (no retries), mocks API_MOCK_DELAY to 0 and useLogger, and includes tests for a valid project ID (success path) and an invalid project ID (error path).
Introduce a new Vitest test file for the useGetProjectStat hook. The tests add a QueryClientProvider wrapper, mock API delay and useLogger, and verify both a successful fetch for a valid project ID and an error case for an invalid ID. Includes license header and uses existing mockProjects test data.
Introduce a new custom hook (apps/customer-portal/webapp/src/api/useGetProjectDetails.ts) that fetches project details by ID using @tanstack/react-query. The hook uses mockProjectDetails and simulates network latency with API_MOCK_DELAY, logs actions via useLogger, and throws an error if the project ID is not found. Query is keyed by ApiQueryKeys.PROJECT_DETAILS, enabled only when projectId is provided, and sets a 5-minute staleTime.
Add a new Vitest test suite at apps/customer-portal/webapp/src/utils/__tests__/projectStats.test.ts covering formatProjectDate, getSLAStatusColor, getSupportTierColor, getProjectTypeColor, getSystemHealthColor, getSubscriptionStatus, and getSubscriptionColor. Tests verify string casing, empty/null handling, ISO/full-date parsing, and subscription status edge cases by using fake timers (fixed system time). File includes project license header.
Add a new projectStats utility module providing helpers for the customer-portal UI: formatProjectDate (formats dates to "MMM D, YYYY"), multiple get*Color functions that map SLA/support tier/project type/system health/subscription statuses to chip color tokens (with unknown values defaulting to "default"), and getSubscriptionStatus which classifies an end date as Active/Expiring Soon/Expired (30-day threshold). These helpers centralize presentation logic for project stats and chip styling.
Replace placeholder overview with real project overview UI and data fetching. Add imports for Grid, useOutletContext, useEffect, API hooks, logger and loader context. Fetch project details and stats (useGetProjectDetails, useGetProjectStat), show/hide a global loader while loading, and log errors. Render ProjectInformationCard, ProjectStatisticsCard, ContactInfoCard, and RecentActivityCard in a responsive Grid and handle invalid projectId. Simplify outer layout (use fragment) and pass sidebarCollapsed to statistics card for responsive behavior.
Fetch project details and integrate them into CreateCasePage: add useGetProjectDetails, show loader while project data loads, and set the project name from projectDetails without overwriting a user-selected project or when projectId is present. Include isProjectLoading in loader and isLoading props, and add an effect to update the local project state. Misc UI/cleanup: remove an unused Divider import/usage and extra padding, change the submit button color to primary, remove unused LinearProgress import in DashboardPage, and export the Message interface in NoveraChatPage.
Add mocks and adjustments to CreateCasePage unit tests: import and mock the useGetProjectDetails hook (providing sample project data), add IconButton and PencilLine mocks for @wso2/oxygen-ui and icon package, and update a test to simulate a failing project-details return. These changes stabilize tests by providing predictable project data and required UI component mocks.
Introduce a new React Query hook useGetProjectStat to fetch project statistics. The hook uses mock data (getMockProjectStats, mockProjects) with a simulated network delay, validates the provided projectId and throws an error if not found, and logs operations via useLogger. Query is keyed by ApiQueryKeys.PROJECT_STATS and is enabled only when projectId is truthy, with a 5-minute staleTime. File includes Apache-2.0 license header.
Change the ActiveFilters chip color from primary to warning when a filter is active. Improve selection detection by matching appliedFilters against both the display label and the option value. Remove the now-unused projectId prop from CasesTableHeader and update the test to reflect that removal.
Extend response models to support richer project and user data. Adds ProjectDetails (including subscription start/end and supportTier), makes UserProfile.status optional, adds optional projects list to CaseSearchResponse, and introduces ProjectStatsResponse with projectStats and recentActivity fields to support dashboard/metrics use cases. Changes live in apps/customer-portal/webapp/src/models/responses.ts.
Add ProjectName.test.tsx to verify ProjectName behavior using vitest and @testing-library/react. The file mocks @wso2/oxygen-ui components and contains tests for non-loading state (renders label, project name and key) and loading state (renders skeletons and hides name/key). Located under projectInformation/__tests__.
Rashmika998
left a comment
There was a problem hiding this comment.
LGTM
directory refactor and path changes will be tracked under #89
|
@dileepapeiris Are we good to merge this? |
Import SubscriptionStatus from models and switch getSubscriptionStatus to return the enum instead of string literals. Update getSubscriptionColor to accept SubscriptionStatus | string and compare against enum values (normalized with toLowerCase()) for consistent color mapping. These changes improve type safety and centralize status values while preserving existing behavior.
Introduce a SubscriptionStatus constant and corresponding TypeScript union type in apps/customer-portal/webapp/src/models/responses.ts. The new exported const provides three statuses (Expired, "Expiring Soon", Active) and the exported type alias derives a union of those values to improve type safety for subscription status fields in API responses.
Replace string literal checks for "Expired" with SubscriptionStatus.Expired in SubscriptionDetails component. Adds import from @/models/responses and updates two comparisons (progress calculation and label text) to improve type-safety and avoid magic strings.
Replace hardcoded 'Active'/'Expired' strings in SubscriptionDetails tests with the SubscriptionStatus enum. Adds import for SubscriptionStatus and updates mocked getSubscriptionStatus/getSubscriptionColor implementations and assertions to use SubscriptionStatus.Active/SubscriptionStatus.Expired. No behavioral change — just aligns tests with the model enum.
@shayanmalinda Yes |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@apps/customer-portal/webapp/src/components/projectDetails/projectOverview/projectInformation/__tests__/SubscriptionDetails.test.tsx`:
- Around line 74-77: Update the stale inline comment in
SubscriptionDetails.test.tsx that says "Active -> 75" to match the mock's actual
return value of 50 (or remove the comment entirely); ensure the test assertions
around the progress element (expect(progress).toHaveAttribute("data-value",
"50") and expect(progress).toHaveAttribute("data-color", "success")) remain
unchanged and reflect the mock for active subscriptions so the comment
accurately documents the mock behavior.
🧹 Nitpick comments (1)
apps/customer-portal/webapp/src/utils/projectStats.ts (1)
132-156: Consider injecting "today" for testability.
getSubscriptionStatuscreatesnew Date()internally, making it non-deterministic and harder to unit test without mockingDate. Consider accepting an optionalreferenceDateparameter.♻️ Suggested refactor for testability
export const getSubscriptionStatus = ( endDateString: string, + referenceDate: Date = new Date(), ): SubscriptionStatus => { if (!endDateString) { return SubscriptionStatus.Active; } - const today = new Date(); + const today = referenceDate; const endDate = new Date(endDateString);
Delete a misleading comment that referenced a default mock value of 75 in the SubscriptionDetails test. The test actually asserts a data-value of 50, so removing the stale comment reduces confusion.
Replace ad-hoc enums and string literals with centralized constants from projectDetailsConstants. Update imports and refactor SLA, support tier, project type, system health, and subscription status checks (including getSubscriptionStatus and color mapping helpers) to use these constants for consistent comparisons and return values.
Delete the SubscriptionStatus constant and its corresponding TypeScript type from apps/customer-portal/webapp/src/models/responses.ts. This cleans up the responses model by removing the no-longer-needed subscription status definition (likely moved or unused).
Import PROJECT_TYPE, SUPPORT_TIER, and CASE_STATUS and replace hard-coded string literals in mockProjectDetails and mockCases with these constants in apps/customer-portal/webapp/src/models/mockData.ts. This reduces magic strings, improves consistency with project constants, and avoids typos; no functional behavior changes.
Introduce centralized constants and corresponding TypeScript union types in projectDetailsConstants.tsx for subscription status, support tier, project type, system health, SLA status, case priority, and case status. Each constant uses `as const` so the derived types are literal unions, providing stricter, consistent typing for project detail logic and UI components.
Replace SubscriptionStatus import with SUBSCRIPTION_STATUS from projectDetailsConstants in SubscriptionDetails.tsx. Update conditional checks to use SUBSCRIPTION_STATUS.EXPIRED when computing progress and rendering the expiration label, centralizing subscription status usage.
Replace direct SubscriptionStatus import with SUBSCRIPTION_STATUS from projectDetailsConstants and update test mocks and assertions accordingly. Adjusted mocked @wso2/oxygen-ui to include a colors object, and updated utility mocks (getSubscriptionStatus, getSubscriptionColor) and expected text values to use the new constant values so tests reflect the refactor to centralized status constants.
Drop the unused `collapsed` parameter from Header's function signature and remove the `sx` prop from the mocked Card in ProjectStatisticsCard tests. This cleans up unused/unused-typed props and aligns the test mock with the Card API, resolving lint/TS warnings without changing runtime behavior.
aaf46f3
into
wso2-open-operations:customer-portal-milestone-1
Purpose
This PR introduces the Project Details page, providing a comprehensive 360-degree view of a specific project. It establishes a tabbed navigation structure (Overview, Deployments, Time Tracking) and implements a data-rich Overview tab composed of modular cards for project metadata, health statistics, contact information, and recent activity.
Screen Recordings and Screenshots
Screen.Recording.2026-02-03.at.17.08.24.mp4
Project Overview Page
** Light Mode**

Dark Mode

###Loading Modal
** Light Mode**

Dark Mode

Goals
TabBarcomponent for consistent sub-navigation across different project modules.Approach
ProjectDetailspage as a layout orchestrator using a sharedTabBarcomponent to switch between functional views.useGetProjectDetails,useGetProjectStat) to decouple UI components from data fetching logic.projectDetailsConstants.tsxfor easy configuration.User stories
Walkthrough
This PR builds the Project Details ecosystem: the main page container, a reusable sub-navigation tab bar, and four major card components (Information, Stats, Contacts, and Activity) with their respective sub-components. It also includes the necessary API hooks and constants to power the view.
Changes
ProjectDetails.tsxTabBar.tsxProjectInformationCard.tsx,ProjectHeader.tsx,ProjectMetadata.tsx, etc.ProjectStatisticsCard.tsxContactInfoCard.tsx,ContactRow.tsxRecentActivityCard.tsxuseGetProjectDetails.ts,useGetProjectStat.tsprojectDetailsConstants.tsxAutomation tests
useGetProjectDetailsanduseGetProjectStatfor successful data resolution, empty states, and API errors.TabBarcorrectly switches between component views within theProjectDetailspage.Summary by CodeRabbit
New Features
Tests