initial completion
This commit is contained in:
@@ -9,6 +9,11 @@ import {
|
||||
useMergeStrategies, useUpdateMergeStrategies,
|
||||
useWebhooks, useCreateWebhook, useUpdateWebhook, useDeleteWebhook, useTestWebhook,
|
||||
} from '../api/queries/workflow'
|
||||
import {
|
||||
useDefaultReviewers, useAddDefaultReviewer, useRemoveDefaultReviewer,
|
||||
useDefaultDescription, useUpdateDefaultDescription,
|
||||
useExcludedFiles, useUpdateExcludedFiles,
|
||||
} from '../api/queries/prs'
|
||||
import { useRecentRepos } from '../hooks/useRecentRepos'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { Skeleton } from '../ui/Skeleton'
|
||||
@@ -145,8 +150,12 @@ export default function RepoSettingsPage() {
|
||||
{section === 'branching-model' && <BranchingModelSection owner={owner} repo={repoName} />}
|
||||
{section === 'merge-strategies' && <MergeStrategiesSection owner={owner} repo={repoName} />}
|
||||
{section === 'webhooks' && <WebhooksSection owner={owner} repo={repoName} />}
|
||||
{section === 'default-reviewers' && <DefaultReviewersSection owner={owner} repo={repoName} />}
|
||||
{section === 'default-description' && <DefaultDescriptionSection owner={owner} repo={repoName} />}
|
||||
{section === 'excluded-files' && <ExcludedFilesSection owner={owner} repo={repoName} />}
|
||||
{!['repository-details','repository-permissions','access-keys','access-tokens',
|
||||
'branch-restrictions','branching-model','merge-strategies','webhooks'].includes(section) && <ComingSoon sectionId={section} />}
|
||||
'branch-restrictions','branching-model','merge-strategies','webhooks',
|
||||
'default-reviewers','default-description','excluded-files'].includes(section) && <ComingSoon sectionId={section} />}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
@@ -1476,6 +1485,201 @@ function WebhooksSection({ owner, repo }: { owner: string; repo: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Default reviewers ────────────────────────────────────────────────────────
|
||||
|
||||
function DefaultReviewersSection({ owner, repo }: { owner: string; repo: string }) {
|
||||
const { data: reviewers = [], isLoading } = useDefaultReviewers(owner, repo)
|
||||
const addReviewer = useAddDefaultReviewer(owner, repo)
|
||||
const removeReviewer = useRemoveDefaultReviewer(owner, repo)
|
||||
const [username, setUsername] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
async function handleAdd(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
const u = username.trim()
|
||||
if (!u) return
|
||||
try {
|
||||
await addReviewer.mutateAsync(u)
|
||||
setUsername('')
|
||||
} catch (err: any) {
|
||||
setError(err.message ?? 'Failed to add reviewer')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl px-6 py-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Default reviewers</h1>
|
||||
<p className="text-sm text-[var(--c-muted)] mt-1">These users are automatically added as reviewers whenever a pull request is opened in this repository.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAdd} className="flex gap-2">
|
||||
<input
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
placeholder="Username"
|
||||
className="flex-1 border border-[var(--c-border)] rounded px-3 py-2 text-sm bg-[var(--c-surface)] text-[var(--c-text)] focus:outline-none focus:border-[var(--c-brand-focus)]"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={addReviewer.isPending || !username.trim()}
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] disabled:opacity-50"
|
||||
>
|
||||
{addReviewer.isPending ? 'Adding…' : 'Add reviewer'}
|
||||
</button>
|
||||
</form>
|
||||
{error && <p className="text-xs text-[var(--c-danger)]">{error}</p>}
|
||||
|
||||
<div className="border border-[var(--c-border)] rounded-lg overflow-hidden">
|
||||
{isLoading ? (
|
||||
<div className="p-4 space-y-3">
|
||||
{[0,1,2].map(i => <Skeleton key={i} className="h-10 rounded" />)}
|
||||
</div>
|
||||
) : reviewers.length === 0 ? (
|
||||
<div className="p-8 text-center text-sm text-[var(--c-muted)]">No default reviewers configured.</div>
|
||||
) : (
|
||||
<ul className="divide-y divide-[var(--c-border)]">
|
||||
{reviewers.map(rv => (
|
||||
<li key={rv.userId} className="flex items-center justify-between px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{rv.avatarUrl ? (
|
||||
<img src={rv.avatarUrl} alt={rv.username} className="w-8 h-8 rounded-full object-cover" />
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-[var(--c-brand)] flex items-center justify-center text-white text-xs font-semibold">
|
||||
{rv.username.slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium text-[var(--c-text)]">{rv.username}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeReviewer.mutate(rv.username)}
|
||||
disabled={removeReviewer.isPending}
|
||||
className="text-xs text-[var(--c-danger)] hover:underline disabled:opacity-50"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Default description ──────────────────────────────────────────────────────
|
||||
|
||||
function DefaultDescriptionSection({ owner, repo }: { owner: string; repo: string }) {
|
||||
const { data, isLoading } = useDefaultDescription(owner, repo)
|
||||
const updateDesc = useUpdateDefaultDescription(owner, repo)
|
||||
const [template, setTemplate] = useState('')
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) setTemplate(data.template ?? '')
|
||||
}, [data])
|
||||
|
||||
async function handleSave(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
await updateDesc.mutateAsync(template)
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl px-6 py-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Default description</h1>
|
||||
<p className="text-sm text-[var(--c-muted)] mt-1">This template pre-fills the pull request body when a new PR is opened. Supports Markdown.</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-48 rounded" />
|
||||
) : (
|
||||
<form onSubmit={handleSave} className="space-y-4">
|
||||
<textarea
|
||||
value={template}
|
||||
onChange={e => setTemplate(e.target.value)}
|
||||
rows={12}
|
||||
placeholder={'## Summary\n\n## Test plan\n\n## Screenshots'}
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2.5 text-sm font-mono bg-[var(--c-surface)] text-[var(--c-text)] focus:outline-none focus:border-[var(--c-brand-focus)] resize-y"
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={updateDesc.isPending}
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] disabled:opacity-50"
|
||||
>
|
||||
{updateDesc.isPending ? 'Saving…' : 'Save template'}
|
||||
</button>
|
||||
{saved && <span className="text-xs text-[var(--c-success)]">Saved</span>}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Excluded files ───────────────────────────────────────────────────────────
|
||||
|
||||
function ExcludedFilesSection({ owner, repo }: { owner: string; repo: string }) {
|
||||
const { data, isLoading } = useExcludedFiles(owner, repo)
|
||||
const updateFiles = useUpdateExcludedFiles(owner, repo)
|
||||
const [patterns, setPatterns] = useState('')
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) setPatterns(data.patterns ?? '')
|
||||
}, [data])
|
||||
|
||||
async function handleSave(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
await updateFiles.mutateAsync(patterns)
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl px-6 py-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-[var(--c-text)]">Excluded files</h1>
|
||||
<p className="text-sm text-[var(--c-muted)] mt-1">
|
||||
Files matching these glob patterns are excluded from pull request diff views. One pattern per line.
|
||||
Example: <code className="text-xs bg-[var(--c-surface-muted)] px-1 py-0.5 rounded font-mono">package-lock.json</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-48 rounded" />
|
||||
) : (
|
||||
<form onSubmit={handleSave} className="space-y-4">
|
||||
<textarea
|
||||
value={patterns}
|
||||
onChange={e => setPatterns(e.target.value)}
|
||||
rows={10}
|
||||
placeholder={'package-lock.json\nyarn.lock\ndist/**\n*.min.js'}
|
||||
className="w-full border border-[var(--c-border)] rounded px-3 py-2.5 text-sm font-mono bg-[var(--c-surface)] text-[var(--c-text)] focus:outline-none focus:border-[var(--c-brand-focus)] resize-y"
|
||||
/>
|
||||
<div className="text-xs text-[var(--c-muted)]">
|
||||
Patterns use glob syntax. <code className="font-mono">*</code> matches any file, <code className="font-mono">**</code> matches any path segment.
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={updateFiles.isPending}
|
||||
className="px-4 py-2 rounded bg-[var(--c-brand)] text-white text-sm font-medium hover:bg-[var(--c-brand-hover)] disabled:opacity-50"
|
||||
>
|
||||
{updateFiles.isPending ? 'Saving…' : 'Save patterns'}
|
||||
</button>
|
||||
{saved && <span className="text-xs text-[var(--c-success)]">Saved</span>}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Coming soon ──────────────────────────────────────────────────────────────
|
||||
|
||||
function ComingSoon({ sectionId }: { sectionId: SectionId }) {
|
||||
|
||||
Reference in New Issue
Block a user