Merge pull request #290 from Worklenz/imp/invite--improvement
Imp/invite improvement
This commit is contained in:
@@ -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
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user