mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(panel): responsive two-column layout for the Conventions editor
The per-project Conventions tab was one long single column in a narrow modal, wasting all the horizontal space. Lay the sections out in a responsive grid — Module boundaries | Rules, then Waivers | Custom rules — with Recent violations full-width on its own row, and widen the modal on large viewports (only on the Conventions tab; Settings stays compact). Collapses to a single column on mobile and is capped at xl so it stays sane up to a 27" display.
This commit is contained in:
@@ -35,7 +35,9 @@ const FORBIDDABLE_KINDS: DefinitionKind[] = [
|
||||
|
||||
function actionToast(verb: string, result: ConventionsActionResult): void {
|
||||
if (result.created && result.pr_number != null) {
|
||||
toast.success(`${verb}: opened PR #${result.pr_number} on ${result.branch}`);
|
||||
toast.success(
|
||||
`${verb}: opened PR #${result.pr_number} on ${result.branch}`,
|
||||
);
|
||||
} else {
|
||||
toast.success(
|
||||
`${verb}: prepared on ${result.branch} (no remote PR — workspace not cloned)`,
|
||||
@@ -108,10 +110,14 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
|
||||
const updateModule = (index: number, next: Partial<ConventionsModule>) =>
|
||||
edit({
|
||||
modules: standard.modules.map((m, i) => (i === index ? { ...m, ...next } : m)),
|
||||
modules: standard.modules.map((m, i) =>
|
||||
i === index ? { ...m, ...next } : m,
|
||||
),
|
||||
});
|
||||
const addModule = () =>
|
||||
edit({ modules: [...standard.modules, { path: "", purpose: "", forbidden: [] }] });
|
||||
edit({
|
||||
modules: [...standard.modules, { path: "", purpose: "", forbidden: [] }],
|
||||
});
|
||||
const removeModule = (index: number) =>
|
||||
edit({ modules: standard.modules.filter((_, i) => i !== index) });
|
||||
const toggleForbidden = (index: number, kind: DefinitionKind) => {
|
||||
@@ -124,7 +130,9 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
|
||||
const updateCustom = (index: number, next: Partial<ConventionsCustomRule>) =>
|
||||
edit({
|
||||
custom: standard.custom.map((c, i) => (i === index ? { ...c, ...next } : c)),
|
||||
custom: standard.custom.map((c, i) =>
|
||||
i === index ? { ...c, ...next } : c,
|
||||
),
|
||||
});
|
||||
const addCustom = () =>
|
||||
edit({
|
||||
@@ -138,10 +146,14 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
|
||||
const updateWaiver = (index: number, next: Partial<ConventionsWaiver>) =>
|
||||
edit({
|
||||
waivers: standard.waivers.map((w, i) => (i === index ? { ...w, ...next } : w)),
|
||||
waivers: standard.waivers.map((w, i) =>
|
||||
i === index ? { ...w, ...next } : w,
|
||||
),
|
||||
});
|
||||
const addWaiver = () =>
|
||||
edit({ waivers: [...standard.waivers, { path: "", rule: "", reason: "" }] });
|
||||
edit({
|
||||
waivers: [...standard.waivers, { path: "", rule: "", reason: "" }],
|
||||
});
|
||||
const removeWaiver = (index: number) =>
|
||||
edit({ waivers: standard.waivers.filter((_, i) => i !== index) });
|
||||
|
||||
@@ -152,6 +164,240 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
const degraded = status === "degraded";
|
||||
const usingDefaults = status === "missing" || status === "unknown";
|
||||
|
||||
const moduleBoundaries = (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Module boundaries</CardTitle>
|
||||
<CardDescription>
|
||||
Which definition kinds are forbidden in each module. Click a kind to
|
||||
toggle it.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{standard.modules.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No modules mapped yet.
|
||||
</p>
|
||||
)}
|
||||
{standard.modules.map((module, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="space-y-2 rounded-md border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={module.path}
|
||||
placeholder="path/to/module"
|
||||
onChange={(e) => updateModule(index, { path: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeModule(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={module.purpose}
|
||||
placeholder="what this module is for"
|
||||
onChange={(e) => updateModule(index, { purpose: e.target.value })}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{FORBIDDABLE_KINDS.map((kind) => (
|
||||
<Badge
|
||||
key={kind}
|
||||
variant={
|
||||
module.forbidden.includes(kind) ? "destructive" : "outline"
|
||||
}
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleForbidden(index, kind)}
|
||||
>
|
||||
no {kind}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addModule}>
|
||||
Add module
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const rules = (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Rules</CardTitle>
|
||||
<CardDescription>
|
||||
Toggle a rule between warn (advisory) and block (refuses the gate).
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Object.values(standard.rules).map((rule) => (
|
||||
<div
|
||||
key={rule.name}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<span className="text-sm">{rule.name.replace(/_/g, " ")}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-10 text-right text-xs text-muted-foreground">
|
||||
{rule.level}
|
||||
</span>
|
||||
<Switch
|
||||
checked={rule.level === "block"}
|
||||
onCheckedChange={(checked) =>
|
||||
setRuleLevel(rule.name, checked ? "block" : "warn")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const waivers = (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Waivers</CardTitle>
|
||||
<CardDescription>
|
||||
Accountable escapes — exempt a file from a rule with a reason
|
||||
(reviewed in the PR, never a silent in-code suppression).
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{standard.waivers.map((waiver, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="space-y-2 rounded-md border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={waiver.path}
|
||||
placeholder="path/to/file.py"
|
||||
onChange={(e) => updateWaiver(index, { path: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
value={waiver.rule}
|
||||
placeholder="rule name"
|
||||
onChange={(e) => updateWaiver(index, { rule: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeWaiver(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={waiver.reason}
|
||||
placeholder="why this is waived"
|
||||
onChange={(e) => updateWaiver(index, { reason: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addWaiver}>
|
||||
Add waiver
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const customRules = (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Custom rules</CardTitle>
|
||||
<CardDescription>
|
||||
Project-specific regex rules — a pattern, a message, and a level.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{standard.custom.map((rule, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="space-y-2 rounded-md border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={rule.id}
|
||||
placeholder="rule-id"
|
||||
onChange={(e) => updateCustom(index, { id: e.target.value })}
|
||||
/>
|
||||
<span className="w-10 text-right text-xs text-muted-foreground">
|
||||
{rule.level}
|
||||
</span>
|
||||
<Switch
|
||||
checked={rule.level === "block"}
|
||||
onCheckedChange={(checked) =>
|
||||
updateCustom(index, { level: checked ? "block" : "warn" })
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeCustom(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={rule.pattern}
|
||||
placeholder="regex pattern"
|
||||
onChange={(e) => updateCustom(index, { pattern: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
value={rule.message}
|
||||
placeholder="message shown when it matches"
|
||||
onChange={(e) => updateCustom(index, { message: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addCustom}>
|
||||
Add custom rule
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const recentViolations = (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Recent violations</CardTitle>
|
||||
<CardDescription>
|
||||
The latest findings recorded across this project's tasks.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{(!findings || findings.length === 0) && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No violations recorded yet.
|
||||
</p>
|
||||
)}
|
||||
{(findings ?? []).map((finding, index) => (
|
||||
<div
|
||||
key={`${finding.file}:${finding.line}:${finding.rule}:${index}`}
|
||||
className="flex items-start justify-between gap-4 text-sm"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<code>
|
||||
{finding.file}:{finding.line}
|
||||
</code>{" "}
|
||||
<span className="text-muted-foreground">{finding.message}</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant={finding.level === "block" ? "destructive" : "secondary"}
|
||||
>
|
||||
{finding.rule}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 py-2">
|
||||
{degraded && (
|
||||
@@ -171,7 +417,9 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
{usingDefaults && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Using auto-derived defaults</CardTitle>
|
||||
<CardTitle className="text-sm">
|
||||
Using auto-derived defaults
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
No <code>.roboco/conventions.yml</code> is committed yet. These
|
||||
rules are auto-derived from the repository and are already
|
||||
@@ -181,227 +429,18 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Module boundaries</CardTitle>
|
||||
<CardDescription>
|
||||
Which definition kinds are forbidden in each module. Click a kind to
|
||||
toggle it.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{standard.modules.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">No modules mapped yet.</p>
|
||||
)}
|
||||
{standard.modules.map((module, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="space-y-2 rounded-md border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={module.path}
|
||||
placeholder="path/to/module"
|
||||
onChange={(e) => updateModule(index, { path: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeModule(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={module.purpose}
|
||||
placeholder="what this module is for"
|
||||
onChange={(e) => updateModule(index, { purpose: e.target.value })}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{FORBIDDABLE_KINDS.map((kind) => (
|
||||
<Badge
|
||||
key={kind}
|
||||
variant={
|
||||
module.forbidden.includes(kind) ? "destructive" : "outline"
|
||||
}
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleForbidden(index, kind)}
|
||||
>
|
||||
no {kind}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addModule}>
|
||||
Add module
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Two columns on wide viewports so the modal isn't a long single column;
|
||||
Module boundaries | Rules, then Waivers | Custom rules. Each cell keeps
|
||||
its natural height (items-start) and stacks to one column on mobile. */}
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:items-start">
|
||||
{moduleBoundaries}
|
||||
{rules}
|
||||
{waivers}
|
||||
{customRules}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Rules</CardTitle>
|
||||
<CardDescription>
|
||||
Toggle a rule between warn (advisory) and block (refuses the gate).
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Object.values(standard.rules).map((rule) => (
|
||||
<div
|
||||
key={rule.name}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<span className="text-sm">{rule.name.replace(/_/g, " ")}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-10 text-right text-xs text-muted-foreground">
|
||||
{rule.level}
|
||||
</span>
|
||||
<Switch
|
||||
checked={rule.level === "block"}
|
||||
onCheckedChange={(checked) =>
|
||||
setRuleLevel(rule.name, checked ? "block" : "warn")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Custom rules</CardTitle>
|
||||
<CardDescription>
|
||||
Project-specific regex rules — a pattern, a message, and a level.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{standard.custom.map((rule, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="space-y-2 rounded-md border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={rule.id}
|
||||
placeholder="rule-id"
|
||||
onChange={(e) => updateCustom(index, { id: e.target.value })}
|
||||
/>
|
||||
<span className="w-10 text-right text-xs text-muted-foreground">
|
||||
{rule.level}
|
||||
</span>
|
||||
<Switch
|
||||
checked={rule.level === "block"}
|
||||
onCheckedChange={(checked) =>
|
||||
updateCustom(index, { level: checked ? "block" : "warn" })
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeCustom(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={rule.pattern}
|
||||
placeholder="regex pattern"
|
||||
onChange={(e) => updateCustom(index, { pattern: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
value={rule.message}
|
||||
placeholder="message shown when it matches"
|
||||
onChange={(e) => updateCustom(index, { message: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addCustom}>
|
||||
Add custom rule
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Waivers</CardTitle>
|
||||
<CardDescription>
|
||||
Accountable escapes — exempt a file from a rule with a reason
|
||||
(reviewed in the PR, never a silent in-code suppression).
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{standard.waivers.map((waiver, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="space-y-2 rounded-md border border-border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={waiver.path}
|
||||
placeholder="path/to/file.py"
|
||||
onChange={(e) => updateWaiver(index, { path: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
value={waiver.rule}
|
||||
placeholder="rule name"
|
||||
onChange={(e) => updateWaiver(index, { rule: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeWaiver(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={waiver.reason}
|
||||
placeholder="why this is waived"
|
||||
onChange={(e) => updateWaiver(index, { reason: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" size="sm" onClick={addWaiver}>
|
||||
Add waiver
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Recent violations</CardTitle>
|
||||
<CardDescription>
|
||||
The latest findings recorded across this project's tasks.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{(!findings || findings.length === 0) && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No violations recorded yet.
|
||||
</p>
|
||||
)}
|
||||
{(findings ?? []).map((finding, index) => (
|
||||
<div
|
||||
key={`${finding.file}:${finding.line}:${finding.rule}:${index}`}
|
||||
className="flex items-start justify-between gap-4 text-sm"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<code>
|
||||
{finding.file}:{finding.line}
|
||||
</code>{" "}
|
||||
<span className="text-muted-foreground">{finding.message}</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant={finding.level === "block" ? "destructive" : "secondary"}
|
||||
>
|
||||
{finding.rule}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Recent violations spans the full width on its own row. */}
|
||||
{recentViolations}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
@@ -417,7 +456,9 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
||||
disabled={(draft == null && !usingDefaults) || save.isPending}
|
||||
onClick={() => save.mutate(draft ?? standard)}
|
||||
>
|
||||
{usingDefaults && draft == null ? "Save defaults to repo" : "Save to repo"}
|
||||
{usingDefaults && draft == null
|
||||
? "Save defaults to repo"
|
||||
: "Save to repo"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,12 +22,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ConventionsTab } from "@/components/conventions/conventions-tab";
|
||||
import { Key, KeyRound } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@@ -65,10 +60,16 @@ function EditProjectForm({
|
||||
const [isActive, setIsActive] = useState(project.is_active);
|
||||
const [testCommand, setTestCommand] = useState(project.test_command || "");
|
||||
const [lintCommand, setLintCommand] = useState(project.lint_command || "");
|
||||
const [formatCommand, setFormatCommand] = useState(project.format_command || "");
|
||||
const [typecheckCommand, setTypecheckCommand] = useState(project.typecheck_command || "");
|
||||
const [formatCommand, setFormatCommand] = useState(
|
||||
project.format_command || "",
|
||||
);
|
||||
const [typecheckCommand, setTypecheckCommand] = useState(
|
||||
project.typecheck_command || "",
|
||||
);
|
||||
const [buildCommand, setBuildCommand] = useState(project.build_command || "");
|
||||
const [qualityCommand, setQualityCommand] = useState(project.quality_command || "");
|
||||
const [qualityCommand, setQualityCommand] = useState(
|
||||
project.quality_command || "",
|
||||
);
|
||||
|
||||
// Token handling
|
||||
const [newToken, setNewToken] = useState("");
|
||||
@@ -113,7 +114,7 @@ function EditProjectForm({
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to update project: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
`Failed to update project: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -122,7 +123,9 @@ function EditProjectForm({
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Project</DialogTitle>
|
||||
<DialogDescription>Update project settings. Slug cannot be changed.</DialogDescription>
|
||||
<DialogDescription>
|
||||
Update project settings. Slug cannot be changed.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
{/* Slug (read-only) */}
|
||||
@@ -165,18 +168,25 @@ function EditProjectForm({
|
||||
{project.has_git_token ? (
|
||||
<>
|
||||
<Key className="h-4 w-4 text-green-500" />
|
||||
<span className="text-green-600 dark:text-green-400">Token is set</span>
|
||||
<span className="text-green-600 dark:text-green-400">
|
||||
Token is set
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<KeyRound className="h-4 w-4 text-amber-500" />
|
||||
<span className="text-amber-600 dark:text-amber-400">No token configured</span>
|
||||
<span className="text-amber-600 dark:text-amber-400">
|
||||
No token configured
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Label>
|
||||
{project.has_git_token && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="clear-token" className="text-xs text-muted-foreground">
|
||||
<Label
|
||||
htmlFor="clear-token"
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
Clear token
|
||||
</Label>
|
||||
<Switch
|
||||
@@ -204,7 +214,8 @@ function EditProjectForm({
|
||||
placeholder="ghp_xxxxxxxxxxxx..."
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Personal access token with repo access for clone, push, and PR operations
|
||||
Personal access token with repo access for clone, push, and PR
|
||||
operations
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -213,7 +224,10 @@ function EditProjectForm({
|
||||
{/* Assigned Cell */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="assigned_cell">Assigned Cell *</Label>
|
||||
<Select value={assignedCell} onValueChange={(value: Team) => setAssignedCell(value)}>
|
||||
<Select
|
||||
value={assignedCell}
|
||||
onValueChange={(value: Team) => setAssignedCell(value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select cell" />
|
||||
</SelectTrigger>
|
||||
@@ -241,7 +255,11 @@ function EditProjectForm({
|
||||
{/* Active Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="is_active">Active</Label>
|
||||
<Switch id="is_active" checked={isActive} onCheckedChange={setIsActive} />
|
||||
<Switch
|
||||
id="is_active"
|
||||
checked={isActive}
|
||||
onCheckedChange={setIsActive}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Advanced Options Toggle */}
|
||||
@@ -315,8 +333,8 @@ function EditProjectForm({
|
||||
placeholder="make gate"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Fast pre-submit gate (lint + types + complexity, no tests) run in
|
||||
the dev's workspace at hand-off to QA.
|
||||
Fast pre-submit gate (lint + types + complexity, no tests) run
|
||||
in the dev's workspace at hand-off to QA.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
@@ -335,12 +353,25 @@ function EditProjectForm({
|
||||
}
|
||||
|
||||
// Main dialog component - handles data fetching and dialog state
|
||||
export function EditProjectDialog({ projectId, open, onOpenChange }: EditProjectDialogProps) {
|
||||
export function EditProjectDialog({
|
||||
projectId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: EditProjectDialogProps) {
|
||||
const { data: project, isLoading } = useProject(projectId);
|
||||
const [tab, setTab] = useState("settings");
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
|
||||
{/* Settings is a compact form; Conventions uses a two-column grid, so it
|
||||
gets a wider, responsive modal (capped so it stays sane on a 27"). */}
|
||||
<DialogContent
|
||||
className={`max-h-[90vh] overflow-y-auto ${
|
||||
tab === "conventions"
|
||||
? "sm:max-w-2xl lg:max-w-5xl xl:max-w-6xl"
|
||||
: "sm:max-w-[525px]"
|
||||
}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="space-y-4 py-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
@@ -349,7 +380,7 @@ export function EditProjectDialog({ projectId, open, onOpenChange }: EditProject
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : project ? (
|
||||
<Tabs defaultValue="settings">
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="conventions">Conventions</TabsTrigger>
|
||||
@@ -368,7 +399,9 @@ export function EditProjectDialog({ projectId, open, onOpenChange }: EditProject
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className="py-8 text-center text-muted-foreground">Project not found</div>
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
Project not found
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user