Files
cnb-center/src/pages/repos/components/RepoCard.tsx
xiongxiao 20edf1893e chore: update dependencies and improve configuration handling
- Bump version to 0.0.7 in package.json and update deployment script.
- Add new dependencies in bun.lock for improved functionality.
- Modify loadFromRemote methods in Gitea and general config stores to return boolean for better error handling.
- Enhance BuildConfig component to ensure proper loading state management.
- Simplify RepoCard component by removing duplicate repository access option.
- Update WorkspaceDetailDialog to include workspace link in state management.
- Fix branch default value in createDevConfig function to use a placeholder.
2026-03-01 01:34:12 +08:00

366 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Card } from '@/components/ui/card'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Star, GitFork, FileText, Edit, FolderGit2, MoreVertical, FileText as IssueIcon, Settings, Play, Trash2, RefreshCw, BookOpen, Copy, Clock, Info, Eye, Square, LinkIcon, ExternalLink, ArrowLeft } from 'lucide-react'
import { useRepoStore } from '../store'
import { useMemo, useState } from 'react'
import { useShallow } from 'zustand/shallow'
import { myOrgs } from '../store/build'
import { app, cnb } from '@/agents/app'
import { toast } from 'sonner'
import { useNavigate } from '@tanstack/react-router'
import clsx from 'clsx'
interface RepoCardProps {
repo: any
showReturn?: boolean
}
export function RepoCard({ showReturn = false, repo }: RepoCardProps) {
const [deletePopoverOpen, setDeletePopoverOpen] = useState(false)
const store = useRepoStore(useShallow((state) => ({
workspaceList: state.workspaceList,
getWorkspaceDetail: state.getWorkspaceDetail,
getList: state.getList,
buildUpdate: state.buildUpdate,
stopWorkspace: state.stopWorkspace,
setSelectedSyncRepo: state.setSelectedSyncRepo,
setSyncDialogOpen: state.setSyncDialogOpen,
startWorkspace: state.startWorkspace,
setEditRepo: state.setEditRepo,
setShowEditDialog: state.setShowEditDialog,
deleteItem: state.deleteItem,
})));
const workspace = useMemo(() => {
return store.workspaceList.find(ws => ws.slug === repo.path)
}, [store.workspaceList, repo.path])
const isWorkspaceActive = !!workspace
const owner = repo.path.split('/')[0]
const isMine = myOrgs.includes(owner)
const navigate = useNavigate();
const isKnowledge = repo?.flags === "KnowledgeBase"
const createKnow = async () => {
const res = await app.run({ path: 'cnb', key: 'build-knowledge-base', payload: { repo: repo.path } })
if (res.code === 200) {
toast.success("知识库创建中")
store.getList({}, true)
}
}
const onClone = async () => {
const url = `git clone https://cnb.cool/${repo.path}`
navigator.clipboard.writeText(url).then(() => {
toast.success('克隆地址已复制到剪贴板')
}).catch(() => {
toast.error('复制失败')
})
}
const onUpdate = async () => {
await store.buildUpdate({ path: repo.path });
}
const handleIssue = (repo: any) => {
window.open(`https://cnb.cool/${repo.path}/-/issues`)
}
const handleSettings = (repo: any) => {
window.open(`https://cnb.cool/${repo.path}/-/settings`)
}
const openInCNB = (isDetail = true) => {
if (!showReturn && isDetail) {
navigate({ to: `/repo?repo=${repo.path}` })
} else {
window.open(`https://cnb.cool/${repo.path}`, '_blank')
}
}
return (
<>
<Card className="relative p-0 overflow-hidden border border-neutral-200 bg-white hover:shadow-xl hover:border-neutral-300 transition-all duration-300 group pb-14">
<div className="p-6 space-y-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2 flex-1 min-w-0">
{showReturn && (
<button
onClick={() => navigate({ to: '/' })}
className="cursor-pointer flex items-center justify-center w-8 h-8 rounded-md hover:bg-neutral-100 transition-colors"
>
<ArrowLeft className="w-4 h-4 text-neutral-600" />
</button>
)}
<div
className="text-lg font-bold text-neutral-900 hover:text-neutral-600 transition-colors line-clamp-1 group-hover:underline"
onClick={() => {
openInCNB()
}}
>
{repo.path}
</div>
{isKnowledge && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<div className="shrink-0">
<BookOpen className="w-5 h-5 text-neutral-700" />
</div>
}
/>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{isWorkspaceActive && (
<span className="relative flex h-2.5 w-2.5 shrink-0">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500"></span>
</span>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
{isWorkspaceActive && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<Button
size="sm"
variant="outline"
onClick={() => {
store.stopWorkspace(workspace)
}}
className="h-8 w-8 p-0 border-neutral-200 hover:border-red-600 hover:bg-red-600 hover:text-white transition-all cursor-pointer"
>
<Square className="w-4 h-4" />
</Button>
}
/>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<Button
size="sm"
variant="outline"
onClick={() => {
if (!isWorkspaceActive) {
store.startWorkspace(repo)
} else {
store.getWorkspaceDetail(workspace)
}
}}
className="h-8 w-8 p-0 border-neutral-200 hover:border-neutral-900 hover:bg-neutral-900 hover:text-white transition-all cursor-pointer"
>
{isWorkspaceActive ? <Eye className="w-4 h-4" /> : <Play className="w-4 h-4" />}
</Button>
}
/>
<TooltipContent>
<p>{isWorkspaceActive ? '查看工作区' : '启动工作区'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
size="sm"
variant="outline"
className="h-8 w-8 p-0 border-neutral-200 hover:border-neutral-900 hover:bg-neutral-900 hover:text-white transition-all cursor-pointer"
>
<MoreVertical className="w-4 h-4" />
</Button>
}
/>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onClick={() => {
window.open(repo.web_url, '_blank')
}} className="cursor-pointer">
<ExternalLink className="w-4 h-4 mr-2" />
访
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {
store.setEditRepo(repo)
store.setShowEditDialog(true)
}} className="cursor-pointer">
<Edit className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {
createKnow()
}} className="cursor-pointer">
<BookOpen className="w-4 h-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {
onUpdate()
}} className="cursor-pointer">
<TooltipProvider>
<Tooltip>
<TooltipTrigger className={'flex gap-1 items-center'}>
<Clock className="w-4 h-4 mr-2" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
<p> commit </p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</DropdownMenuItem>
<DropdownMenuItem onClick={onClone} className="cursor-pointer">
<Copy className="w-4 h-4 mr-2" />
Clone URL
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleIssue(repo)} className="cursor-pointer">
<IssueIcon className="w-4 h-4 mr-2" />
访
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleSettings(repo)} className="cursor-pointer">
<Settings className="w-4 h-4 mr-2" />
访
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault()
setDeletePopoverOpen(true)
}}
className="cursor-pointer text-red-600 focus:text-red-600 focus:bg-red-50"
>
<Trash2 className="w-4 h-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Popover open={deletePopoverOpen} onOpenChange={setDeletePopoverOpen}>
<PopoverTrigger >
<div style={{ display: 'none' }} />
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-4">
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
<p className="text-sm text-neutral-500">
<span className="font-semibold text-neutral-900">{repo.path}</span>
</p>
</div>
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setDeletePopoverOpen(false)}
>
</Button>
<Button
size="sm"
variant="outline"
className="bg-red-600 text-white border-red-600 hover:bg-red-700 hover:border-red-700"
onClick={() => {
if (repo.path)
store.deleteItem(repo.path)
setDeletePopoverOpen(false)
}}
>
</Button>
</div>
</div>
</PopoverContent>
</Popover>
</div>
</div>
<div className="flex flex-wrap gap-2">
{repo.topics && (<>
{
repo.topics.split(',').map((topic: string, idx: number) => (
<Badge key={idx} variant="outline" className="text-xs border-neutral-300 text-neutral-700 hover:bg-neutral-100 transition-colors">
{topic.trim()}
</Badge>
))
}
</>
)}
<Badge variant="outline" className="text-xs border-neutral-300 text-neutral-700 hover:bg-neutral-100 transition-colors">{repo.visibility_level}</Badge>
</div>
<div className={clsx(!showReturn && "cursor-pointer")} onClick={() => {
{ !showReturn && openInCNB(false) }
}}>
{repo.site && (
<div
className="text-xs text-neutral-500 hover:text-neutral-900 hover:underline flex transition-colors"
onClick={() => {
window.open(repo.site, '_blank')
}}
>
<LinkIcon className="w-4 h-4 shrink-0 mr-2" />
<div className='truncate grow'>
{repo.site}
</div>
</div>
)}
{repo.description && (
<p className="ml-2 text-sm text-neutral-600 line-clamp-2 min-h-10 grow">
{repo.description}
</p>
)}
</div>
<div className="absolute bottom-0 left-0 right-0 flex items-center gap-4 text-xs text-neutral-500 px-6 py-3 border-t border-neutral-100 bg-neutral-50">
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
<Star className="w-3.5 h-3.5" />
<span className="font-medium">{repo.star_count}</span>
</span>
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
<GitFork className="w-3.5 h-3.5" />
<span className="font-medium">{repo.fork_count}</span>
</span>
<span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors">
<FileText className="w-3.5 h-3.5" />
<span className="font-medium">{repo.open_issue_count}</span>
</span>
{isWorkspaceActive && <span className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors cursor-pointer"
onClick={() => {
store.getWorkspaceDetail(workspace)
}}>
<Play className="w-3.5 h-3.5" />
<span className="font-medium"></span>
</span>}
{isMine && (
<span
className="flex items-center gap-1.5 hover:text-neutral-900 transition-colors cursor-pointer"
onClick={() => {
store.setSelectedSyncRepo(repo)
store.setSyncDialogOpen(true)
}}
>
<RefreshCw className="w-3.5 h-3.5" />
<span className="font-medium"></span>
</span>
)}
</div>
</div>
</Card>
</>
)
}