"use client"; import * as React from 'react'; import { useEffect, useState, useRef } from 'react'; import { getSupabaseClient } from '../../utils/supabase'; import { AuthChangeEvent, Session } from '@supabase/supabase-js'; import { Loader2, X, Check } from 'lucide-react'; import { cn } from '@/lib/utils'; // Define our own Project type interface Project { id: string; name: string; description?: string | null; attributes?: Record; created_at?: string; updated_at?: string; deleted_at?: string | null; schema_version?: number | null; creator_id?: string | null; team_name?: string; } // 添加需要的类型定义 interface ProjectWithTeam { project_id: string; projects: Project; teams?: { name: string }; } // ProjectSelector component with multi-select support export function ProjectSelector({ value, onChange, className, multiple = false, teamId, teamIds, }: { value?: string | string[]; onChange?: (projectId: string | string[]) => void; className?: string; multiple?: boolean; teamId?: string; // Optional team ID to filter projects by team teamIds?: string[]; // Optional array of team IDs to filter projects by multiple teams }) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [projects, setProjects] = useState([]); const [selectedIds, setSelectedIds] = useState([]); const [isOpen, setIsOpen] = useState(false); const selectorRef = useRef(null); // Normalize team IDs to ensure we're always working with an array const effectiveTeamIds = React.useMemo(() => { if (teamIds && teamIds.length > 0) { return teamIds; } else if (teamId) { return [teamId]; } return undefined; }, [teamId, teamIds]); // Initialize selected projects based on value prop useEffect(() => { if (value) { if (Array.isArray(value)) { setSelectedIds(value); } else { setSelectedIds(value ? [value] : []); } } else { setSelectedIds([]); } }, [value]); // Add click outside listener to close dropdown useEffect(() => { function handleClickOutside(event: MouseEvent) { if (selectorRef.current && !selectorRef.current.contains(event.target as Node)) { setIsOpen(false); } } // Only add the event listener if the dropdown is open if (isOpen) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [isOpen]); useEffect(() => { let isMounted = true; const fetchProjects = async (userId: string) => { if (!isMounted) return; setLoading(true); setError(null); console.log(`开始获取项目数据,用户ID: ${userId}, 团队ID过滤: ${effectiveTeamIds?.join(', ') || '无'}`); try { const supabase = getSupabaseClient(); console.log('Supabase客户端已创建,准备获取项目数据'); if (effectiveTeamIds && effectiveTeamIds.length > 0) { // If team IDs are provided, get projects for those teams console.log(`通过团队ID获取项目: ${effectiveTeamIds.join(', ')}`); const { data: projectsData, error: projectsError } = await supabase .from('team_projects') .select('project_id, projects:project_id(*), teams:team_id(name)') .in('team_id', effectiveTeamIds) .is('projects.deleted_at', null); console.log(`团队项目查询结果:`, projectsData ? `找到${projectsData.length}个` : '无数据', projectsError ? `错误: ${projectsError.message}` : '无错误'); if (projectsError) throw projectsError; if (!projectsData || projectsData.length === 0) { console.log('未找到团队项目,返回空列表'); if (isMounted) setProjects([]); return; } // Extract projects from response with team info if (isMounted) { console.log('处理团队项目数据'); const projectList: Project[] = []; for (const item of projectsData as unknown as ProjectWithTeam[]) { if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) { const project = item.projects as Project; if (item.teams && typeof item.teams === 'object' && 'name' in item.teams) { project.team_name = item.teams.name; } // Avoid duplicate projects from different teams if (!projectList.some(p => p.id === project.id)) { projectList.push(project); } } } console.log(`处理后的项目数据: ${projectList.length}个`); setProjects(projectList); } } else { // If no team IDs, get all user's projects console.log(`获取用户所有项目,用户ID: ${userId}`); const { data: projectsData, error: projectsError } = await supabase .from('user_projects') .select('project_id, projects:project_id(*)') .eq('user_id', userId) .is('projects.deleted_at', null); console.log(`用户项目查询结果:`, projectsData ? `找到${projectsData.length}个` : '无数据', projectsError ? `错误: ${projectsError.message}` : '无错误'); if (projectsError) throw projectsError; if (!projectsData || projectsData.length === 0) { console.log('未找到用户项目,返回空列表'); if (isMounted) setProjects([]); return; } // Fetch team info for these projects const projectIds = projectsData.map(item => item.project_id); console.log(`获取项目的团队信息,项目IDs: ${projectIds.join(', ')}`); // Get team info for each project const { data: teamProjectsData, error: teamProjectsError } = await supabase .from('team_projects') .select('project_id, teams:team_id(name)') .in('project_id', projectIds); console.log(`项目团队关系查询结果:`, teamProjectsData ? `找到${teamProjectsData.length}个` : '无数据'); if (teamProjectsError) throw teamProjectsError; // Create project ID to team name mapping const projectTeamMap: Record = {}; if (teamProjectsData) { teamProjectsData.forEach(item => { if (item.teams && typeof item.teams === 'object' && 'name' in item.teams) { projectTeamMap[item.project_id] = (item.teams as { name: string }).name; } }); } // Extract projects with team names if (isMounted && projectsData) { console.log('处理用户项目数据'); const projectList: Project[] = []; for (const item of projectsData) { if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) { const project = item.projects as Project; project.team_name = projectTeamMap[project.id]; projectList.push(project); } } console.log(`处理后的项目数据: ${projectList.length}个`); setProjects(projectList); } } } catch (err) { console.error('获取项目数据出错:', err); if (isMounted) { setError(err instanceof Error ? err.message : '获取项目数据失败'); } } finally { if (isMounted) { setLoading(false); } } }; // 获取Supabase客户端实例并订阅认证状态变化 try { const supabase = getSupabaseClient(); console.log('注册项目选择器认证状态变化监听器'); const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { console.log(`项目选择器认证状态变化: ${event}`, session ? `用户ID: ${session.user.id}` : '无会话'); if (event === 'SIGNED_IN' && session?.user?.id) { fetchProjects(session.user.id); } else if (event === 'SIGNED_OUT') { setProjects([]); setError(null); } }); // 初始化时获取当前会话 console.log('项目选择器获取当前会话状态'); supabase.auth.getSession().then(({ data: { session } }) => { console.log('项目选择器当前会话状态:', session ? `用户已登录,ID: ${session.user.id}` : '用户未登录'); if (session?.user?.id) { fetchProjects(session.user.id); } else { // 如果没有会话但组件需要初始化,可以设置加载完成 setLoading(false); } }).catch(err => { console.error('项目选择器获取会话状态失败:', err); // 确保即使获取会话失败也停止加载状态 setLoading(false); }); return () => { console.log('ProjectSelector组件卸载,清理订阅'); isMounted = false; subscription.unsubscribe(); }; } catch (initError) { console.error('初始化ProjectSelector出错:', initError); // 确保在初始化出错时也停止加载状态 setLoading(false); setError('初始化失败,请刷新页面重试'); return () => { isMounted = false; }; } }, [effectiveTeamIds]); const handleToggle = () => { if (!loading && !error && projects.length > 0) { setIsOpen(!isOpen); } }; const handleProjectSelect = (projectId: string) => { let newSelected: string[]; if (multiple) { // For multi-select: toggle project in/out of selection if (selectedIds.includes(projectId)) { newSelected = selectedIds.filter(id => id !== projectId); } else { newSelected = [...selectedIds, projectId]; } } else { // For single-select: replace selection with the new project newSelected = [projectId]; setIsOpen(false); } setSelectedIds(newSelected); if (onChange) { onChange(multiple ? newSelected : newSelected[0] || ''); } }; const removeProject = (e: React.MouseEvent, projectId: string) => { e.stopPropagation(); const newSelected = selectedIds.filter(id => id !== projectId); setSelectedIds(newSelected); if (onChange) { onChange(multiple ? newSelected : newSelected[0] || ''); } }; if (loading) { return (
); } if (error) { return (
{error}
); } if (projects.length === 0) { return (
No projects available
); } const selectedProjects = projects.filter(project => selectedIds.includes(project.id)); return (
{selectedProjects.length > 0 ? (
{selectedProjects.map(project => (
{project.name} {multiple && ( removeProject(e, project.id)} /> )}
))}
) : (
Select a project
)}
{isOpen && (
{projects.map(project => (
handleProjectSelect(project.id)} > {project.name} {project.team_name && ( {project.team_name} )} {project.description && ( {project.description} )} {selectedIds.includes(project.id) && ( )}
))}
)}
); }