refactor(AssigneeSelector): streamline component logic and enhance dropdown behavior
- Removed unused kanbanMode prop and related logic to simplify the AssigneeSelector component. - Updated dropdown position handling to improve visibility and responsiveness during interactions. - Optimized member selection logic for better performance and user feedback. - Enhanced the rendering of team members with improved visual feedback for pending changes.
This commit is contained in:
@@ -5,29 +5,25 @@ import { PlusOutlined, UserAddOutlined } from '@ant-design/icons';
|
|||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
|
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
|
||||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
|
||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
import { Avatar, Checkbox } from '@/components';
|
import { Avatar, Button, Checkbox } from '@/components';
|
||||||
import { sortTeamMembers } from '@/utils/sort-team-members';
|
import { sortTeamMembers } from '@/utils/sort-team-members';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice';
|
import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice';
|
||||||
import { updateTaskAssignees } from '@/features/task-management/task-management.slice';
|
import { updateEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
|
|
||||||
|
|
||||||
interface AssigneeSelectorProps {
|
interface AssigneeSelectorProps {
|
||||||
task: IProjectTask;
|
task: IProjectTask;
|
||||||
groupId?: string | null;
|
groupId?: string | null;
|
||||||
isDarkMode?: boolean;
|
isDarkMode?: boolean;
|
||||||
kanbanMode?: boolean; // <-- Add this prop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
||||||
task,
|
task,
|
||||||
groupId = null,
|
groupId = null,
|
||||||
isDarkMode = false,
|
isDarkMode = false
|
||||||
kanbanMode = false, // <-- Default to false
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -35,12 +31,6 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
||||||
const [optimisticAssignees, setOptimisticAssignees] = useState<string[]>([]); // For optimistic updates
|
const [optimisticAssignees, setOptimisticAssignees] = useState<string[]>([]); // For optimistic updates
|
||||||
const [pendingChanges, setPendingChanges] = useState<Set<string>>(new Set()); // Track pending member changes
|
const [pendingChanges, setPendingChanges] = useState<Set<string>>(new Set()); // Track pending member changes
|
||||||
|
|
||||||
// Initialize optimistic assignees from task data on mount or when task changes
|
|
||||||
useEffect(() => {
|
|
||||||
const currentAssigneeIds = task?.assignees?.map(a => a.team_member_id) || [];
|
|
||||||
setOptimisticAssignees(currentAssigneeIds);
|
|
||||||
}, [task?.assignees]);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -61,16 +51,9 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
const updateDropdownPosition = useCallback(() => {
|
const updateDropdownPosition = useCallback(() => {
|
||||||
if (buttonRef.current) {
|
if (buttonRef.current) {
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
const dropdownHeight = 280; // More accurate height: header(40) + max-height(192) + footer(40) + padding
|
|
||||||
|
|
||||||
// Check if dropdown would go below viewport
|
|
||||||
const spaceBelow = viewportHeight - rect.bottom;
|
|
||||||
const shouldShowAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
|
|
||||||
|
|
||||||
setDropdownPosition({
|
setDropdownPosition({
|
||||||
top: shouldShowAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4,
|
top: rect.bottom + window.scrollY + 2,
|
||||||
left: rect.left,
|
left: rect.left + window.scrollX,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -78,21 +61,27 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
// Close dropdown when clicking outside and handle scroll
|
// Close dropdown when clicking outside and handle scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node) &&
|
||||||
dropdownRef.current &&
|
buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
|
||||||
!dropdownRef.current.contains(event.target as Node) &&
|
|
||||||
buttonRef.current &&
|
|
||||||
!buttonRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = (event: Event) => {
|
const handleScroll = () => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
// Only close dropdown if scrolling happens outside the dropdown
|
// Check if the button is still visible in the viewport
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
if (buttonRef.current) {
|
||||||
setIsOpen(false);
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
|
const isVisible = rect.top >= 0 && rect.left >= 0 &&
|
||||||
|
rect.bottom <= window.innerHeight &&
|
||||||
|
rect.right <= window.innerWidth;
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
updateDropdownPosition();
|
||||||
|
} else {
|
||||||
|
// Hide dropdown if button is not visible
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -107,7 +96,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);
|
||||||
@@ -122,22 +111,19 @@ 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 - use optimistic assignees for current state
|
|
||||||
const currentAssigneeIds = optimisticAssignees.length > 0
|
|
||||||
? optimisticAssignees
|
|
||||||
: task?.assignees?.map(assignee => assignee.team_member_id) || [];
|
|
||||||
|
|
||||||
const membersData: (ITeamMembersViewModel & { selected?: boolean })[] = (members?.data || []).map(member => ({
|
// Prepare team members data when opening
|
||||||
|
const assignees = task?.assignees?.map(assignee => assignee.team_member_id);
|
||||||
|
const membersData = (members?.data || []).map(member => ({
|
||||||
...member,
|
...member,
|
||||||
selected: currentAssigneeIds.includes(member.id),
|
selected: assignees?.includes(member.id),
|
||||||
}));
|
}));
|
||||||
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(() => {
|
||||||
@@ -154,20 +140,16 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
// Add to pending changes for visual feedback
|
// Add to pending changes for visual feedback
|
||||||
setPendingChanges(prev => new Set(prev).add(memberId));
|
setPendingChanges(prev => new Set(prev).add(memberId));
|
||||||
|
|
||||||
// Get the current list of assignees, prioritizing optimistic updates for immediate feedback
|
// OPTIMISTIC UPDATE: Update local state immediately for instant UI feedback
|
||||||
const currentAssigneeIds = optimisticAssignees.length > 0
|
const currentAssignees = task?.assignees?.map(a => a.team_member_id) || [];
|
||||||
? optimisticAssignees
|
|
||||||
: task?.assignees?.map(a => a.team_member_id) || [];
|
|
||||||
|
|
||||||
let newAssigneeIds: string[];
|
let newAssigneeIds: string[];
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
// Adding assignee: ensure no duplicates
|
// Adding assignee
|
||||||
const uniqueIds = new Set([...currentAssigneeIds, memberId]);
|
newAssigneeIds = [...currentAssignees, memberId];
|
||||||
newAssigneeIds = Array.from(uniqueIds);
|
|
||||||
} else {
|
} else {
|
||||||
// Removing assignee
|
// Removing assignee
|
||||||
newAssigneeIds = currentAssigneeIds.filter(id => id !== memberId);
|
newAssigneeIds = currentAssignees.filter(id => id !== memberId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update optimistic state for immediate UI feedback in dropdown
|
// Update optimistic state for immediate UI feedback in dropdown
|
||||||
@@ -176,9 +158,11 @@ 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, selected: checked } : member
|
member.id === memberId
|
||||||
),
|
? { ...member, selected: checked }
|
||||||
|
: member
|
||||||
|
)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
@@ -192,35 +176,17 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
|
|
||||||
// Emit socket event - the socket handler will update Redux with proper types
|
// Emit socket event - the socket handler will update Redux with proper types
|
||||||
socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body));
|
socket?.emit(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), JSON.stringify(body));
|
||||||
socket?.once(SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(), (data: any) => {
|
socket?.once(
|
||||||
// Instead of updating enhancedKanbanSlice, update the main taskManagementSlice
|
SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(),
|
||||||
// Filter members to get the actual InlineMember objects for the new assignees
|
(data: any) => {
|
||||||
const updatedAssigneeNames: InlineMember[] = (members?.data || [])
|
|
||||||
.filter((member): member is ITeamMemberViewModel & { id: string; name: string } => {
|
|
||||||
return typeof member.id === 'string' && typeof member.name === 'string' && newAssigneeIds.includes(member.id);
|
|
||||||
})
|
|
||||||
.map(member => ({
|
|
||||||
name: member.name || '',
|
|
||||||
id: member.id || '',
|
|
||||||
team_member_id: member.id || '',
|
|
||||||
avatar_url: member.avatar_url || '',
|
|
||||||
color_code: member.color_code || '',
|
|
||||||
}));
|
|
||||||
|
|
||||||
dispatch(updateTaskAssignees({
|
|
||||||
taskId: task.id || '',
|
|
||||||
assigneeIds: newAssigneeIds,
|
|
||||||
assigneeNames: updatedAssigneeNames,
|
|
||||||
}));
|
|
||||||
if (kanbanMode) {
|
|
||||||
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
// Remove from pending changes after a short delay (optimistic)
|
// Remove from pending changes after a short delay (optimistic)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setPendingChanges(prev => {
|
setPendingChanges(prev => {
|
||||||
const newSet = new Set<string>(Array.from(prev));
|
const newSet = new Set(prev);
|
||||||
newSet.delete(memberId);
|
newSet.delete(memberId);
|
||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
@@ -229,8 +195,11 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
|||||||
|
|
||||||
const checkMemberSelected = (memberId: string) => {
|
const checkMemberSelected = (memberId: string) => {
|
||||||
if (!memberId) return false;
|
if (!memberId) return false;
|
||||||
// Always use optimistic assignees for dropdown display
|
// Use optimistic assignees if available, otherwise fall back to task assignees
|
||||||
return optimisticAssignees.includes(memberId);
|
const assignees = optimisticAssignees.length > 0
|
||||||
|
? optimisticAssignees
|
||||||
|
: task?.assignees?.map(assignee => assignee.team_member_id) || [];
|
||||||
|
return assignees.includes(memberId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInviteProjectMemberDrawer = () => {
|
const handleInviteProjectMemberDrawer = () => {
|
||||||
@@ -246,159 +215,149 @@ 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'
|
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<PlusOutlined className="text-xs" />
|
<PlusOutlined className="text-xs" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen &&
|
{isOpen && createPortal(
|
||||||
createPortal(
|
<div
|
||||||
<div
|
ref={dropdownRef}
|
||||||
ref={dropdownRef}
|
onClick={e => e.stopPropagation()}
|
||||||
onClick={e => e.stopPropagation()}
|
className={`
|
||||||
className={`
|
|
||||||
fixed z-9999 w-72 rounded-md shadow-lg border
|
fixed z-9999 w-72 rounded-md shadow-lg border
|
||||||
${isDarkMode ? 'bg-gray-800 border-gray-600' : 'bg-white border-gray-200'}
|
${isDarkMode
|
||||||
|
? 'bg-gray-800 border-gray-600'
|
||||||
|
: 'bg-white border-gray-200'
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
top: dropdownPosition.top,
|
top: dropdownPosition.top,
|
||||||
left: dropdownPosition.left,
|
left: dropdownPosition.left,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className={`p-2 border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`}>
|
<div className={`p-2 border-b ${isDarkMode ? 'border-gray-600' : 'border-gray-300'}`}>
|
||||||
<input
|
<input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={e => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="Search members..."
|
placeholder="Search members..."
|
||||||
className={`
|
className={`
|
||||||
w-full px-2 py-1 text-xs rounded border
|
w-full px-2 py-1 text-xs rounded border
|
||||||
${
|
${isDarkMode
|
||||||
isDarkMode
|
? 'bg-gray-700 border-gray-600 text-gray-100 placeholder-gray-400 focus:border-blue-500'
|
||||||
? 'bg-gray-700 border-gray-600 text-gray-100 placeholder-gray-400 focus:border-blue-500'
|
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-500 focus:border-blue-500'
|
||||||
: 'bg-white border-gray-300 text-gray-900 placeholder-gray-500 focus:border-blue-500'
|
|
||||||
}
|
}
|
||||||
focus:outline-none focus:ring-1 focus:ring-blue-500
|
focus:outline-none focus:ring-1 focus:ring-blue-500
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Members List */}
|
{/* Members List */}
|
||||||
<div className="max-h-48 overflow-y-auto">
|
<div className="max-h-48 overflow-y-auto">
|
||||||
{filteredMembers && filteredMembers.length > 0 ? (
|
{filteredMembers && filteredMembers.length > 0 ? (
|
||||||
filteredMembers.map(member => (
|
filteredMembers.map((member) => (
|
||||||
<div
|
<div
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!member.pending_invitation) {
|
if (!member.pending_invitation) {
|
||||||
const isSelected = checkMemberSelected(member.id || '');
|
const isSelected = checkMemberSelected(member.id || '');
|
||||||
handleMemberToggle(member.id || '', !isSelected);
|
handleMemberToggle(member.id || '', !isSelected);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
// Add visual feedback for immediate response
|
// Add visual feedback for immediate response
|
||||||
transition: 'all 0.15s ease-in-out',
|
transition: 'all 0.15s ease-in-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<span onClick={e => e.stopPropagation()}>
|
<span onClick={e => e.stopPropagation()}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={checkMemberSelected(member.id || '')}
|
checked={checkMemberSelected(member.id || '')}
|
||||||
onChange={checked => handleMemberToggle(member.id || '', checked)}
|
onChange={(checked) => handleMemberToggle(member.id || '', checked)}
|
||||||
disabled={
|
disabled={member.pending_invitation || pendingChanges.has(member.id || '')}
|
||||||
member.pending_invitation || pendingChanges.has(member.id || '')
|
isDarkMode={isDarkMode}
|
||||||
}
|
/>
|
||||||
isDarkMode={isDarkMode}
|
</span>
|
||||||
/>
|
{pendingChanges.has(member.id || '') && (
|
||||||
</span>
|
<div className={`absolute inset-0 flex items-center justify-center ${
|
||||||
{pendingChanges.has(member.id || '') && (
|
isDarkMode ? 'bg-gray-800/50' : 'bg-white/50'
|
||||||
<div
|
}`}>
|
||||||
className={`absolute inset-0 flex items-center justify-center ${
|
<div className={`w-3 h-3 border border-t-transparent rounded-full animate-spin ${
|
||||||
isDarkMode ? 'bg-gray-800/50' : 'bg-white/50'
|
isDarkMode ? 'border-blue-400' : 'border-blue-600'
|
||||||
}`}
|
}`} />
|
||||||
>
|
</div>
|
||||||
<div
|
)}
|
||||||
className={`w-3 h-3 border border-t-transparent rounded-full animate-spin ${
|
</div>
|
||||||
isDarkMode ? 'border-blue-400' : 'border-blue-600'
|
|
||||||
}`}
|
<Avatar
|
||||||
/>
|
src={member.avatar_url}
|
||||||
</div>
|
name={member.name || ''}
|
||||||
|
size={24}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className={`text-xs font-medium truncate ${isDarkMode ? 'text-gray-100' : 'text-gray-900'}`}>
|
||||||
|
{member.name}
|
||||||
|
</div>
|
||||||
|
<div className={`text-xs truncate ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||||
|
{member.email}
|
||||||
|
{member.pending_invitation && (
|
||||||
|
<span className="text-red-400 ml-1">(Pending)</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Avatar
|
|
||||||
src={member.avatar_url}
|
|
||||||
name={member.name || ''}
|
|
||||||
size={24}
|
|
||||||
isDarkMode={isDarkMode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div
|
|
||||||
className={`text-xs font-medium truncate ${isDarkMode ? 'text-gray-100' : 'text-gray-900'}`}
|
|
||||||
>
|
|
||||||
{member.name}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`text-xs truncate ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
|
||||||
>
|
|
||||||
{member.email}
|
|
||||||
{member.pending_invitation && (
|
|
||||||
<span className="text-red-400 ml-1">(Pending)</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`p-4 text-center ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}
|
|
||||||
>
|
|
||||||
<div className="text-xs">No members found</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
))
|
||||||
</div>
|
) : (
|
||||||
|
<div className={`p-4 text-center ${isDarkMode ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||||
|
<div className="text-xs">No members found</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className={`p-2 border-t ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
|
<div className={`p-2 border-t ${isDarkMode ? 'border-gray-600' : 'border-gray-200'}`}>
|
||||||
<button
|
<button
|
||||||
className={`
|
className={`
|
||||||
w-full flex items-center justify-center gap-1 px-2 py-1 text-xs rounded
|
w-full flex items-center justify-center gap-1 px-2 py-1 text-xs rounded
|
||||||
transition-colors
|
transition-colors
|
||||||
${isDarkMode ? 'text-blue-400 hover:bg-gray-700' : 'text-blue-600 hover:bg-blue-50'}
|
${isDarkMode
|
||||||
|
? 'text-blue-400 hover:bg-gray-700'
|
||||||
|
: 'text-blue-600 hover:bg-blue-50'
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
onClick={handleInviteProjectMemberDrawer}
|
onClick={handleInviteProjectMemberDrawer}
|
||||||
>
|
>
|
||||||
<UserAddOutlined />
|
<UserAddOutlined />
|
||||||
Invite member
|
Invite member
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AssigneeSelector;
|
export default AssigneeSelector;
|
||||||
Reference in New Issue
Block a user