[Customer Portal][FE][Web] : Add Users tab to Project Details page#151
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Project Users feature: mock project user model and dataset, a React Query hook to fetch users, a two-step Add Project User dialog, a Project Users tab with table/add/delete UI and status-colored chips, and integrates the tab into Project Details. (49 words) Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant UI as ProjectUsersTab
participant Hook as useGetProjectUsers
participant State as LocalState
participant Dialog as AddProjectUserDialog
User->>UI: open "Users" tab
UI->>Hook: fetch([ApiQueryKeys.PROJECT_USERS, projectId])
Hook-->>UI: return mockProjectUsers
UI->>State: initialize localUsers
UI-->>User: render users table
User->>UI: click "Add Project User"
UI->>Dialog: open()
User->>Dialog: enter email → Next
Dialog->>Dialog: validate email
User->>Dialog: enter first/last name → Done
Dialog-->>UI: onSubmit(email, firstName, lastName)
UI->>State: append new user (status: "Invited")
UI-->>User: update table
Dialog-->>UI: close()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 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 |
apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx`:
- Around line 66-70: The render body currently calls setLocalUsers and
setInitialized when fetchedUsers exist, which is an anti-pattern; move that sync
into a useEffect that depends on fetchedUsers and initialized so state updates
happen after render. Import useEffect if missing, and inside a useEffect(() => {
if (fetchedUsers && !initialized) { setLocalUsers(fetchedUsers);
setInitialized(true); } }, [fetchedUsers, initialized]) perform the update;
reference the existing identifiers fetchedUsers, initialized, setLocalUsers, and
setInitialized to locate where to change.
- Around line 171-178: The Chip in ProjectUsersTab is using an invalid sx prop
sx={{ font: "caption" }} which does nothing; change that to sx={{ typography:
"caption" }} on the Chip component (the one rendering label={user.status}) so
MUI applies the caption typography variant; search for the Chip in
ProjectUsersTab that uses getUserStatusColor(user.status) to locate and update
the sx prop accordingly.
🧹 Nitpick comments (2)
apps/customer-portal/webapp/src/constants/projectDetailsConstants.tsx (1)
203-207: Inconsistent casing convention forPROJECT_USER_TYPESvalues.All other status constants in this file use title-case values (e.g.,
SUBSCRIPTION_STATUS.EXPIRED = "Expired",CASE_STATUS.OPEN = "Open",SLA_STATUS.GOOD = "All Good"), butPROJECT_USER_TYPESuses lowercase ("invited","registered"). TheMockProjectUser.statustype also uses title-case ("Invited" | "Registered"). WhilegetUserStatusColornormalizes viatoLowerCase()so it works at runtime, aligning casing with the rest of the constants improves consistency.Suggested fix
export const PROJECT_USER_TYPES = { - INVITED: "invited", - REGISTERED: "registered", + INVITED: "Invited", + REGISTERED: "Registered", } as const;apps/customer-portal/webapp/src/models/mockData.ts (1)
1779-1808: Mock user names are not capitalized.The
firstNameandlastNamevalues are all lowercase (e.g.,"nisal","perera"), which will display as-is in the table. Consider capitalizing them for a polished demo appearance.
apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx
Outdated
Show resolved
Hide resolved
apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
This pull request introduces a Users tab to the project details page, enabling display and management of project users with mock data support. The feature includes UI components for listing users in a table, adding new users through a multi-step dialog, and temporary local state management for add/delete operations until backend APIs are available.
Changes:
- Added a Users tab with table display of project users including status chips and delete functionality
- Implemented a multi-step dialog for adding users with email validation
- Added mock data support with
useGetProjectUsershook andPROJECT_USER_TYPESconstants
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/customer-portal/webapp/src/pages/ProjectDetails.tsx | Integrated ProjectUsersTab component into the project details page routing |
| apps/customer-portal/webapp/src/constants/projectDetailsConstants.tsx | Added Users tab configuration and PROJECT_USER_TYPES constants |
| apps/customer-portal/webapp/src/utils/projectStats.ts | Added getUserStatusColor utility for status chip styling |
| apps/customer-portal/webapp/src/models/mockData.ts | Added MockProjectUser interface and mockProjectUsers array |
| apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx | Implemented main users tab component with table, local state management, and user operations |
| apps/customer-portal/webapp/src/components/project-details/users/AddProjectUserDialog.tsx | Implemented multi-step dialog for adding users with email validation |
| apps/customer-portal/webapp/src/api/useGetProjectUsers.ts | Added React Query hook for fetching project users with mock support |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
apps/customer-portal/webapp/src/constants/projectDetailsConstants.tsx
Outdated
Show resolved
Hide resolved
| export default function ProjectUsersTab({ | ||
| projectId, | ||
| }: ProjectUsersTabProps): JSX.Element { | ||
| const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false); | ||
| const [localUsers, setLocalUsers] = useState<MockProjectUser[]>([]); | ||
| const [initialized, setInitialized] = useState<boolean>(false); | ||
|
|
||
| const { | ||
| data: fetchedUsers, | ||
| isFetching, | ||
| error, | ||
| } = useGetProjectUsers(projectId); | ||
|
|
||
| // Sync fetched data to local state for add/delete operations | ||
| if (fetchedUsers && !initialized) { | ||
| setLocalUsers(fetchedUsers); | ||
| setInitialized(true); | ||
| } | ||
|
|
||
| const users = initialized ? localUsers : (fetchedUsers ?? []); | ||
|
|
||
| const handleDeleteUser = (userId: string): void => { | ||
| // TODO: Replace with actual API call to delete user | ||
| setLocalUsers((prevUsers) => | ||
| prevUsers.filter((user) => user.id !== userId), | ||
| ); | ||
| }; | ||
|
|
||
| const handleAddUser = (newUser: { | ||
| email: string; | ||
| firstName: string; | ||
| lastName: string; | ||
| }): void => { | ||
| // TODO: Replace with API call to add user | ||
| const user: MockProjectUser = { | ||
| id: Date.now().toString(), | ||
| firstName: newUser.firstName || "--", | ||
| lastName: newUser.lastName || "--", | ||
| email: newUser.email, | ||
| status: "Invited", | ||
| }; | ||
| setLocalUsers((prevUsers) => [...prevUsers, user]); | ||
| setIsDialogOpen(false); | ||
| }; | ||
|
|
||
| const renderTableSkeleton = (): JSX.Element => ( | ||
| <> | ||
| {[1, 2, 3].map((row) => ( | ||
| <TableRow key={row}> | ||
| {[1, 2, 3, 4, 5].map((col) => ( | ||
| <TableCell key={col}> | ||
| <Skeleton variant="text" width="80%" /> | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| ))} | ||
| </> | ||
| ); | ||
|
|
||
| return ( | ||
| <Box> | ||
| <Box | ||
| sx={{ | ||
| display: "flex", | ||
| justifyContent: "space-between", | ||
| alignItems: "center", | ||
| mb: 2, | ||
| }} | ||
| > | ||
| <Typography variant="h6">Project Users</Typography> | ||
| <Button | ||
| variant="contained" | ||
| color="primary" | ||
| startIcon={<Plus size={16} />} | ||
| onClick={() => setIsDialogOpen(true)} | ||
| > | ||
| Add Project User | ||
| </Button> | ||
| </Box> | ||
|
|
||
| <TableContainer component={Paper} variant="outlined"> | ||
| <Table> | ||
| <TableHead> | ||
| <TableRow> | ||
| <TableCell>First Name</TableCell> | ||
| <TableCell>Last Name</TableCell> | ||
| <TableCell>Email</TableCell> | ||
| <TableCell>Status</TableCell> | ||
| <TableCell align="right">Action</TableCell> | ||
| </TableRow> | ||
| </TableHead> | ||
| <TableBody> | ||
| {isFetching ? ( | ||
| renderTableSkeleton() | ||
| ) : error ? ( | ||
| <TableRow> | ||
| <TableCell colSpan={5} align="center"> | ||
| <ErrorIndicator entityName="project users" /> | ||
| </TableCell> | ||
| </TableRow> | ||
| ) : users.length === 0 ? ( | ||
| <TableRow> | ||
| <TableCell colSpan={5} align="center"> | ||
| <Typography | ||
| variant="body2" | ||
| color="text.secondary" | ||
| sx={{ py: 2 }} | ||
| > | ||
| No users found for this project. | ||
| </Typography> | ||
| </TableCell> | ||
| </TableRow> | ||
| ) : ( | ||
| users.map((user) => ( | ||
| <TableRow key={user.id}> | ||
| <TableCell>{user.firstName}</TableCell> | ||
| <TableCell>{user.lastName}</TableCell> | ||
| <TableCell>{user.email}</TableCell> | ||
| <TableCell> | ||
| <Chip | ||
| label={user.status} | ||
| size="small" | ||
| variant="outlined" | ||
| color={getUserStatusColor(user.status)} | ||
| sx={{ font: "caption" }} | ||
| /> | ||
| </TableCell> | ||
| <TableCell align="right"> | ||
| <Tooltip title="Remove user"> | ||
| <IconButton | ||
| color="error" | ||
| size="small" | ||
| onClick={() => handleDeleteUser(user.id)} | ||
| > | ||
| <Trash size={18} /> | ||
| </IconButton> | ||
| </Tooltip> | ||
| </TableCell> | ||
| </TableRow> | ||
| )) | ||
| )} | ||
| </TableBody> | ||
| </Table> | ||
| </TableContainer> | ||
|
|
||
| {/* Add Project User Dialog */} | ||
| <AddProjectUserDialog | ||
| open={isDialogOpen} | ||
| onClose={() => setIsDialogOpen(false)} | ||
| onSubmit={handleAddUser} | ||
| /> | ||
| </Box> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Test coverage is missing for the new ProjectUsersTab component. The codebase has established test coverage for similar components (e.g., TimeTrackingStatCards has tests at apps/customer-portal/webapp/src/components/project-details/time-tracking/tests/TimeTrackingStatCards.test.tsx). Consider adding tests for ProjectUsersTab to cover user listing, adding users, deleting users, loading states, and error states.
There was a problem hiding this comment.
FYI : Reviewers those tests will address later
| export default function AddProjectUserDialog({ | ||
| open, | ||
| onClose, | ||
| onSubmit, | ||
| }: AddProjectUserDialogProps): JSX.Element { | ||
| const [step, setStep] = useState<1 | 2>(1); | ||
| const [email, setEmail] = useState<string>(""); | ||
| const [firstName, setFirstName] = useState<string>(""); | ||
| const [lastName, setLastName] = useState<string>(""); | ||
| const [emailError, setEmailError] = useState<string>(""); | ||
|
|
||
| const resetForm = (): void => { | ||
| setStep(1); | ||
| setEmail(""); | ||
| setFirstName(""); | ||
| setLastName(""); | ||
| setEmailError(""); | ||
| }; | ||
|
|
||
| const handleClose = (): void => { | ||
| resetForm(); | ||
| onClose(); | ||
| }; | ||
|
|
||
| const validateEmail = (value: string): boolean => { | ||
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | ||
| return emailRegex.test(value); | ||
| }; | ||
|
|
||
| const handleNext = (): void => { | ||
| if (!email.trim()) { | ||
| setEmailError("Email is required."); | ||
| return; | ||
| } | ||
| if (!validateEmail(email.trim())) { | ||
| setEmailError("Please enter a valid email address."); | ||
| return; | ||
| } | ||
| setEmailError(""); | ||
| setStep(2); | ||
| }; | ||
|
|
||
| const handleDone = (): void => { | ||
| onSubmit({ | ||
| email: email.trim(), | ||
| firstName: firstName.trim(), | ||
| lastName: lastName.trim(), | ||
| }); | ||
| resetForm(); | ||
| }; | ||
|
|
||
| return ( | ||
| <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> | ||
| <DialogTitle> | ||
| {step === 1 ? "Add Project User" : "User Details"} | ||
| </DialogTitle> | ||
| <DialogContent> | ||
| {step === 1 ? ( | ||
| <Box sx={{ pt: 1 }}> | ||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | ||
| Enter the email address of the user you want to add to this | ||
| project. | ||
| </Typography> | ||
| <TextField | ||
| autoFocus | ||
| label="Email Address" | ||
| type="email" | ||
| fullWidth | ||
| required | ||
| value={email} | ||
| onChange={(e) => { | ||
| setEmail(e.target.value); | ||
| if (emailError) setEmailError(""); | ||
| }} | ||
| error={!!emailError} | ||
| helperText={emailError} | ||
| onKeyDown={(e) => { | ||
| if (e.key === "Enter") handleNext(); | ||
| }} | ||
| /> | ||
| </Box> | ||
| ) : ( | ||
| <Box sx={{ pt: 1 }}> | ||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | ||
| Provide the name details for <strong>{email}</strong>. | ||
| </Typography> | ||
| <TextField | ||
| autoFocus | ||
| label="First Name" | ||
| fullWidth | ||
| value={firstName} | ||
| onChange={(e) => setFirstName(e.target.value)} | ||
| sx={{ mb: 2 }} | ||
| /> | ||
| <TextField | ||
| label="Last Name" | ||
| fullWidth | ||
| value={lastName} | ||
| onChange={(e) => setLastName(e.target.value)} | ||
| /> | ||
| </Box> | ||
| )} | ||
| </DialogContent> | ||
| <DialogActions sx={{ px: 3, pb: 2 }}> | ||
| {step === 1 ? ( | ||
| <> | ||
| <Button variant="text" color="secondary" onClick={handleClose}> | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| variant="contained" | ||
| color="primary" | ||
| onClick={handleNext} | ||
| disabled={!email.trim()} | ||
| > | ||
| Next | ||
| </Button> | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <Button variant="text" color="secondary" onClick={() => setStep(1)}> | ||
| Back | ||
| </Button> | ||
| <Button variant="contained" color="primary" onClick={handleDone}> | ||
| Done | ||
| </Button> | ||
| </> | ||
| )} | ||
| </DialogActions> | ||
| </Dialog> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Test coverage is missing for the new AddProjectUserDialog component. The codebase follows a pattern of testing UI components (as seen in other component tests). Consider adding tests to cover the multi-step dialog flow, email validation, form submission, form reset, and the back/cancel functionality.
There was a problem hiding this comment.
FYI : Reviewers those tests will address later
| export default function useGetProjectUsers( | ||
| projectId: string, | ||
| ): UseQueryResult<MockProjectUser[]> { | ||
| const { isMockEnabled } = useMockConfig(); | ||
|
|
||
| return useQuery<MockProjectUser[]>({ | ||
| queryKey: ["projectUsers", projectId], | ||
| queryFn: async () => { | ||
| if (isMockEnabled) { | ||
| return mockProjectUsers; | ||
| } | ||
|
|
||
| // TODO: Implement API call when endpoint is available | ||
| return []; | ||
| }, | ||
| enabled: !!projectId, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Test coverage is missing for the new useGetProjectUsers hook. The codebase has established test coverage for similar API hooks (e.g., useGetProjectDeployments has tests). Consider adding tests to verify mock data handling, API calls when mock is disabled, query key structure, and the enabled condition.
There was a problem hiding this comment.
FYI : Reviewers those tests will address later
apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx
Outdated
Show resolved
Hide resolved
| */ | ||
| export const getUserStatusColor = ( | ||
| status: string, | ||
| ): "primary" | "info" | "default" | "success" | "warning" | "error" => { |
There was a problem hiding this comment.
These should be moved to a custom type. @dileepapeiris do that when you find the time to.
There was a problem hiding this comment.
I will address this. Thank you.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx`:
- Around line 147-148: The component currently shows the skeleton based on React
Query's isFetching which also becomes true on background refetches and hides
local optimistic state; change the conditional to use isLoading instead so the
skeleton only appears on the initial load. In ProjectUsersTab (the render block
that calls renderTableSkeleton), replace checks using isFetching with isLoading
(keeping the existing initialized guard intact) so the skeleton is shown only
when there is no cached data; ensure any references to isFetching in that render
path are updated to isLoading and that renderTableSkeleton remains called only
for the initial load.
🧹 Nitpick comments (1)
apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx (1)
83-98: No duplicate-email guard before adding a user.
handleAddUserappends the new user unconditionally. If the same email is submitted twice, the table will show duplicate rows. A quick check againstlocalUserswould improve the UX even while the backend is mocked.Suggested guard
const handleAddUser = (newUser: { email: string; firstName: string; lastName: string; }): void => { + const alreadyExists = localUsers.some( + (u) => u.email.toLowerCase() === newUser.email.trim().toLowerCase(), + ); + if (alreadyExists) { + // Optionally show a toast/snackbar here + return; + } // TODO: Replace with API call to add user const user: MockProjectUser = {
apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx
Show resolved
Hide resolved
apps/customer-portal/webapp/src/components/project-details/users/ProjectUsersTab.tsx
Show resolved
Hide resolved
apps/customer-portal/webapp/src/components/project-details/users/AddProjectUserDialog.tsx
Show resolved
Hide resolved
a4172b1
into
wso2-open-operations:customer-portal-milestone-1
This pull request introduces the "Users" tab to the project details page, enabling the display and management of project users. It includes new UI components for listing users, adding new users via a dialog, and mock data support to facilitate development until backend APIs are available. The changes also add utility functions and constants to support user status handling.
Feature: Project Users Management
ProjectUsersTabcomponent to fetch, display, and locally manage the list of users, including skeleton loading, error handling, and user deletion (mocked).AddProjectUserDialogcomponent, providing a multi-step dialog for entering user email and details, with validation and form reset logic.API and Mock Data Support
useGetProjectUsershook to fetch project users, supporting mock data when mock mode is enabled.mockData.ts, used for development and testing.User Status Utilities and Constants
PROJECT_USER_TYPESconstants for user status types, and implemented thegetUserStatusColorutility for consistent status chip coloring in the UI. [1] [2] [3]Summary by CodeRabbit