import { useState, useEffect } from 'react'; import { X, Package, GitPullRequest, ExternalLink, RefreshCw, Trash2, CheckCircle, XCircle, AlertCircle, Clock, MessageSquare, User, Upload, Plus } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import { open } from '@tauri-apps/plugin-shell'; import type { GitHubService, PublishedPlugin, PendingReview, CheckStatus, PRComment, PRReview } from '../services/GitHubService'; import { PluginUpdateDialog } from './PluginUpdateDialog'; import { PluginPublishWizard } from './PluginPublishWizard'; import '../styles/UserDashboard.css'; interface UserDashboardProps { githubService: GitHubService; onClose: () => void; locale: string; } type Tab = 'published' | 'pending'; type PRFilter = 'all' | 'open' | 'merged' | 'closed'; export function UserDashboard({ githubService, onClose, locale }: UserDashboardProps) { const [activeTab, setActiveTab] = useState('published'); const [publishedPlugins, setPublishedPlugins] = useState([]); const [pendingReviews, setPendingReviews] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [deletingPR, setDeletingPR] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const [prFilter, setPRFilter] = useState('open'); const [expandedComments, setExpandedComments] = useState>(new Set()); const [confirmDeletePlugin, setConfirmDeletePlugin] = useState(null); const [deleteReason, setDeleteReason] = useState(''); const [deletingPlugin, setDeletingPlugin] = useState(false); const [deleteProgress, setDeleteProgress] = useState({ message: '', progress: 0 }); const [resolvingConflicts, setResolvingConflicts] = useState(null); const [recreatingPR, setRecreatingPR] = useState(null); const [pluginToUpdate, setPluginToUpdate] = useState(null); const [showPublishWizard, setShowPublishWizard] = useState(false); const [expandedVersions, setExpandedVersions] = useState>(new Set()); const user = githubService.getUser(); const t = (key: string) => { const translations: Record> = { zh: { title: '个人中心', published: '已发布插件', pending: '审核中', refresh: '刷新', loading: '加载中...', error: '加载失败', retry: '重试', loadError: '无法加载数据', networkError: '可能是网络连接问题,请检查您的网络设置后重试', noPublished: '还没有发布任何插件', noPending: '没有待审核的插件', version: '版本', latestVersion: '最新版本', allVersions: '所有版本', showVersions: '显示版本历史', hideVersions: '隐藏版本历史', versionCount: '{{count}} 个版本', category: '分类', publishedAt: '发布于', createdAt: '提交于', status: '状态', viewPR: '查看PR', viewRepo: '查看仓库', statusOpen: '审核中', statusMerged: '已合并', statusClosed: '已关闭', deletePR: '删除PR', confirmDeleteTitle: '确认删除', confirmDeleteMessage: '确定要关闭并删除这个 Pull Request 吗?此操作无法撤销。', confirm: '确认', cancel: '取消', deleting: '删除中...', deleteSuccess: '删除成功', deleteFailed: '删除失败', filterAll: '全部', filterOpen: '进行中', filterMerged: '已合并', filterClosed: '已关闭', ciChecks: 'CI检查', ciPassing: '通过', ciFailed: '失败', ciPending: '等待中', viewDetails: '查看详情', comments: '评论', reviews: '审查意见', reviewApproved: '已批准', reviewChangesRequested: '需要修改', reviewCommented: '已评论', noCommentsYet: '暂无评论', showComments: '显示评论', hideComments: '隐藏评论', updatePlugin: '更新插件', publishNewPlugin: '发布新插件', cannotUpdate: '无法更新', hasPendingPR: '该插件有待审核的 PR', pleaseDealWithPR: '请先处理现有的 PR #{{number}},然后再提交新的更新。', deletePlugin: '删除插件', confirmDeletePluginTitle: '确认删除插件', confirmDeletePluginMessage: '确定要删除插件 "{{name}}" 吗?这将创建一个删除请求PR,需要审核后才会从市场移除。', deleteReasonLabel: '删除原因(必填)', deleteReasonPlaceholder: '请说明删除此插件的原因...', confirmDelete: '确认删除', deletePluginError: '删除插件失败', deletePluginSuccess: '删除请求已提交!', openPRQuestion: '是否打开PR查看详情?', deleteConflictError: '删除时发生冲突,可能是因为分支版本不一致。', retryDeleteQuestion: '是否重试?(将删除旧分支并重新创建)', deletePending: '删除请求中', conflicts: '冲突', conflictsDetected: '检测到冲突', conflictFilesLabel: '冲突文件', resolveConflicts: '解决冲突', resolving: '解决中...', resolveConflictsSuccess: '冲突已解决!正在刷新...', resolveConflictsFailed: '解决冲突失败', resolveConflictsHint: '点击此按钮将主分支的最新代码合并到此PR,可能会自动解决冲突', manualResolveRequired: '此冲突需要手动解决', manualResolveHint: '自动合并失败,存在代码冲突。', openInGitHub: '在GitHub上解决', manualResolveInstructions: '请点击下面的按钮在GitHub上手动解决冲突,或者在本地克隆仓库后解决。', recreatePR: '重新创建PR', recreatePRHint: '删除当前分支并基于最新主分支重新创建删除请求(推荐)', recreatePRConfirm: '确定要关闭当前PR并重新创建吗?\n\n这将:\n1. 关闭当前PR\n2. 删除冲突的分支\n3. 基于最新主分支重新创建删除请求\n\n删除原因会保留。', recreating: '重新创建中...', recreatePRSuccess: '已重新创建删除请求!', recreatePRFailed: '重新创建失败' }, en: { title: 'User Dashboard', published: 'Published Plugins', pending: 'Pending Reviews', refresh: 'Refresh', loading: 'Loading...', error: 'Failed to load', retry: 'Retry', loadError: 'Unable to load data', networkError: 'This might be a network connection issue. Please check your network settings and try again', noPublished: 'No published plugins yet', noPending: 'No pending reviews', version: 'Version', latestVersion: 'Latest Version', allVersions: 'All Versions', showVersions: 'Show Version History', hideVersions: 'Hide Version History', versionCount: '{{count}} versions', category: 'Category', publishedAt: 'Published at', createdAt: 'Submitted at', status: 'Status', viewPR: 'View PR', viewRepo: 'View Repository', statusOpen: 'Open', statusMerged: 'Merged', statusClosed: 'Closed', deletePR: 'Delete PR', confirmDeleteTitle: 'Confirm Delete', confirmDeleteMessage: 'Are you sure you want to close and delete this Pull Request? This action cannot be undone.', confirm: 'Confirm', cancel: 'Cancel', deleting: 'Deleting...', deleteSuccess: 'Deleted successfully', deleteFailed: 'Failed to delete', filterAll: 'All', filterOpen: 'Open', filterMerged: 'Merged', filterClosed: 'Closed', ciChecks: 'CI Checks', ciPassing: 'Passing', ciFailed: 'Failed', ciPending: 'Pending', viewDetails: 'View Details', comments: 'Comments', reviews: 'Reviews', reviewApproved: 'Approved', reviewChangesRequested: 'Changes Requested', reviewCommented: 'Commented', noCommentsYet: 'No comments yet', showComments: 'Show Comments', hideComments: 'Hide Comments', updatePlugin: 'Update Plugin', publishNewPlugin: 'Publish New Plugin', cannotUpdate: 'Cannot Update', hasPendingPR: 'This plugin has a pending PR', pleaseDealWithPR: 'Please handle the existing PR #{{number}} before submitting a new update.', deletePlugin: 'Delete Plugin', confirmDeletePluginTitle: 'Confirm Plugin Deletion', confirmDeletePluginMessage: 'Are you sure you want to delete plugin "{{name}}"? This will create a deletion request PR that requires review before removal from marketplace.', deleteReasonLabel: 'Reason for Deletion (Required)', deleteReasonPlaceholder: 'Please explain why you want to delete this plugin...', confirmDelete: 'Confirm Delete', deletePluginError: 'Failed to delete plugin', deletePluginSuccess: 'Deletion request submitted!', openPRQuestion: 'Open PR to view details?', deleteConflictError: 'Conflict occurred during deletion, possibly due to branch version mismatch.', retryDeleteQuestion: 'Retry? (Will delete old branch and recreate)', deletePending: 'Deletion Pending', conflicts: 'Conflicts', conflictsDetected: 'Conflicts Detected', conflictFilesLabel: 'Conflict Files', resolveConflicts: 'Resolve Conflicts', resolving: 'Resolving...', resolveConflictsSuccess: 'Conflicts resolved! Refreshing...', resolveConflictsFailed: 'Failed to resolve conflicts', resolveConflictsHint: 'Click to merge latest changes from main branch into this PR, which may automatically resolve conflicts', manualResolveRequired: 'Manual resolution required', manualResolveHint: 'Automatic merge failed due to code conflicts.', openInGitHub: 'Resolve on GitHub', manualResolveInstructions: 'Please click the button below to resolve conflicts on GitHub, or clone the repository locally.', recreatePR: 'Recreate PR', recreatePRHint: 'Delete current branch and recreate deletion request based on latest main branch (Recommended)', recreatePRConfirm: 'Are you sure you want to close current PR and recreate?\n\nThis will:\n1. Close current PR\n2. Delete conflicting branch\n3. Recreate deletion request based on latest main branch\n\nDeletion reason will be preserved.', recreating: 'Recreating...', recreatePRSuccess: 'Deletion request recreated!', recreatePRFailed: 'Failed to recreate' } }; return translations[locale]?.[key] || translations.en?.[key] || key; }; const loadData = async () => { setLoading(true); setError(''); try { const [published, pending] = await Promise.all([ githubService.getPublishedPlugins(), githubService.getPendingReviews() ]); setPublishedPlugins(published); setPendingReviews(pending); } catch (err) { console.error('[UserDashboard] Failed to load data:', err); setError(err instanceof Error ? err.message : 'Failed to load data'); } finally { setLoading(false); } }; useEffect(() => { loadData(); }, []); const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString(locale === 'zh' ? 'zh-CN' : 'en-US', { year: 'numeric', month: 'short', day: 'numeric' }); }; const handleUpdatePlugin = (plugin: PublishedPlugin) => { setPluginToUpdate(plugin); }; const getStatusBadgeClass = (status: string) => { switch (status) { case 'open': return 'status-badge status-open'; case 'merged': return 'status-badge status-merged'; case 'closed': return 'status-badge status-closed'; default: return 'status-badge'; } }; const handleDeletePlugin = async () => { if (!confirmDeletePlugin || !deleteReason.trim()) { return; } setDeletingPlugin(true); setDeleteProgress({ message: '', progress: 0 }); try { const { PluginPublishService } = await import('../services/PluginPublishService'); const publishService = new PluginPublishService(githubService); publishService.setProgressCallback((progress) => { setDeleteProgress({ message: progress.message, progress: progress.progress }); }); const prUrl = await publishService.deletePlugin( confirmDeletePlugin.id, confirmDeletePlugin.name, confirmDeletePlugin.category_type as 'official' | 'community', deleteReason ); console.log(`[UserDashboard] Delete PR created:`, prUrl); setConfirmDeletePlugin(null); setDeleteReason(''); await loadData(); if (confirm(t('deletePluginSuccess') + '\n\n' + t('openPRQuestion'))) { open(prUrl); } } catch (err) { console.error('[UserDashboard] Failed to delete plugin:', err); const errorMsg = err instanceof Error ? err.message : String(err); if (errorMsg.includes('conflicts') || errorMsg.includes('does not match')) { if (confirm(t('deleteConflictError') + '\n\n' + t('retryDeleteQuestion'))) { setDeletingPlugin(false); setDeleteProgress({ message: '', progress: 0 }); setTimeout(async () => { setDeletingPlugin(true); try { const { PluginPublishService } = await import('../services/PluginPublishService'); const publishService = new PluginPublishService(githubService); publishService.setProgressCallback((progress) => { setDeleteProgress({ message: progress.message, progress: progress.progress }); }); const prUrl = await publishService.deletePlugin( confirmDeletePlugin.id, confirmDeletePlugin.name, confirmDeletePlugin.category_type as 'official' | 'community', deleteReason, true ); setConfirmDeletePlugin(null); setDeleteReason(''); await loadData(); if (confirm(t('deletePluginSuccess') + '\n\n' + t('openPRQuestion'))) { open(prUrl); } } catch (retryErr) { console.error('[UserDashboard] Retry failed:', retryErr); alert(t('deletePluginError') + ': ' + (retryErr instanceof Error ? retryErr.message : String(retryErr))); } finally { setDeletingPlugin(false); } }, 100); return; } } alert(t('deletePluginError') + ': ' + errorMsg); } finally { setDeletingPlugin(false); } }; const handleDeletePR = async (prNumber: number) => { setDeletingPR(prNumber); try { await githubService.closePullRequest('esengine', 'ecs-editor-plugins', prNumber); console.log(`[UserDashboard] Successfully closed PR #${prNumber}`); await loadData(); setConfirmDelete(null); } catch (err) { console.error('[UserDashboard] Failed to close PR:', err); setError(t('deleteFailed') + ': ' + (err instanceof Error ? err.message : String(err))); } finally { setDeletingPR(null); } }; const handleResolveConflicts = async (review: PendingReview) => { setResolvingConflicts(review.prNumber); try { await githubService.updatePRBranch('esengine', 'ecs-editor-plugins', review.prNumber); console.log(`[UserDashboard] Successfully resolved conflicts for PR #${review.prNumber}`); alert(t('resolveConflictsSuccess')); await loadData(); } catch (err) { console.error('[UserDashboard] Failed to resolve conflicts:', err); const errorMsg = err instanceof Error ? err.message : String(err); if (errorMsg.includes('422') || errorMsg.includes('merge conflict')) { const message = `${t('manualResolveRequired')}\n\n${t('manualResolveHint')}\n${t('manualResolveInstructions')}\n\n${t('openInGitHub')}?`; if (confirm(message)) { open(review.prUrl); } } else { alert(t('resolveConflictsFailed') + ': ' + errorMsg); } } finally { setResolvingConflicts(null); } }; const handleRecreatePR = async (review: PendingReview) => { if (!confirm(t('recreatePRConfirm'))) { return; } const removeMatch = review.pluginName.match(/^(.+)$/); if (!removeMatch) { alert(t('recreatePRFailed') + ': Cannot extract plugin name'); return; } const pluginName = removeMatch[0]; const plugin = publishedPlugins.find(p => p.name === pluginName); if (!plugin) { alert(t('recreatePRFailed') + ': Plugin not found in published list'); return; } setRecreatingPR(review.prNumber); try { await githubService.closePullRequest('esengine', 'ecs-editor-plugins', review.prNumber); console.log(`[UserDashboard] Closed PR #${review.prNumber}`); if (review.headBranch && review.headRepo) { const [repoOwner, repoName] = review.headRepo.split('/'); if (repoOwner && repoName) { try { await githubService.deleteBranch(repoOwner, repoName, review.headBranch); console.log(`[UserDashboard] Deleted branch ${review.headBranch}`); } catch (err) { console.warn('[UserDashboard] Failed to delete branch:', err); } } } const { PluginPublishService } = await import('../services/PluginPublishService'); const publishService = new PluginPublishService(githubService); const deleteReason = 'Recreated due to conflicts'; const prUrl = await publishService.deletePlugin( plugin.id, plugin.name, plugin.category_type as 'official' | 'community', deleteReason, true ); console.log(`[UserDashboard] Recreated delete PR:`, prUrl); alert(t('recreatePRSuccess')); await loadData(); if (confirm(t('openPRQuestion'))) { open(prUrl); } } catch (err) { console.error('[UserDashboard] Failed to recreate PR:', err); alert(t('recreatePRFailed') + ': ' + (err instanceof Error ? err.message : String(err))); } finally { setRecreatingPR(null); } }; const getCheckIcon = (check: CheckStatus) => { if (!check.conclusion || check.conclusion === 'pending') { return ; } if (check.conclusion === 'success') { return ; } return ; }; const getCheckClassName = (check: CheckStatus) => { if (!check.conclusion || check.conclusion === 'pending') { return 'check-status check-pending'; } if (check.conclusion === 'success') { return 'check-status check-success'; } return 'check-status check-failure'; }; const getFilteredReviews = () => { if (prFilter === 'all') { return pendingReviews; } return pendingReviews.filter(review => review.status === prFilter); }; const handleLinkClick = (href: string) => (e: React.MouseEvent) => { e.preventDefault(); open(href).catch(err => { console.error('[UserDashboard] Failed to open link:', err); }); }; const MarkdownLink = ({ href, children }: { href?: string; children: React.ReactNode }) => { if (!href) return {children}; return ( {children} ); }; const toggleComments = (prNumber: number) => { const newExpanded = new Set(expandedComments); if (newExpanded.has(prNumber)) { newExpanded.delete(prNumber); } else { newExpanded.add(prNumber); } setExpandedComments(newExpanded); }; const getReviewStateLabel = (state: string) => { switch (state) { case 'APPROVED': return t('reviewApproved'); case 'CHANGES_REQUESTED': return t('reviewChangesRequested'); case 'COMMENTED': return t('reviewCommented'); default: return state; } }; const getReviewStateClass = (state: string) => { switch (state) { case 'APPROVED': return 'review-state-approved'; case 'CHANGES_REQUESTED': return 'review-state-changes'; case 'COMMENTED': return 'review-state-commented'; default: return ''; } }; const renderPublishedPlugins = () => { if (loading) { return
{t('loading')}
; } if (error) { return (

{t('loadError')}

{t('networkError')}

{error}

); } if (publishedPlugins.length === 0) { return
{t('noPublished')}
; } return (
{publishedPlugins.map((plugin) => { const isExpanded = expandedVersions.has(plugin.id); const hasMultipleVersions = plugin.versions.length > 1; const pendingPR = pendingReviews.find(pr => pr.pluginName === plugin.name && pr.status === 'open'); return (

{plugin.name}

{plugin.description}

{t('latestVersion')}: {plugin.latestVersion} {hasMultipleVersions && ( )} {t('category')}: {plugin.category} {t('publishedAt')}: {formatDate(plugin.versions[0]?.publishedAt || '')}
{/* 版本历史列表 */} {isExpanded && hasMultipleVersions && (

{t('allVersions')}

{plugin.versions.map((version) => ( ))}
)}
{pendingPR && (
PR #{pendingPR.prNumber} {t('statusOpen')}
)} {plugin.repositoryUrl && ( {t('viewRepo')} )} {plugin.versions[0]?.prUrl && ( {t('viewPR')} )}
); })}
); }; const renderPendingReviews = () => { if (loading) { return
{t('loading')}
; } if (error) { return (

{t('loadError')}

{t('networkError')}

{error}

); } const filteredReviews = getFilteredReviews(); if (filteredReviews.length === 0) { return
{t('noPending')}
; } return ( <>
{filteredReviews.map((review) => (

{review.pluginName} v{review.version}

{t(`status${review.status.charAt(0).toUpperCase()}${review.status.slice(1)}`)}
PR #{review.prNumber} {t('createdAt')}: {formatDate(review.createdAt)}
{review.hasConflicts && (
{t('conflictsDetected')}
{review.conflictFiles && review.conflictFiles.length > 0 && (
{t('conflictFilesLabel')}:
    {review.conflictFiles.map((file, index) => (
  • {file}
  • ))}
)}
)} {review.checks && review.checks.length > 0 && (
{t('ciChecks')}:
{review.checks.map((check, index) => (
{getCheckIcon(check)} {check.name} {check.detailsUrl && ( {t('viewDetails')} )}
))}
)} {((review.reviews && review.reviews.length > 0) || (review.comments && review.comments.length > 0)) && (
{expandedComments.has(review.prNumber) && (
{review.reviews && review.reviews.length > 0 && (
{t('reviews')}:
{review.reviews.map((reviewItem) => (
{reviewItem.user.login}
{reviewItem.user.login} {getReviewStateLabel(reviewItem.state)}
{formatDate(reviewItem.submitted_at)}
{reviewItem.body}
))}
)} {review.comments && review.comments.length > 0 && (
{t('comments')}:
{review.comments.map((comment) => (
{comment.user.login}
{comment.user.login} {formatDate(comment.created_at)}
{comment.body}
))}
)}
)}
)}
{t('viewPR')} {review.status === 'open' && ( )}
))}
); }; return (

{t('title')}

{user && (
{user.name}
{user.name || user.login}
@{user.login}
)}
{activeTab === 'published' ? renderPublishedPlugins() : renderPendingReviews()}
{confirmDelete && (

{t('confirmDeleteTitle')}

{t('confirmDeleteMessage')}

)} {confirmDeletePlugin && (

{t('confirmDeletePluginTitle')}

{t('confirmDeletePluginMessage').replace('{{name}}', confirmDeletePlugin.name)}