|
|
|
@@ -1,92 +1,530 @@
|
|
|
|
|
import { useState } from 'react'
|
|
|
|
|
import { useParams, Link, useNavigate } from 'react-router-dom'
|
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
|
import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom'
|
|
|
|
|
import { useRepo, useUpdateRepo, useDeleteRepo } from '../api/queries/repos'
|
|
|
|
|
import { Skeleton } from '../ui/Skeleton'
|
|
|
|
|
|
|
|
|
|
// ─── Sidebar definition ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
type SectionId =
|
|
|
|
|
| 'repository-details'
|
|
|
|
|
| 'repository-permissions'
|
|
|
|
|
| 'access-keys'
|
|
|
|
|
| 'access-tokens'
|
|
|
|
|
| 'branch-restrictions'
|
|
|
|
|
| 'branching-model'
|
|
|
|
|
| 'merge-strategies'
|
|
|
|
|
| 'webhooks'
|
|
|
|
|
| 'default-reviewers'
|
|
|
|
|
| 'default-description'
|
|
|
|
|
| 'excluded-files'
|
|
|
|
|
| 'git-lfs'
|
|
|
|
|
|
|
|
|
|
const SIDEBAR: { group: string; items: { id: SectionId; label: string; badge?: string }[] }[] = [
|
|
|
|
|
{
|
|
|
|
|
group: 'General',
|
|
|
|
|
items: [
|
|
|
|
|
{ id: 'repository-details', label: 'Repository details' },
|
|
|
|
|
{ id: 'repository-permissions', label: 'Repository permissions' },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
group: 'Security',
|
|
|
|
|
items: [
|
|
|
|
|
{ id: 'access-keys', label: 'Access keys' },
|
|
|
|
|
{ id: 'access-tokens', label: 'Access tokens' },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
group: 'Workflow',
|
|
|
|
|
items: [
|
|
|
|
|
{ id: 'branch-restrictions', label: 'Branch restrictions' },
|
|
|
|
|
{ id: 'branching-model', label: 'Branching model' },
|
|
|
|
|
{ id: 'merge-strategies', label: 'Merge strategies' },
|
|
|
|
|
{ id: 'webhooks', label: 'Webhooks' },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
group: 'Pull Requests',
|
|
|
|
|
items: [
|
|
|
|
|
{ id: 'default-reviewers', label: 'Default reviewers' },
|
|
|
|
|
{ id: 'default-description', label: 'Default description' },
|
|
|
|
|
{ id: 'excluded-files', label: 'Excluded files' },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
group: 'Features',
|
|
|
|
|
items: [
|
|
|
|
|
{ id: 'git-lfs', label: 'Git LFS' },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const SECTION_META: Record<SectionId, { title: string; description: string }> = {
|
|
|
|
|
'repository-details': { title: 'Repository details', description: '' },
|
|
|
|
|
'repository-permissions': {
|
|
|
|
|
title: 'Repository permissions',
|
|
|
|
|
description: 'Control who can read, write, or administer this repository. Invite collaborators and manage team access.',
|
|
|
|
|
},
|
|
|
|
|
'access-keys': {
|
|
|
|
|
title: 'Access keys',
|
|
|
|
|
description: 'SSH deploy keys give read or write access to this repository without requiring an interactive user account. Useful for CI/CD systems.',
|
|
|
|
|
},
|
|
|
|
|
'access-tokens': {
|
|
|
|
|
title: 'Access tokens',
|
|
|
|
|
description: 'Repository access tokens give third-party tools secure, scoped access to this repository via the API.',
|
|
|
|
|
},
|
|
|
|
|
'branch-restrictions': {
|
|
|
|
|
title: 'Branch restrictions',
|
|
|
|
|
description: 'Protect important branches by restricting who can push, delete, or force-push to them. Define which users or groups can merge pull requests.',
|
|
|
|
|
},
|
|
|
|
|
'branching-model': {
|
|
|
|
|
title: 'Branching model',
|
|
|
|
|
description: 'Define a branching strategy (e.g. Gitflow) to enforce consistent branch naming across your team. Automatically categorise branches.',
|
|
|
|
|
},
|
|
|
|
|
'merge-strategies': {
|
|
|
|
|
title: 'Merge strategies',
|
|
|
|
|
description: 'Configure which merge strategies (merge commit, squash, rebase) are allowed when closing pull requests.',
|
|
|
|
|
},
|
|
|
|
|
'webhooks': {
|
|
|
|
|
title: 'Webhooks',
|
|
|
|
|
description: 'Send real-time HTTP POST notifications to external services when events occur in this repository — push, PR creation, comments, and more.',
|
|
|
|
|
},
|
|
|
|
|
'default-reviewers': {
|
|
|
|
|
title: 'Default reviewers',
|
|
|
|
|
description: 'Automatically add reviewers to pull requests based on the files changed or the target branch.',
|
|
|
|
|
},
|
|
|
|
|
'default-description': {
|
|
|
|
|
title: 'Default description',
|
|
|
|
|
description: 'Set a default description template that pre-fills the body field when a pull request is created.',
|
|
|
|
|
},
|
|
|
|
|
'excluded-files': {
|
|
|
|
|
title: 'Excluded files',
|
|
|
|
|
description: 'Specify files that should be excluded from pull request diff views and review requirements.',
|
|
|
|
|
},
|
|
|
|
|
'git-lfs': {
|
|
|
|
|
title: 'Git LFS',
|
|
|
|
|
description: 'Git Large File Storage replaces large files with text pointers inside git, while storing the actual files on a remote server.',
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Main page ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export default function RepoSettingsPage() {
|
|
|
|
|
const { owner = '', repo: repoName = '' } = useParams<{ owner: string; repo: string }>()
|
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
const { data: repo } = useRepo(owner, repoName)
|
|
|
|
|
const updateRepo = useUpdateRepo(owner, repoName)
|
|
|
|
|
const deleteRepo = useDeleteRepo(owner, repoName)
|
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
|
|
|
const [sidebarSearch, setSidebarSearch] = useState('')
|
|
|
|
|
const section = (searchParams.get('section') ?? 'repository-details') as SectionId
|
|
|
|
|
|
|
|
|
|
const [description, setDescription] = useState(repo?.description ?? '')
|
|
|
|
|
const [isPrivate, setIsPrivate] = useState(repo?.isPrivate ?? false)
|
|
|
|
|
function goTo(id: SectionId) {
|
|
|
|
|
setSearchParams({ section: id })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const filtered = sidebarSearch.trim()
|
|
|
|
|
? SIDEBAR.map(g => ({
|
|
|
|
|
...g,
|
|
|
|
|
items: g.items.filter(i => i.label.toLowerCase().includes(sidebarSearch.toLowerCase())),
|
|
|
|
|
})).filter(g => g.items.length > 0)
|
|
|
|
|
: SIDEBAR
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full overflow-hidden">
|
|
|
|
|
{/* ── Left sidebar ── */}
|
|
|
|
|
<aside className="w-60 shrink-0 border-r border-[#DFE1E6] bg-white hidden md:flex flex-col overflow-hidden">
|
|
|
|
|
{/* Top: back + search */}
|
|
|
|
|
<div className="p-3 border-b border-[#DFE1E6] shrink-0 space-y-2.5">
|
|
|
|
|
<Link
|
|
|
|
|
to={`/repos/${owner}/${repoName}`}
|
|
|
|
|
className="flex items-center gap-1.5 text-sm text-[#172B4D] hover:text-[#0052CC] font-medium"
|
|
|
|
|
>
|
|
|
|
|
<svg width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
|
|
|
|
</svg>
|
|
|
|
|
Repository settings
|
|
|
|
|
</Link>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<svg className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#5E6C84] pointer-events-none"
|
|
|
|
|
width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<input
|
|
|
|
|
value={sidebarSearch}
|
|
|
|
|
onChange={e => setSidebarSearch(e.target.value)}
|
|
|
|
|
placeholder="Jump to settings…"
|
|
|
|
|
className="w-full text-xs bg-[#F4F5F7] border border-[#DFE1E6] rounded pl-7 pr-3 py-1.5 focus:outline-none focus:border-[#4C9AFF]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Nav items */}
|
|
|
|
|
<nav className="flex-1 overflow-y-auto py-2">
|
|
|
|
|
{filtered.map(group => (
|
|
|
|
|
<div key={group.group} className="mb-1">
|
|
|
|
|
<p className="text-[10px] font-semibold text-[#5E6C84] uppercase tracking-wider px-4 py-1.5">
|
|
|
|
|
{group.group}
|
|
|
|
|
</p>
|
|
|
|
|
{group.items.map(item => (
|
|
|
|
|
<button
|
|
|
|
|
key={item.id}
|
|
|
|
|
onClick={() => goTo(item.id)}
|
|
|
|
|
className={`w-full text-left px-4 py-2 text-sm transition-colors flex items-center justify-between ${
|
|
|
|
|
section === item.id
|
|
|
|
|
? 'bg-[#DEEBFF] text-[#0052CC] font-medium border-l-[3px] border-[#0052CC]'
|
|
|
|
|
: 'text-[#172B4D] hover:bg-[#F4F5F7] border-l-[3px] border-transparent'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{item.label}
|
|
|
|
|
{item.badge && (
|
|
|
|
|
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-[#DFE1E6] text-[#5E6C84]">
|
|
|
|
|
{item.badge}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</nav>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
{/* ── Main content ── */}
|
|
|
|
|
<main className="flex-1 overflow-y-auto min-w-0">
|
|
|
|
|
{/* Mobile section selector */}
|
|
|
|
|
<div className="md:hidden p-3 border-b border-[#DFE1E6] bg-white">
|
|
|
|
|
<select
|
|
|
|
|
value={section}
|
|
|
|
|
onChange={e => goTo(e.target.value as SectionId)}
|
|
|
|
|
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none"
|
|
|
|
|
>
|
|
|
|
|
{SIDEBAR.map(g =>
|
|
|
|
|
g.items.map(i => (
|
|
|
|
|
<option key={i.id} value={i.id}>{g.group} → {i.label}</option>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{section === 'repository-details'
|
|
|
|
|
? <RepositoryDetailsSection owner={owner} repo={repoName} />
|
|
|
|
|
: <ComingSoon sectionId={section} />
|
|
|
|
|
}
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Repository details section ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function formatSize(bytes: number): string {
|
|
|
|
|
if (bytes === 0) return '0 B'
|
|
|
|
|
if (bytes < 1024) return `${bytes} B`
|
|
|
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
|
|
|
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`
|
|
|
|
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function RepositoryDetailsSection({ owner, repo }: { owner: string; repo: string }) {
|
|
|
|
|
const { data: repoData, isLoading } = useRepo(owner, repo)
|
|
|
|
|
const updateRepo = useUpdateRepo(owner, repo)
|
|
|
|
|
const deleteRepo = useDeleteRepo(owner, repo)
|
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
|
|
|
|
|
const [description, setDescription] = useState('')
|
|
|
|
|
const [isPrivate, setIsPrivate] = useState(false)
|
|
|
|
|
const [defaultBranch, setDefaultBranch] = useState('')
|
|
|
|
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
|
|
|
const [confirmDelete, setConfirmDelete] = useState('')
|
|
|
|
|
const [saved, setSaved] = useState(false)
|
|
|
|
|
|
|
|
|
|
const handleSave = async (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
await updateRepo.mutateAsync({ description, isPrivate })
|
|
|
|
|
setSaved(true)
|
|
|
|
|
setTimeout(() => setSaved(false), 2000)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (repoData) {
|
|
|
|
|
setDescription(repoData.description ?? '')
|
|
|
|
|
setIsPrivate(repoData.isPrivate)
|
|
|
|
|
setDefaultBranch(repoData.defaultBranch)
|
|
|
|
|
}
|
|
|
|
|
}, [repoData])
|
|
|
|
|
|
|
|
|
|
const isDirty = repoData != null && (
|
|
|
|
|
description !== (repoData.description ?? '') ||
|
|
|
|
|
isPrivate !== repoData.isPrivate ||
|
|
|
|
|
defaultBranch !== repoData.defaultBranch
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
function handleDiscard() {
|
|
|
|
|
if (!repoData) return
|
|
|
|
|
setDescription(repoData.description ?? '')
|
|
|
|
|
setIsPrivate(repoData.isPrivate)
|
|
|
|
|
setDefaultBranch(repoData.defaultBranch)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDelete = async () => {
|
|
|
|
|
if (confirmDelete !== repoName) return
|
|
|
|
|
async function handleSave(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
await updateRepo.mutateAsync({ description, isPrivate, defaultBranch })
|
|
|
|
|
setSaved(true)
|
|
|
|
|
setTimeout(() => setSaved(false), 3000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleDelete() {
|
|
|
|
|
if (confirmDelete !== repo || !repoData) return
|
|
|
|
|
await deleteRepo.mutateAsync()
|
|
|
|
|
navigate('/repos')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isLoading || !repoData) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="max-w-2xl mx-auto px-4 md:px-6 py-6 space-y-8">
|
|
|
|
|
<div className="flex items-center gap-1 text-sm">
|
|
|
|
|
<Link to={`/repos/${owner}/${repoName}`} className="text-[#0052CC] hover:underline">{repoName}</Link>
|
|
|
|
|
<span className="text-[#5E6C84]">/</span>
|
|
|
|
|
<span className="font-semibold text-[#172B4D]">Settings</span>
|
|
|
|
|
<div className="max-w-2xl px-6 py-6 space-y-4">
|
|
|
|
|
<Skeleton className="h-8 w-48" />
|
|
|
|
|
<Skeleton className="h-5 w-full" />
|
|
|
|
|
<Skeleton className="h-24 w-full" />
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initials avatar colour based on repo name
|
|
|
|
|
const avatarColor = '#0052CC'
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="max-w-2xl px-6 py-6 space-y-6">
|
|
|
|
|
|
|
|
|
|
{/* Page header */}
|
|
|
|
|
<div className="flex items-start justify-between gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center gap-1 text-xs text-[#5E6C84] mb-1.5">
|
|
|
|
|
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[#0052CC]">{owner}</Link>
|
|
|
|
|
<span>/</span>
|
|
|
|
|
<Link to={`/repos/${owner}/${repo}`} className="hover:text-[#0052CC]">{repo}</Link>
|
|
|
|
|
</div>
|
|
|
|
|
<h1 className="text-xl font-semibold text-[#172B4D]">Repository details</h1>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<h1 className="text-xl font-semibold text-[#172B4D]">Repository Settings</h1>
|
|
|
|
|
|
|
|
|
|
<section className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
|
|
|
|
<div className="px-5 py-4 border-b border-[#DFE1E6] bg-[#FAFBFC]">
|
|
|
|
|
<h2 className="text-sm font-semibold text-[#172B4D]">General</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<form onSubmit={handleSave} className="px-5 py-5 space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Repository name</label>
|
|
|
|
|
<input value={repoName} disabled
|
|
|
|
|
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm bg-[#F4F5F7] text-[#5E6C84] cursor-not-allowed" />
|
|
|
|
|
<p className="text-xs text-[#5E6C84] mt-1">Renaming requires migrating git remotes — coming soon.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-xs font-semibold text-[#172B4D] mb-1">Description</label>
|
|
|
|
|
<input value={description} onChange={e => setDescription(e.target.value)}
|
|
|
|
|
placeholder="Short description of this repository"
|
|
|
|
|
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]" />
|
|
|
|
|
</div>
|
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
<input type="checkbox" checked={isPrivate} onChange={e => setIsPrivate(e.target.checked)} />
|
|
|
|
|
<span className="text-sm text-[#172B4D]">Private repository</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<button type="submit" disabled={updateRepo.isPending}
|
|
|
|
|
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[44px]">
|
|
|
|
|
{updateRepo.isPending ? 'Saving…' : 'Save changes'}
|
|
|
|
|
<div className="relative shrink-0">
|
|
|
|
|
<button className="flex items-center gap-1.5 px-3 py-1.5 border border-[#DFE1E6] rounded text-sm text-[#172B4D] hover:bg-[#F4F5F7] font-medium">
|
|
|
|
|
Manage repository
|
|
|
|
|
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
{saved && <span className="text-xs text-[#00875A] font-medium">Saved!</span>}
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<section className="border border-[#FFEBE6] rounded-lg overflow-hidden">
|
|
|
|
|
<div className="px-5 py-4 border-b border-[#FFEBE6] bg-[#FFEBE6]/50">
|
|
|
|
|
<h2 className="text-sm font-semibold text-[#BF2600]">Danger zone</h2>
|
|
|
|
|
<div className="border-t border-[#DFE1E6]" />
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSave} className="space-y-6">
|
|
|
|
|
|
|
|
|
|
{/* Avatar */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[#172B4D] mb-3">Avatar</label>
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<div
|
|
|
|
|
className="w-12 h-12 rounded-lg flex items-center justify-center text-white text-xl font-bold"
|
|
|
|
|
style={{ backgroundColor: avatarColor }}
|
|
|
|
|
>
|
|
|
|
|
{repoData.name[0]?.toUpperCase()}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="px-5 py-5 space-y-3">
|
|
|
|
|
<p className="text-sm text-[#172B4D] font-medium">Delete this repository</p>
|
|
|
|
|
<p className="text-xs text-[#5E6C84]">
|
|
|
|
|
This action is permanent. Type <code className="font-mono bg-[#F4F5F7] px-1 rounded">{repoName}</code> to confirm.
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="px-3 py-1.5 text-sm border border-[#DFE1E6] rounded text-[#172B4D] hover:bg-[#F4F5F7]"
|
|
|
|
|
>
|
|
|
|
|
Change avatar
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Repository name (read-only) */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[#172B4D] mb-1">
|
|
|
|
|
Repository name <span className="text-[#DE350B]">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
value={repoData.name}
|
|
|
|
|
disabled
|
|
|
|
|
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm bg-[#F4F5F7] text-[#5E6C84] cursor-not-allowed"
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xs text-[#5E6C84] mt-1">
|
|
|
|
|
Renaming would require all collaborators to update their git remotes. Coming soon.
|
|
|
|
|
</p>
|
|
|
|
|
<input value={confirmDelete} onChange={e => setConfirmDelete(e.target.value)}
|
|
|
|
|
placeholder={repoName}
|
|
|
|
|
className="w-full border border-[#DE350B] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#DE350B]" />
|
|
|
|
|
<button onClick={handleDelete}
|
|
|
|
|
disabled={confirmDelete !== repoName || deleteRepo.isPending}
|
|
|
|
|
className="px-4 py-2 rounded bg-[#DE350B] text-white text-sm font-medium hover:bg-[#BF2600] disabled:opacity-40 min-h-[44px]">
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Size (read-only) */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[#172B4D] mb-1">Size</label>
|
|
|
|
|
<p className="text-sm text-[#172B4D]">{formatSize(repoData.size)}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Description */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[#172B4D] mb-1">Description</label>
|
|
|
|
|
<textarea
|
|
|
|
|
value={description}
|
|
|
|
|
onChange={e => setDescription(e.target.value)}
|
|
|
|
|
rows={4}
|
|
|
|
|
placeholder="Describe this repository…"
|
|
|
|
|
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF] resize-y"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Access level */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[#172B4D] mb-2">Access level</label>
|
|
|
|
|
<label className="flex items-start gap-2.5 cursor-pointer">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={isPrivate}
|
|
|
|
|
onChange={e => setIsPrivate(e.target.checked)}
|
|
|
|
|
className="mt-0.5 w-4 h-4 accent-[#0052CC]"
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<span className="text-sm text-[#172B4D]">
|
|
|
|
|
{isPrivate ? '✓ This is a private repository' : 'Make this repository private'}
|
|
|
|
|
</span>
|
|
|
|
|
<p className="text-xs text-[#5E6C84] mt-0.5">
|
|
|
|
|
{isPrivate
|
|
|
|
|
? 'Only collaborators you invite can see and push to this repository.'
|
|
|
|
|
: 'Anyone can view this repository. Make it private to restrict access.'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Advanced (collapsible) */}
|
|
|
|
|
<div className="border border-[#DFE1E6] rounded-lg overflow-hidden">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setShowAdvanced(s => !s)}
|
|
|
|
|
className="w-full flex items-center gap-2 px-4 py-3 text-sm font-medium text-[#172B4D] hover:bg-[#F4F5F7] text-left"
|
|
|
|
|
>
|
|
|
|
|
<svg
|
|
|
|
|
width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"
|
|
|
|
|
className={`transition-transform shrink-0 ${showAdvanced ? 'rotate-90' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
|
|
|
|
</svg>
|
|
|
|
|
Advanced
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{showAdvanced && (
|
|
|
|
|
<div className="border-t border-[#DFE1E6] px-4 py-5 space-y-6 bg-[#FAFBFC]">
|
|
|
|
|
|
|
|
|
|
{/* Default branch */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[#172B4D] mb-1">Default branch</label>
|
|
|
|
|
<input
|
|
|
|
|
value={defaultBranch}
|
|
|
|
|
onChange={e => setDefaultBranch(e.target.value)}
|
|
|
|
|
className="w-full max-w-xs border border-[#DFE1E6] bg-white rounded px-3 py-2 text-sm focus:outline-none focus:border-[#4C9AFF]"
|
|
|
|
|
placeholder="main"
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xs text-[#5E6C84] mt-1">Used as the base branch for new pull requests.</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Danger zone */}
|
|
|
|
|
<div className="border border-[#FFEBE6] rounded-lg overflow-hidden">
|
|
|
|
|
<div className="px-4 py-3 bg-[#FFEBE6]/50 border-b border-[#FFEBE6]">
|
|
|
|
|
<h3 className="text-sm font-semibold text-[#BF2600]">Delete repository</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="px-4 py-4 space-y-3 bg-white">
|
|
|
|
|
<p className="text-sm text-[#172B4D]">
|
|
|
|
|
This is permanent and cannot be undone. All commits, branches, pull requests, issues, and settings will be lost.
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-[#5E6C84]">
|
|
|
|
|
Type <code className="font-mono bg-[#F4F5F7] border border-[#DFE1E6] px-1.5 py-0.5 rounded">{repo}</code> to confirm.
|
|
|
|
|
</p>
|
|
|
|
|
<input
|
|
|
|
|
value={confirmDelete}
|
|
|
|
|
onChange={e => setConfirmDelete(e.target.value)}
|
|
|
|
|
placeholder={repo}
|
|
|
|
|
className="w-full border border-[#DFE1E6] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#DE350B]"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleDelete}
|
|
|
|
|
disabled={confirmDelete !== repo || deleteRepo.isPending}
|
|
|
|
|
className="px-4 py-2 rounded bg-[#DE350B] text-white text-sm font-medium hover:bg-[#BF2600] disabled:opacity-40"
|
|
|
|
|
>
|
|
|
|
|
{deleteRepo.isPending ? 'Deleting…' : 'Delete repository'}
|
|
|
|
|
</button>
|
|
|
|
|
{deleteRepo.isError && (
|
|
|
|
|
<p className="text-xs text-[#DE350B]">{(deleteRepo.error as Error).message}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
<div className="flex items-center justify-end gap-3 pt-2 border-t border-[#DFE1E6]">
|
|
|
|
|
{updateRepo.isError && (
|
|
|
|
|
<p className="text-xs text-[#DE350B] mr-auto">{(updateRepo.error as Error).message}</p>
|
|
|
|
|
)}
|
|
|
|
|
{saved && (
|
|
|
|
|
<span className="text-xs text-[#00875A] font-medium mr-auto flex items-center gap-1">
|
|
|
|
|
<svg width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
|
|
|
|
</svg>
|
|
|
|
|
Changes saved
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleDiscard}
|
|
|
|
|
disabled={!isDirty}
|
|
|
|
|
className="px-4 py-2 rounded border border-[#DFE1E6] text-sm text-[#172B4D] hover:bg-[#F4F5F7] disabled:opacity-40 min-h-[36px]"
|
|
|
|
|
>
|
|
|
|
|
Discard
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={updateRepo.isPending || !isDirty}
|
|
|
|
|
className="px-4 py-2 rounded bg-[#0052CC] text-white text-sm font-medium hover:bg-[#0065FF] disabled:opacity-50 min-h-[36px]"
|
|
|
|
|
>
|
|
|
|
|
{updateRepo.isPending ? 'Saving…' : 'Save changes'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Coming soon section ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const COMING_SOON_ICONS: Partial<Record<SectionId, React.ReactNode>> = {
|
|
|
|
|
'access-keys': (
|
|
|
|
|
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 0 1 21.75 8.25Z" />
|
|
|
|
|
</svg>
|
|
|
|
|
),
|
|
|
|
|
'webhooks': (
|
|
|
|
|
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
|
|
|
|
|
</svg>
|
|
|
|
|
),
|
|
|
|
|
'branch-restrictions': (
|
|
|
|
|
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
|
|
|
|
|
</svg>
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ComingSoon({ sectionId }: { sectionId: SectionId }) {
|
|
|
|
|
const meta = SECTION_META[sectionId]
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="max-w-2xl px-6 py-6">
|
|
|
|
|
<h1 className="text-xl font-semibold text-[#172B4D] mb-1">{meta.title}</h1>
|
|
|
|
|
|
|
|
|
|
<div className="mt-8 flex flex-col items-center justify-center py-16 border border-dashed border-[#DFE1E6] rounded-lg text-center bg-[#FAFBFC]">
|
|
|
|
|
<div className="mb-4 text-[#97A0AF]">
|
|
|
|
|
{COMING_SOON_ICONS[sectionId] ?? (
|
|
|
|
|
<svg width="40" height="40" fill="none" stroke="#97A0AF" strokeWidth="1.2" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />
|
|
|
|
|
</svg>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-base font-semibold text-[#172B4D] mb-2">{meta.title}</h2>
|
|
|
|
|
<p className="text-sm text-[#5E6C84] max-w-sm leading-relaxed">{meta.description}</p>
|
|
|
|
|
<span className="mt-5 inline-block text-[10px] font-semibold uppercase tracking-wider text-white bg-[#97A0AF] px-2.5 py-1 rounded-full">
|
|
|
|
|
Coming soon
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|