From 27605b4d68a1bccb7449c2c11237f2ab98cc8009 Mon Sep 17 00:00:00 2001 From: shancds Date: Fri, 25 Jul 2025 09:47:09 +0530 Subject: [PATCH 1/3] feat(assignee-selector): enhance member invitation functionality and integrate project manager checks - Added hooks for project manager status and authentication to manage member invitation visibility. - Refactored dropdown toggle logic for improved readability and performance. - Updated UI to conditionally render the invite member button based on user roles (admin or project manager). - Cleaned up code formatting for better consistency and maintainability. --- .../src/components/AssigneeSelector.tsx | 108 +++++++++--------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/worklenz-frontend/src/components/AssigneeSelector.tsx b/worklenz-frontend/src/components/AssigneeSelector.tsx index 3f786959..28126441 100644 --- a/worklenz-frontend/src/components/AssigneeSelector.tsx +++ b/worklenz-frontend/src/components/AssigneeSelector.tsx @@ -13,6 +13,8 @@ import { sortTeamMembers } from '@/utils/sort-team-members'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { setIsFromAssigner, toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice'; import { updateEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice'; +import useIsProjectManager from '@/hooks/useIsProjectManager'; +import { useAuthStatus } from '@/hooks/useAuthStatus'; interface AssigneeSelectorProps { task: IProjectTask; @@ -21,9 +23,9 @@ interface AssigneeSelectorProps { kanbanMode?: boolean; } -const AssigneeSelector: React.FC = ({ - task, - groupId = null, +const AssigneeSelector: React.FC = ({ + task, + groupId = null, isDarkMode = false, kanbanMode = false }) => { @@ -42,6 +44,8 @@ const AssigneeSelector: React.FC = ({ const currentSession = useAuthService().getCurrentSession(); const { socket } = useSocket(); const dispatch = useAppDispatch(); + const { isAdmin } = useAuthStatus(); + const isProjectManager = useIsProjectManager(); const filteredMembers = useMemo(() => { return teamMembers?.data?.filter(member => @@ -64,7 +68,7 @@ const AssigneeSelector: React.FC = ({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && - buttonRef.current && !buttonRef.current.contains(event.target as Node)) { + buttonRef.current && !buttonRef.current.contains(event.target as Node)) { setIsOpen(false); } }; @@ -74,10 +78,10 @@ const AssigneeSelector: React.FC = ({ // Check if the button is still visible in the viewport if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); - const isVisible = rect.top >= 0 && rect.left >= 0 && - rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth; - + const isVisible = rect.top >= 0 && rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth; + if (isVisible) { updateDropdownPosition(); } else { @@ -98,7 +102,7 @@ const AssigneeSelector: React.FC = ({ document.addEventListener('mousedown', handleClickOutside); window.addEventListener('scroll', handleScroll, true); window.addEventListener('resize', handleResize); - + return () => { document.removeEventListener('mousedown', handleClickOutside); window.removeEventListener('scroll', handleScroll, true); @@ -113,10 +117,10 @@ const AssigneeSelector: React.FC = ({ const handleDropdownToggle = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - + if (!isOpen) { updateDropdownPosition(); - + // Prepare team members data when opening const assignees = task?.assignees?.map(assignee => assignee.team_member_id); const membersData = (members?.data || []).map(member => ({ @@ -125,7 +129,7 @@ const AssigneeSelector: React.FC = ({ })); const sortedMembers = sortTeamMembers(membersData); setTeamMembers({ data: sortedMembers }); - + setIsOpen(true); // Focus search input after opening setTimeout(() => { @@ -160,8 +164,8 @@ const AssigneeSelector: React.FC = ({ // Update local team members state for dropdown UI setTeamMembers(prev => ({ ...prev, - data: (prev.data || []).map(member => - member.id === memberId + data: (prev.data || []).map(member => + member.id === memberId ? { ...member, selected: checked } : member ) @@ -198,8 +202,8 @@ const AssigneeSelector: React.FC = ({ const checkMemberSelected = (memberId: string) => { if (!memberId) return false; // Use optimistic assignees if available, otherwise fall back to task assignees - const assignees = optimisticAssignees.length > 0 - ? optimisticAssignees + const assignees = optimisticAssignees.length > 0 + ? optimisticAssignees : task?.assignees?.map(assignee => assignee.team_member_id) || []; return assignees.includes(memberId); }; @@ -218,12 +222,12 @@ const AssigneeSelector: React.FC = ({ className={` w-5 h-5 rounded-full border border-dashed flex items-center justify-center transition-colors duration-200 - ${isOpen - ? isDarkMode - ? 'border-blue-500 bg-blue-900/20 text-blue-400' + ${isOpen + ? isDarkMode + ? 'border-blue-500 bg-blue-900/20 text-blue-400' : 'border-blue-500 bg-blue-50 text-blue-600' - : isDarkMode - ? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400' + : isDarkMode + ? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400' : 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600' } `} @@ -237,8 +241,8 @@ const AssigneeSelector: React.FC = ({ onClick={e => e.stopPropagation()} className={` fixed z-[99999] w-72 rounded-md shadow-lg border - ${isDarkMode - ? 'bg-gray-800 border-gray-600' + ${isDarkMode + ? 'bg-gray-800 border-gray-600' : 'bg-white border-gray-200' } `} @@ -274,10 +278,10 @@ const AssigneeSelector: React.FC = ({ key={member.id} className={` flex items-center gap-2 p-2 cursor-pointer transition-colors - ${member.pending_invitation - ? 'opacity-50 cursor-not-allowed' - : isDarkMode - ? 'hover:bg-gray-700' + ${member.pending_invitation + ? 'opacity-50 cursor-not-allowed' + : isDarkMode + ? 'hover:bg-gray-700' : 'hover:bg-gray-50' } `} @@ -302,23 +306,21 @@ const AssigneeSelector: React.FC = ({ /> {pendingChanges.has(member.id || '') && ( -
-
+
+
)}
- + - +
{member.name} @@ -340,22 +342,26 @@ const AssigneeSelector: React.FC = ({
{/* Footer */} -
- -
+ + {(isAdmin || isProjectManager) && ( +
+ +
+ )} +
, document.body )} From a9d0244ca2091915b2ea113791fe02c19d1fb710 Mon Sep 17 00:00:00 2001 From: shancds Date: Fri, 25 Jul 2025 11:31:36 +0530 Subject: [PATCH 2/3] fix(update-member-drawer): correct job title assignment in member update request - Updated the job title assignment in the member update request to use the value from the form field instead of a previously hardcoded variable. --- .../src/components/settings/update-member-drawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worklenz-frontend/src/components/settings/update-member-drawer.tsx b/worklenz-frontend/src/components/settings/update-member-drawer.tsx index 5be63897..27050dc3 100644 --- a/worklenz-frontend/src/components/settings/update-member-drawer.tsx +++ b/worklenz-frontend/src/components/settings/update-member-drawer.tsx @@ -94,7 +94,7 @@ const UpdateMemberDrawer = ({ selectedMemberId, onRoleUpdate }: UpdateMemberDraw try { const body: ITeamMemberCreateRequest = { - job_title: selectedJobTitle, + job_title: form.getFieldValue('jobTitle'), emails: [teamMember.email], is_admin: values.access === 'admin', }; From 944acf99dbd6a03dfca6d48d51aca91c36a44435 Mon Sep 17 00:00:00 2001 From: shancds Date: Fri, 25 Jul 2025 12:07:43 +0530 Subject: [PATCH 3/3] feat(project-member-drawer): filter out already invited members from the selection list - Implemented logic to filter available members by excluding those already part of the current project. - Updated the member selection dropdown to display only non-invited members, enhancing the user experience during the invitation process. --- .../project-member-invite-drawer.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/worklenz-frontend/src/components/projects/project-member-invite-drawer/project-member-invite-drawer.tsx b/worklenz-frontend/src/components/projects/project-member-invite-drawer/project-member-invite-drawer.tsx index 00c01d93..01f423e8 100644 --- a/worklenz-frontend/src/components/projects/project-member-invite-drawer/project-member-invite-drawer.tsx +++ b/worklenz-frontend/src/components/projects/project-member-invite-drawer/project-member-invite-drawer.tsx @@ -33,6 +33,12 @@ const ProjectMemberDrawer = () => { const [members, setMembers] = useState({ data: [], total: 0 }); const [teamMembersLoading, setTeamMembersLoading] = useState(false); + // Filter out members already in the project + const currentProjectMemberIds = (currentMembersList || []).map(m => m.team_member_id).filter(Boolean); + const availableMembers = (members?.data || []).filter( + member => member.id && !currentProjectMemberIds.includes(member.id) + ); + const fetchProjectMembers = async () => { if (!projectId) return; dispatch(getAllProjectMembers(projectId)); @@ -226,7 +232,7 @@ const ProjectMemberDrawer = () => { onSearch={handleSearch} onChange={handleSelectChange} onKeyDown={handleKeyDown} - options={members?.data?.map(member => ({ + options={availableMembers.map(member => ({ key: member.id, value: member.id, name: member.name,