Merge pull request #290 from Worklenz/imp/invite--improvement

Imp/invite  improvement
This commit is contained in:
Chamika J
2025-07-25 13:01:29 +05:30
committed by GitHub
3 changed files with 65 additions and 53 deletions

View File

@@ -13,6 +13,8 @@ import { sortTeamMembers } from '@/utils/sort-team-members';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { setIsFromAssigner, toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice'; import { setIsFromAssigner, toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice';
import { updateEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import { updateEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import useIsProjectManager from '@/hooks/useIsProjectManager';
import { useAuthStatus } from '@/hooks/useAuthStatus';
interface AssigneeSelectorProps { interface AssigneeSelectorProps {
task: IProjectTask; task: IProjectTask;
@@ -21,9 +23,9 @@ interface AssigneeSelectorProps {
kanbanMode?: boolean; kanbanMode?: boolean;
} }
const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
task, task,
groupId = null, groupId = null,
isDarkMode = false, isDarkMode = false,
kanbanMode = false kanbanMode = false
}) => { }) => {
@@ -42,6 +44,8 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
const currentSession = useAuthService().getCurrentSession(); const currentSession = useAuthService().getCurrentSession();
const { socket } = useSocket(); const { socket } = useSocket();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isAdmin } = useAuthStatus();
const isProjectManager = useIsProjectManager();
const filteredMembers = useMemo(() => { const filteredMembers = useMemo(() => {
return teamMembers?.data?.filter(member => return teamMembers?.data?.filter(member =>
@@ -64,7 +68,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && 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); setIsOpen(false);
} }
}; };
@@ -74,10 +78,10 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
// Check if the button is still visible in the viewport // Check if the button is still visible in the viewport
if (buttonRef.current) { if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect(); const rect = buttonRef.current.getBoundingClientRect();
const isVisible = rect.top >= 0 && rect.left >= 0 && const isVisible = rect.top >= 0 && rect.left >= 0 &&
rect.bottom <= window.innerHeight && rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth; rect.right <= window.innerWidth;
if (isVisible) { if (isVisible) {
updateDropdownPosition(); updateDropdownPosition();
} else { } else {
@@ -98,7 +102,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
window.addEventListener('scroll', handleScroll, true); window.addEventListener('scroll', handleScroll, true);
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
window.removeEventListener('scroll', handleScroll, true); window.removeEventListener('scroll', handleScroll, true);
@@ -113,10 +117,10 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
const handleDropdownToggle = (e: React.MouseEvent) => { const handleDropdownToggle = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!isOpen) { if (!isOpen) {
updateDropdownPosition(); updateDropdownPosition();
// Prepare team members data when opening // Prepare team members data when opening
const assignees = task?.assignees?.map(assignee => assignee.team_member_id); const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
const membersData = (members?.data || []).map(member => ({ const membersData = (members?.data || []).map(member => ({
@@ -125,7 +129,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
})); }));
const sortedMembers = sortTeamMembers(membersData); const sortedMembers = sortTeamMembers(membersData);
setTeamMembers({ data: sortedMembers }); setTeamMembers({ data: sortedMembers });
setIsOpen(true); setIsOpen(true);
// Focus search input after opening // Focus search input after opening
setTimeout(() => { setTimeout(() => {
@@ -160,8 +164,8 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
// Update local team members state for dropdown UI // Update local team members state for dropdown UI
setTeamMembers(prev => ({ setTeamMembers(prev => ({
...prev, ...prev,
data: (prev.data || []).map(member => data: (prev.data || []).map(member =>
member.id === memberId member.id === memberId
? { ...member, selected: checked } ? { ...member, selected: checked }
: member : member
) )
@@ -198,8 +202,8 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
const checkMemberSelected = (memberId: string) => { const checkMemberSelected = (memberId: string) => {
if (!memberId) return false; if (!memberId) return false;
// Use optimistic assignees if available, otherwise fall back to task assignees // Use optimistic assignees if available, otherwise fall back to task assignees
const assignees = optimisticAssignees.length > 0 const assignees = optimisticAssignees.length > 0
? optimisticAssignees ? optimisticAssignees
: task?.assignees?.map(assignee => assignee.team_member_id) || []; : task?.assignees?.map(assignee => assignee.team_member_id) || [];
return assignees.includes(memberId); return assignees.includes(memberId);
}; };
@@ -218,12 +222,12 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
className={` className={`
w-5 h-5 rounded-full border border-dashed flex items-center justify-center w-5 h-5 rounded-full border border-dashed flex items-center justify-center
transition-colors duration-200 transition-colors duration-200
${isOpen ${isOpen
? isDarkMode ? isDarkMode
? 'border-blue-500 bg-blue-900/20 text-blue-400' ? 'border-blue-500 bg-blue-900/20 text-blue-400'
: 'border-blue-500 bg-blue-50 text-blue-600' : 'border-blue-500 bg-blue-50 text-blue-600'
: isDarkMode : isDarkMode
? 'border-gray-600 hover:border-gray-500 hover:bg-gray-800 text-gray-400' ? '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' : 'border-gray-300 hover:border-gray-400 hover:bg-gray-100 text-gray-600'
} }
`} `}
@@ -237,8 +241,8 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
className={` className={`
fixed z-[99999] w-72 rounded-md shadow-lg border fixed z-[99999] w-72 rounded-md shadow-lg border
${isDarkMode ${isDarkMode
? 'bg-gray-800 border-gray-600' ? 'bg-gray-800 border-gray-600'
: 'bg-white border-gray-200' : 'bg-white border-gray-200'
} }
`} `}
@@ -274,10 +278,10 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
key={member.id} key={member.id}
className={` className={`
flex items-center gap-2 p-2 cursor-pointer transition-colors flex items-center gap-2 p-2 cursor-pointer transition-colors
${member.pending_invitation ${member.pending_invitation
? 'opacity-50 cursor-not-allowed' ? 'opacity-50 cursor-not-allowed'
: isDarkMode : isDarkMode
? 'hover:bg-gray-700' ? 'hover:bg-gray-700'
: 'hover:bg-gray-50' : 'hover:bg-gray-50'
} }
`} `}
@@ -302,23 +306,21 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
/> />
</span> </span>
{pendingChanges.has(member.id || '') && ( {pendingChanges.has(member.id || '') && (
<div className={`absolute inset-0 flex items-center justify-center ${ <div className={`absolute inset-0 flex items-center justify-center ${isDarkMode ? 'bg-gray-800/50' : 'bg-white/50'
isDarkMode ? 'bg-gray-800/50' : 'bg-white/50' }`}>
}`}> <div className={`w-3 h-3 border border-t-transparent rounded-full animate-spin ${isDarkMode ? 'border-blue-400' : 'border-blue-600'
<div className={`w-3 h-3 border border-t-transparent rounded-full animate-spin ${ }`} />
isDarkMode ? 'border-blue-400' : 'border-blue-600'
}`} />
</div> </div>
)} )}
</div> </div>
<Avatar <Avatar
src={member.avatar_url} src={member.avatar_url}
name={member.name || ''} name={member.name || ''}
size={24} size={24}
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className={`text-xs font-medium truncate ${isDarkMode ? 'text-gray-100' : 'text-gray-900'}`}> <div className={`text-xs font-medium truncate ${isDarkMode ? 'text-gray-100' : 'text-gray-900'}`}>
{member.name} {member.name}
@@ -340,22 +342,26 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
</div> </div>
{/* Footer */} {/* Footer */}
<div className={`p-2 border-t ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
<button {(isAdmin || isProjectManager) && (
className={` <div className={`p-2 border-t ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
w-full flex items-center justify-center gap-1 px-2 py-1 text-xs rounded <button
transition-colors className={`
${isDarkMode w-full flex items-center justify-center gap-1 px-2 py-1 text-xs rounded
? 'text-blue-400 hover:bg-gray-700' transition-colors
: 'text-blue-600 hover:bg-blue-50' ${isDarkMode
} ? 'text-blue-400 hover:bg-gray-700'
`} : 'text-blue-600 hover:bg-blue-50'
onClick={handleInviteProjectMemberDrawer} }
> `}
<UserAddOutlined /> onClick={handleInviteProjectMemberDrawer}
Invite member >
</button> <UserAddOutlined />
</div> Invite member
</button>
</div>
)}
</div>, </div>,
document.body document.body
)} )}

View File

@@ -33,6 +33,12 @@ const ProjectMemberDrawer = () => {
const [members, setMembers] = useState<ITeamMembersViewModel>({ data: [], total: 0 }); const [members, setMembers] = useState<ITeamMembersViewModel>({ data: [], total: 0 });
const [teamMembersLoading, setTeamMembersLoading] = useState(false); 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 () => { const fetchProjectMembers = async () => {
if (!projectId) return; if (!projectId) return;
dispatch(getAllProjectMembers(projectId)); dispatch(getAllProjectMembers(projectId));
@@ -226,7 +232,7 @@ const ProjectMemberDrawer = () => {
onSearch={handleSearch} onSearch={handleSearch}
onChange={handleSelectChange} onChange={handleSelectChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
options={members?.data?.map(member => ({ options={availableMembers.map(member => ({
key: member.id, key: member.id,
value: member.id, value: member.id,
name: member.name, name: member.name,

View File

@@ -94,7 +94,7 @@ const UpdateMemberDrawer = ({ selectedMemberId, onRoleUpdate }: UpdateMemberDraw
try { try {
const body: ITeamMemberCreateRequest = { const body: ITeamMemberCreateRequest = {
job_title: selectedJobTitle, job_title: form.getFieldValue('jobTitle'),
emails: [teamMember.email], emails: [teamMember.email],
is_admin: values.access === 'admin', is_admin: values.access === 'admin',
}; };