[PT2025BMHW-119][SuperAdminUI,SharedUI] feat: Implement Global Users Management Page#147
[PT2025BMHW-119][SuperAdminUI,SharedUI] feat: Implement Global Users Management Page#147
Conversation
Implements edit mode for the multi-step tenant form. - Reuses CreateTenantDialog for editing existing tenants - Pre-populates form with existing tenant data - Adds visual indicators for modified fields (orange dot) - Implements concurrent edit detection and warning system - Handles immutable fields (domain) in edit mode - Adds support for 'create' and 'edit' modes in TenantFormDialog Jira: PT2025BMHW-112
* [PT2025BMHW-113][SharedUI,SuperAdminUI] feat: Implement Delete Tenant Confirmation & Refactor Tenants Page - Create DeleteTenantDialog component with premium UI - Add security check requiring tenant name confirmation - Display impact warnings (Active Users count) - Show irreversible action alert - Refactor TenantsPage: - Extract TenantsTable component - Extract TenantsFilters component - Simplify main page logic
…n Assignment & Management * [PT2025BMHW-118][SharedUI,SuperAdminUI,Backend] feat: Implement Tenant Admin Assignment & Management - Backend: Add GetUsers endpoint to UsersController for user search - SharedUI: Add useUsers hook and tenant admin assignment mutations - SuperAdminUI: Implement AssignAdminModal with user search - SuperAdminUI: Implement EditTenantModal for tenant details updates - SuperAdminUI: Add functionality to unassign tenant admins - SuperAdminUI: Integrate new modals into TenantDetailPage
…ment Page * [PT2025BMHW-119][SuperAdminUI,SharedUI] feat: Implement Global Users Management Page - Create new UsersPage in SuperAdmin - Implement UsersTable with sorting and pagination - Implement UsersFilters for search and status filtering - Integrate existing useUsers hook from shared package - Remove duplicate queries.ts file in shared/features/users
16a783a to
eda2ee1
Compare
|
@copilot fix conflicts with develop |
There was a problem hiding this comment.
Pull request overview
This PR implements a global users management page for the SuperAdmin application and refactors the existing tenants management components for better maintainability. The implementation adds server-side pagination, filtering, and sorting capabilities to the users list, along with placeholder action handlers for user management operations.
Changes:
- Added new Users management page with table, filters, and pagination in SuperAdmin UI
- Added backend API endpoint for fetching paginated user lists
- Refactored Tenants components by extracting TenantsTable, TenantsFilters, and dialog components
- Added new modals for tenant management (Edit, Delete, Assign Admin)
- Updated Vite configurations with package alias resolution
- Added mutation hooks for assigning/unassigning tenant admins
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| Infrastructure/NGOPlatform.SuperAdmin/NGOPlatform.SuperAdmin.Api/Controllers/UsersController.cs | Added GET endpoint for paginated users list using CQRS pattern |
| Infrastructure/NGOPlatform.AppHost/appsettings.Development.json | Changed mobile platform setting from "ios" to "no-mobile" |
| Frontend/packages/shared/src/lib/mocks/mock-users-large.ts | Added empty mock users file (has encoding issues) |
| Frontend/packages/shared/src/lib/mocks/mock-tenants-large.ts | Added mock data for 800 tenants for testing |
| Frontend/packages/shared/src/features/tenant-admins/mutations/index.ts | Added useAssignTenantAdmin and useUnassignTenantAdmin hooks with query invalidation |
| Frontend/apps/tenantadmin/vite.config.ts | Added package alias resolution and formatting improvements |
| Frontend/apps/superadmin/vite.config.ts | Added package alias resolution and formatting improvements |
| Frontend/apps/superadmin/src/components/users/UsersTable.tsx | New component for displaying paginated users table with sorting |
| Frontend/apps/superadmin/src/components/users/UsersPage.tsx | New main users management page with filters and pagination |
| Frontend/apps/superadmin/src/components/users/UsersFilters.tsx | New component for user search and status filtering |
| Frontend/apps/superadmin/src/components/tenants/TenantsTable.tsx | Extracted table component from TenantsPage for reusability |
| Frontend/apps/superadmin/src/components/tenants/TenantsPage.tsx | Refactored to use extracted components, reduced from 510 to 248 lines |
| Frontend/apps/superadmin/src/components/tenants/TenantsFilters.tsx | Extracted filters component for tenant search and status |
| Frontend/apps/superadmin/src/components/tenants/TenantFormDialog.tsx | Enhanced with edit mode, dirty field tracking, and concurrent edit detection |
| Frontend/apps/superadmin/src/components/tenants/TenantDetailPage.tsx | Added edit modal and admin assignment/removal functionality |
| Frontend/apps/superadmin/src/components/tenants/EditTenantModal.tsx | New modal for editing tenant details |
| Frontend/apps/superadmin/src/components/tenants/DeleteTenantDialog.tsx | New confirmation dialog for tenant deletion with safeguards |
| Frontend/apps/superadmin/src/components/tenants/AssignAdminModal.tsx | New modal for assigning existing users as tenant admins |
Comments suppressed due to low confidence (3)
Frontend/apps/superadmin/src/components/tenants/TenantFormDialog.tsx:58
- The TypeScript ignore comment on line 57 suggests there's a type mismatch with the useTenant hook. Instead of suppressing the type error, the issue should be fixed. Either the useTenant hook should be updated to support the optional parameters being passed, or this code should be refactored to match the expected signature.
Frontend/apps/superadmin/src/components/tenants/TenantFormDialog.tsx:196 - Concurrency check implementation issue: The concurrent edit detection compares
updatedAttimestamps for exact equality, which is fragile. Minor clock skew, serialization differences, or database precision differences could cause false positives. Consider using a version number/ETag approach, or allowing a small time tolerance, or implementing optimistic locking with a proper version field in the backend.
Frontend/apps/superadmin/src/components/tenants/TenantFormDialog.tsx:46 - Unused variable warning.
| import { features, type User } from '@ngo-platform/shared'; | ||
| import { useEffect, useState, type FC } from 'react'; | ||
| import { UsersFilters } from './UsersFilters'; | ||
| import { UsersTable, type UserSortField } from './UsersTable'; | ||
|
|
||
| const ITEMS_PER_PAGE = 10; | ||
|
|
||
| export const UsersPage: FC = () => { | ||
| return ( | ||
| <div className="p-8"> | ||
| <div className="mb-6"> | ||
| <h1 className="text-3xl font-bold text-neutral-900 dark:text-white mb-2">Users</h1> | ||
| <p className="text-neutral-600 dark:text-neutral-400">Manage platform users</p> | ||
| </div> | ||
| <Card> | ||
| <CardContent className="p-8 text-center"> | ||
| <Icon name="users" size={48} className="mx-auto text-neutral-400 mb-4" /> | ||
| <p className="text-lg text-neutral-600 dark:text-neutral-400">Users management coming soon...</p> | ||
| </CardContent> | ||
| </Card> | ||
| </div> | ||
| ); | ||
| // State | ||
| const [searchQuery, setSearchQuery] = useState(''); | ||
| const [statusFilter, setStatusFilter] = useState<'All' | 'Active' | 'Inactive'>('All'); | ||
| const [sortField, setSortField] = useState<UserSortField>('createdAt'); | ||
| const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); | ||
| const [currentPage, setCurrentPage] = useState(1); | ||
|
|
||
| // Queries | ||
| // Convert status filter to boolean or undefined | ||
| const isActive = statusFilter === 'All' ? undefined : statusFilter === 'Active'; | ||
|
|
||
| const { data: usersResponse, isLoading, error, refetch } = features.useUsers({ | ||
| page: currentPage, | ||
| pageSize: ITEMS_PER_PAGE, | ||
| search: searchQuery || undefined, | ||
| isActive: isActive, | ||
| sortBy: sortField, | ||
| sortOrder: sortOrder, | ||
| }); | ||
|
|
||
| const users: User[] = usersResponse?.items ?? []; | ||
| const totalPages = usersResponse?.totalPages ?? 1; | ||
| const totalCount = usersResponse?.totalCount ?? 0; | ||
|
|
||
| // Reset to page 1 when filters change | ||
| useEffect(() => { | ||
| setCurrentPage(1); | ||
| }, [searchQuery, statusFilter, sortField, sortOrder]); | ||
|
|
||
| // Handlers | ||
| const handleSort = (field: UserSortField) => { | ||
| if (sortField === field) { | ||
| setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc')); | ||
| } else { | ||
| setSortField(field); | ||
| setSortOrder('asc'); | ||
| } | ||
| }; | ||
|
|
||
| const handleView = (user: User) => { | ||
| // Navigate to user details or open modal | ||
| // For now, simple console log or alert as per plan | ||
| console.log('View user', user); | ||
| // navigate(`/users/${user.id}`); // If we had a detail page | ||
| }; | ||
|
|
||
| const handleDeactivate = async (user: User) => { | ||
| if (confirm(`Are you sure you want to deactivate ${user.fullName}?`)) { | ||
| try { | ||
| // await deactivateUser.mutateAsync(user.id); | ||
| console.log('Deactivating user', user.id); | ||
| // refetch(); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const handleActivate = async (user: User) => { | ||
| if (confirm(`Are you sure you want to activate ${user.fullName}?`)) { | ||
| console.log('Activating user', user.id); | ||
| // await activateUser.mutateAsync(user.id); | ||
| // refetch(); | ||
| } | ||
| }; | ||
|
|
||
| const handleResetPassword = async (user: User) => { | ||
| if (confirm(`Send password reset email to ${user.email}?`)) { | ||
| console.log('Resetting password for', user.id); | ||
| } | ||
| }; | ||
|
|
||
| const goToPage = (page: number) => { | ||
| if (page >= 1 && page <= totalPages) { | ||
| setCurrentPage(page); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="p-6 lg:p-8"> | ||
| {/* Header */} | ||
| <div className="mb-6 flex items-center justify-between"> | ||
| <div> | ||
| <h1 className="text-3xl font-bold text-neutral-900 dark:text-white mb-2">Users</h1> | ||
| <p className="text-neutral-600 dark:text-neutral-400">Manage all users across the platform</p> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Filters */} | ||
| <UsersFilters | ||
| searchQuery={searchQuery} | ||
| onSearchChange={setSearchQuery} | ||
| statusFilter={statusFilter} | ||
| onStatusChange={setStatusFilter} | ||
| onClearAll={() => { | ||
| setSearchQuery(''); | ||
| setStatusFilter('All'); | ||
| }} | ||
| /> | ||
|
|
||
| {/* Table */} | ||
| <UsersTable | ||
| users={users} | ||
| isLoading={isLoading} | ||
| error={error instanceof Error ? error : null} | ||
| totalCount={totalCount} | ||
| sortField={sortField} | ||
| sortOrder={sortOrder} | ||
| onSort={handleSort} | ||
| onView={handleView} | ||
| onDeactivate={handleDeactivate} | ||
| onActivate={handleActivate} | ||
| onResetPassword={handleResetPassword} | ||
| /> | ||
|
|
||
| {/* Pagination */} | ||
| {(usersResponse?.totalCount ?? 0) > 0 && ( | ||
| <div className="mt-4 flex items-center justify-center gap-2"> | ||
| <button | ||
| onClick={() => goToPage(currentPage - 1)} | ||
| disabled={currentPage === 1} | ||
| className="px-3 py-1 rounded border border-neutral-300 dark:border-neutral-600 disabled:opacity-50 hover:bg-neutral-50 dark:hover:bg-neutral-800" | ||
| > | ||
| Previous | ||
| </button> | ||
| <span className="text-sm text-neutral-600 dark:text-neutral-400"> | ||
| Page {currentPage} of {totalPages} | ||
| </span> | ||
| <button | ||
| onClick={() => goToPage(currentPage + 1)} | ||
| disabled={currentPage === totalPages} | ||
| className="px-3 py-1 rounded border border-neutral-300 dark:border-neutral-600 disabled:opacity-50 hover:bg-neutral-50 dark:hover:bg-neutral-800" | ||
| > | ||
| Next | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Missing test coverage for the new users management page. The PR adds UsersPage, UsersTable, and UsersFilters components but doesn't include any corresponding test files. Based on the repository's testing conventions (xUnit for backend, Jest for frontend), tests should be added for these new components, especially for the filtering, sorting, and pagination logic.
| const handleView = (user: User) => { | ||
| // Navigate to user details or open modal | ||
| // For now, simple console log or alert as per plan | ||
| console.log('View user', user); | ||
| // navigate(`/users/${user.id}`); // If we had a detail page | ||
| }; | ||
|
|
||
| const handleDeactivate = async (user: User) => { | ||
| if (confirm(`Are you sure you want to deactivate ${user.fullName}?`)) { | ||
| try { | ||
| // await deactivateUser.mutateAsync(user.id); | ||
| console.log('Deactivating user', user.id); | ||
| // refetch(); | ||
| } catch (e) { | ||
| console.error(e); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const handleActivate = async (user: User) => { | ||
| if (confirm(`Are you sure you want to activate ${user.fullName}?`)) { | ||
| console.log('Activating user', user.id); | ||
| // await activateUser.mutateAsync(user.id); | ||
| // refetch(); | ||
| } | ||
| }; | ||
|
|
||
| const handleResetPassword = async (user: User) => { | ||
| if (confirm(`Send password reset email to ${user.email}?`)) { | ||
| console.log('Resetting password for', user.id); |
There was a problem hiding this comment.
Placeholder action handlers: The handlers for view, deactivate, activate, and reset password are all placeholders that only log to console. While the PR description mentions "action placeholders", these should either be implemented or clearly marked as TODO with tracking tickets, rather than having silent console.log statements that won't be visible to users.
| const handleView = (user: User) => { | |
| // Navigate to user details or open modal | |
| // For now, simple console log or alert as per plan | |
| console.log('View user', user); | |
| // navigate(`/users/${user.id}`); // If we had a detail page | |
| }; | |
| const handleDeactivate = async (user: User) => { | |
| if (confirm(`Are you sure you want to deactivate ${user.fullName}?`)) { | |
| try { | |
| // await deactivateUser.mutateAsync(user.id); | |
| console.log('Deactivating user', user.id); | |
| // refetch(); | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| } | |
| }; | |
| const handleActivate = async (user: User) => { | |
| if (confirm(`Are you sure you want to activate ${user.fullName}?`)) { | |
| console.log('Activating user', user.id); | |
| // await activateUser.mutateAsync(user.id); | |
| // refetch(); | |
| } | |
| }; | |
| const handleResetPassword = async (user: User) => { | |
| if (confirm(`Send password reset email to ${user.email}?`)) { | |
| console.log('Resetting password for', user.id); | |
| // TODO: Implement user view navigation (e.g. navigate to user details page). | |
| const handleView = (user: User) => { | |
| // Placeholder implementation: inform user that this action is not yet available. | |
| alert(`Viewing details for "${user.fullName}" is not available yet.`); | |
| // Example future implementation: | |
| // navigate(`/users/${user.id}`); | |
| }; | |
| // TODO: Implement user deactivation (e.g. call deactivate user API and refetch list). | |
| const handleDeactivate = async (user: User) => { | |
| if (confirm(`Are you sure you want to deactivate ${user.fullName}?`)) { | |
| // Placeholder implementation: inform user that this action is not yet available. | |
| alert(`Deactivation for "${user.fullName}" is not available yet.`); | |
| } | |
| }; | |
| // TODO: Implement user activation (e.g. call activate user API and refetch list). | |
| const handleActivate = async (user: User) => { | |
| if (confirm(`Are you sure you want to activate ${user.fullName}?`)) { | |
| // Placeholder implementation: inform user that this action is not yet available. | |
| alert(`Activation for "${user.fullName}" is not available yet.`); | |
| } | |
| }; | |
| // TODO: Implement password reset (e.g. trigger reset email via API). | |
| const handleResetPassword = async (user: User) => { | |
| if (confirm(`Send password reset email to ${user.email}?`)) { | |
| // Placeholder implementation: inform user that this action is not yet available. | |
| alert(`Password reset for "${user.email}" is not available yet.`); |
| <th | ||
| className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider cursor-pointer hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors user-select-none" | ||
| // onClick={() => onSort('isActive')} // Hook might not support sort by status yet, but UI can show it |
There was a problem hiding this comment.
The Status column header has sorting UI hints (cursor-pointer, hover states) but the onClick handler is commented out. This creates a confusing user experience where the column appears sortable but doesn't respond to clicks. Either implement the sorting functionality or remove the sorting UI affordances from this column.
| <th | |
| className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider cursor-pointer hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors user-select-none" | |
| // onClick={() => onSort('isActive')} // Hook might not support sort by status yet, but UI can show it | |
| <th | |
| className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider" | |
| // onClick={() => onSort('isActive')} // Hook might not support sort by status yet |
| <div className="w-10 h-10 bg-secondary-100 dark:bg-secondary-900/30 text-secondary-600 dark:text-secondary-400 rounded-full flex items-center justify-center font-bold text-sm flex-shrink-0 border border-secondary-200 dark:border-secondary-800"> | ||
| {user.firstName?.charAt(0)}{user.lastName?.charAt(0)} | ||
| </div> |
There was a problem hiding this comment.
Potential null reference issue: The code uses user.firstName?.charAt(0) and user.lastName?.charAt(0) to create initials, but the backend UserListItemDto defines FirstName and LastName as non-nullable strings (with default value string.Empty). If either field is empty, charAt(0) will return an empty string, resulting in no initials being displayed. Consider adding a fallback, such as using the first character of email or a default icon.
| <thead className="bg-neutral-50 dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700"> | ||
| <tr> | ||
| <th | ||
| className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider cursor-pointer hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors user-select-none" | ||
| onClick={() => onSort('firstName')} // Simplification: sorting by name usually goes by first name or generic "name" | ||
| > | ||
| <div className="flex items-center gap-1 group"> | ||
| User | ||
| <span className="opacity-0 group-hover:opacity-100 transition-opacity"> | ||
| {getSortIcon('firstName')} | ||
| </span> | ||
| </div> | ||
| </th> | ||
| <th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider"> | ||
| Role | ||
| </th> | ||
| <th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider"> | ||
| Tenant | ||
| </th> | ||
| <th | ||
| className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider cursor-pointer hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors user-select-none" | ||
| // onClick={() => onSort('isActive')} // Hook might not support sort by status yet, but UI can show it | ||
| > | ||
| Status | ||
| </th> | ||
| <th | ||
| className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider cursor-pointer hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors user-select-none" | ||
| onClick={() => onSort('createdAt')} | ||
| > | ||
| <div className="flex items-center gap-1 group"> | ||
| Created | ||
| <span className="opacity-0 group-hover:opacity-100 transition-opacity"> | ||
| {getSortIcon('createdAt')} | ||
| </span> | ||
| </div> | ||
| </th> | ||
| <th className="px-6 py-3 text-right text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider"> | ||
| Actions | ||
| </th> | ||
| </tr> | ||
| </thead> |
There was a problem hiding this comment.
Accessibility issue: The sortable table headers lack proper ARIA attributes. Sortable columns should include aria-sort="ascending|descending|none" to indicate the current sort state to screen reader users. Additionally, the column headers should have role="columnheader" and the clickable area should include aria-label describing the sorting action.
| {/* Pagination */} | ||
| {(usersResponse?.totalCount ?? 0) > 0 && ( | ||
| <div className="mt-4 flex items-center justify-center gap-2"> | ||
| <button | ||
| onClick={() => goToPage(currentPage - 1)} | ||
| disabled={currentPage === 1} | ||
| className="px-3 py-1 rounded border border-neutral-300 dark:border-neutral-600 disabled:opacity-50 hover:bg-neutral-50 dark:hover:bg-neutral-800" | ||
| > | ||
| Previous | ||
| </button> | ||
| <span className="text-sm text-neutral-600 dark:text-neutral-400"> | ||
| Page {currentPage} of {totalPages} | ||
| </span> | ||
| <button | ||
| onClick={() => goToPage(currentPage + 1)} | ||
| disabled={currentPage === totalPages} | ||
| className="px-3 py-1 rounded border border-neutral-300 dark:border-neutral-600 disabled:opacity-50 hover:bg-neutral-50 dark:hover:bg-neutral-800" | ||
| > | ||
| Next | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
Accessibility issue: The pagination buttons lack proper ARIA labels. Buttons with only "Previous" and "Next" text should include aria-label attributes to provide context about what is being paginated (e.g., "Previous page of users", "Next page of users") for screen reader users.
| <Button variant="ghost" size="sm" onClick={() => onView(user)} title="View Details"> | ||
| <Icon name="eye" size={16} /> | ||
| </Button> | ||
| <Button variant="ghost" size="sm" onClick={() => onResetPassword(user)} title="Reset Password"> | ||
| <Icon name="key" size={16} /> | ||
| </Button> | ||
| {user.isActive ? ( | ||
| <Button variant="ghost" size="sm" className="text-error-600 hover:text-error-700 hover:bg-error-50 dark:hover:bg-error-900/20" onClick={() => onDeactivate(user)} title="Deactivate User"> | ||
| <Icon name="userX" size={16} /> | ||
| </Button> | ||
| ) : ( | ||
| <Button variant="ghost" size="sm" className="text-success-600 hover:text-success-700 hover:bg-success-50 dark:hover:bg-success-900/20" onClick={() => onActivate(user)} title="Activate User"> | ||
| <Icon name="checkCircle" size={16} /> |
There was a problem hiding this comment.
Accessibility issue: Action buttons only show icons without visible text labels. While they have title attributes for tooltips, screen reader users may not benefit from this. Consider adding visually-hidden text labels or using aria-label attributes to describe each action (e.g., aria-label="View user details for {user.fullName}").
| <Button variant="ghost" size="sm" onClick={() => onView(user)} title="View Details"> | |
| <Icon name="eye" size={16} /> | |
| </Button> | |
| <Button variant="ghost" size="sm" onClick={() => onResetPassword(user)} title="Reset Password"> | |
| <Icon name="key" size={16} /> | |
| </Button> | |
| {user.isActive ? ( | |
| <Button variant="ghost" size="sm" className="text-error-600 hover:text-error-700 hover:bg-error-50 dark:hover:bg-error-900/20" onClick={() => onDeactivate(user)} title="Deactivate User"> | |
| <Icon name="userX" size={16} /> | |
| </Button> | |
| ) : ( | |
| <Button variant="ghost" size="sm" className="text-success-600 hover:text-success-700 hover:bg-success-50 dark:hover:bg-success-900/20" onClick={() => onActivate(user)} title="Activate User"> | |
| <Icon name="checkCircle" size={16} /> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => onView(user)} | |
| title="View Details" | |
| aria-label="View details" | |
| > | |
| <Icon name="eye" size={16} aria-hidden="true" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => onResetPassword(user)} | |
| title="Reset Password" | |
| aria-label="Reset password" | |
| > | |
| <Icon name="key" size={16} aria-hidden="true" /> | |
| </Button> | |
| {user.isActive ? ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="text-error-600 hover:text-error-700 hover:bg-error-50 dark:hover:bg-error-900/20" | |
| onClick={() => onDeactivate(user)} | |
| title="Deactivate User" | |
| aria-label="Deactivate user" | |
| > | |
| <Icon name="userX" size={16} aria-hidden="true" /> | |
| </Button> | |
| ) : ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="text-success-600 hover:text-success-700 hover:bg-success-50 dark:hover:bg-success-900/20" | |
| onClick={() => onActivate(user)} | |
| title="Activate User" | |
| aria-label="Activate user" | |
| > | |
| <Icon name="checkCircle" size={16} aria-hidden="true" /> |
| "DefaultConnection": "Server=tcp:ngo-platform.database.windows.net,1433;Initial Catalog=ngo-platform-dev;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;Authentication=\"Active Directory Default\";" | ||
| }, | ||
| "ASPIRE_MOBILE_PLATFORM": "ios", | ||
| "ASPIRE_MOBILE_PLATFORM": "no-mobile", |
There was a problem hiding this comment.
The configuration change from "ios" to "no-mobile" appears intentional for development purposes, but this should be documented in the commit message or PR description why this change was made. If this is just a personal development preference, it should not be committed to the repository as it affects all developers.
| "ASPIRE_MOBILE_PLATFORM": "no-mobile", | |
| "ASPIRE_MOBILE_PLATFORM": "ios", |
| resolve: { | ||
| alias: { | ||
| "@ngo-platform/shared": path.resolve( | ||
| __dirname, | ||
| "../../packages/shared/src/index.ts", | ||
| ), | ||
| }, | ||
| }, |
There was a problem hiding this comment.
The vite.config.ts files are adding a resolve alias for "@ngo-platform/shared" which might conflict with the workspace's native package resolution. Since this is a Yarn workspace monorepo, the package should be resolvable without this alias. This manual alias could cause issues where the IDE and build tools have different resolution paths. Verify if this alias is actually necessary or if it can be removed.
| resolve: { | |
| alias: { | |
| "@ngo-platform/shared": path.resolve( | |
| __dirname, | |
| "../../packages/shared/src/index.ts", | |
| ), | |
| }, | |
| }, |
| // Convert status filter to boolean or undefined | ||
| const isActive = statusFilter === 'All' ? undefined : statusFilter === 'Active'; | ||
|
|
||
| const { data: usersResponse, isLoading, error, refetch } = features.useUsers({ |
There was a problem hiding this comment.
Unused variable refetch.
| const { data: usersResponse, isLoading, error, refetch } = features.useUsers({ | |
| const { data: usersResponse, isLoading, error } = features.useUsers({ |
|
@copilot fix conflicts with develop |
…develop and resolve conflicts Merge conflicts resolved: - Accepted tenant sorting implementation from develop (backend + frontend) - Removed vite.config.ts alias (not needed with Yarn workspaces) - Reverted ASPIRE_MOBILE_PLATFORM to 'ios' (default) - Kept our Users Management implementation (UsersPage, UsersController) - Kept our refactored Tenants components (TenantFormDialog, TenantsFilters, etc.) - Kept our tenant admin assignment features (AssignAdminModal, UnassignModal) - Accepted DashboardPage tenant count feature from develop - Kept mobile HTTP support in config for SSL certificate issues
Co-authored-by: Nikutsuki <70451760+Nikutsuki@users.noreply.github.com>
[PT2025BMHW-119][SuperAdminUI,SharedUI,Backend,Infrastructure] Merge develop and resolve conflicts
cos tam cos tam