RepoSettings.tsx
Raw
1import type { LabelRow, RepositoryRow } from "../../db/index.ts";
2
3interface SecretSummary {
4 id: number;
5 name: string;
6 description: string | null;
7 created_at: string;
8}
9
10import type { SessionUser } from "../../middleware/session.ts";
11import { Layout } from "../layout.tsx";
12import { RepoHeader } from "./RepoHeader.tsx";
13import { RepoNav } from "./RepoNav.tsx";
14
15interface RepoSettingsProps {
16 user: SessionUser;
17 repo: RepositoryRow;
18 branches: string[];
19 labels: LabelRow[];
20 secrets: SecretSummary[];
21 success?: string;
22 error?: string;
23}
24
25export function RepoSettings({
26 user,
27 repo,
28 branches,
29 labels,
30 secrets,
31 success,
32 error,
33}: RepoSettingsProps) {
34 return (
35 <Layout user={user} title={`Settings — ${repo.name}`}>
36 <div class="container">
37 <RepoHeader repo={repo} />
38 <RepoNav repo={repo} active="settings" user={user} />
39 {!!success && (
40 <p class="form-success" safe>
41 {success}
42 </p>
43 )}
44 {!!error && (
45 <p class="form-error" safe>
46 {error}
47 </p>
48 )}
49 <form
50 method="POST"
51 action={`/${repo.name}/settings`}
52 class="form-card"
53 >
54 <div class="form-group">
55 <label for="description">Description</label>
56 <input
57 id="description"
58 name="description"
59 type="text"
60 value={repo.description ?? ""}
61 placeholder="A short description"
62 />
63 </div>
64 <div class="form-group">
65 <label for="default_branch">Default branch</label>
66 {branches.length > 0 ? (
67 <select
68 id="default_branch"
69 name="default_branch"
70 class="branch-select"
71 >
72 {branches.map((b) => (
73 <option
74 value={b}
75 selected={
76 b === repo.default_branch
77 ? true
78 : undefined
79 }
80 safe
81 >
82 {b}
83 </option>
84 ))}
85 </select>
86 ) : (
87 <p class="form-hint">
88 No branches yet — push your first commit to set
89 the default branch.
90 </p>
91 )}
92 </div>
93 <div class="form-group">
94 <label class="checkbox-label">
95 <input
96 type="checkbox"
97 name="is_private"
98 value="1"
99 checked={repo.is_private === 1}
100 />
101 Private repository (only visible to you)
102 </label>
103 </div>
104 <div class="form-group">
105 <label class="checkbox-label">
106 <input
107 type="checkbox"
108 name="is_pinned"
109 value="1"
110 checked={repo.is_pinned === 1}
111 />
112 Pin this repository (always shown at the top of the
113 list)
114 </label>
115 </div>
116 <div class="form-group">
117 <label class="checkbox-label">
118 <input
119 type="checkbox"
120 name="allow_user_labels"
121 value="1"
122 checked={repo.allow_user_labels === 1}
123 />
124 Allow users to add/remove labels on their own issues
125 and patches
126 </label>
127 </div>
128 <div class="form-group">
129 <label for="issue_template">
130 Issue template{" "}
131 <span class="text-muted">
132 (Markdown, prefilled when opening a new issue)
133 </span>
134 </label>
135 <textarea
136 id="issue_template"
137 name="issue_template"
138 rows="8"
139 placeholder="## Description&#10;&#10;## Steps to reproduce&#10;&#10;## Expected behavior"
140 safe
141 >
142 {repo.issue_template ?? ""}
143 </textarea>
144 </div>
145 <div class="form-group">
146 <label for="patch_template">
147 Patch template{" "}
148 <span class="text-muted">
149 (Markdown, prefilled when submitting a new
150 patch)
151 </span>
152 </label>
153 <textarea
154 id="patch_template"
155 name="patch_template"
156 rows="8"
157 placeholder="## Summary&#10;&#10;## Testing"
158 safe
159 >
160 {repo.patch_template ?? ""}
161 </textarea>
162 </div>
163 <button type="submit" class="btn btn-primary">
164 Save settings
165 </button>
166 </form>
167 <div class="form-card">
168 <h2 class="section-title">Labels</h2>
169 {labels.length > 0 && (
170 <div class="label-settings-list">
171 {labels.map((label) => (
172 <div class="label-settings-item">
173 <span
174 class="label-settings-swatch"
175 style={`background:${label.color}`}
176 />
177 <span class="label-settings-name" safe>
178 {label.name}
179 </span>
180 <form
181 method="POST"
182 action={`/${repo.name}/settings/labels/delete`}
183 >
184 <input
185 type="hidden"
186 name="id"
187 value={String(label.id)}
188 />
189 <button
190 class="btn btn-danger btn-sm"
191 type="submit"
192 >
193 Delete
194 </button>
195 </form>
196 </div>
197 ))}
198 </div>
199 )}
200 <form
201 method="POST"
202 action={`/${repo.name}/settings/labels`}
203 class="label-add-form"
204 >
205 <input
206 class="form-input label-name-input"
207 type="text"
208 name="name"
209 placeholder="Label name"
210 maxlength="50"
211 required
212 />
213 <label
214 class="label-color-swatch-label"
215 title="Pick a color"
216 >
217 <input
218 type="color"
219 name="color"
220 value="#808080"
221 class="label-color-input"
222 />
223 </label>
224 <button type="submit" class="btn btn-secondary btn-sm">
225 Add label
226 </button>
227 </form>
228 </div>
229 <div class="form-card">
230 <h2 class="section-title">CI Secrets</h2>
231 <p
232 class="text-muted"
233 style="font-size: var(--text-sm); margin-bottom: var(--space-3)"
234 >
235 Secrets are injected as environment variables into
236 pipeline runs and masked in logs. Values are write-only
237 — they cannot be retrieved after saving.
238 </p>
239 {secrets.length > 0 && (
240 <div class="label-settings-list">
241 {secrets.map((secret) => (
242 <div class="label-settings-item">
243 <span class="label-settings-name">
244 <code safe>{secret.name}</code>
245 </span>
246 {!!secret.description && (
247 <span class="text-muted" safe>
248 {secret.description}
249 </span>
250 )}
251 <span class="text-muted">●●●●●●</span>
252 <form
253 method="POST"
254 action={`/${repo.name}/settings/ci-secrets/delete`}
255 >
256 <input
257 type="hidden"
258 name="id"
259 value={String(secret.id)}
260 />
261 <button
262 class="btn btn-danger btn-sm"
263 type="submit"
264 >
265 Delete
266 </button>
267 </form>
268 </div>
269 ))}
270 </div>
271 )}
272 <form
273 method="POST"
274 action={`/${repo.name}/settings/ci-secrets`}
275 class="label-add-form"
276 >
277 <input
278 class="form-input label-name-input"
279 type="text"
280 name="name"
281 placeholder="SECRET_NAME"
282 pattern="[A-Za-z_][A-Za-z0-9_]*"
283 required
284 />
285 <input
286 class="form-input label-name-input"
287 type="password"
288 name="value"
289 placeholder="Value"
290 autocomplete="off"
291 required
292 />
293 <input
294 class="form-input label-name-input"
295 type="text"
296 name="description"
297 placeholder="Description (optional)"
298 />
299 <button type="submit" class="btn btn-secondary btn-sm">
300 Save secret
301 </button>
302 </form>
303 </div>
304 <div class="danger-zone">
305 <h2 class="section-title danger-title">Danger zone</h2>
306 <div class="form-card danger-card">
307 <div class="danger-item">
308 <div class="danger-item-info">
309 <strong>Rename this repository</strong>
310 <p class="text-muted">
311 Changing the name breaks existing clone URLs
312 and links to this repo. Collaborators will
313 need to update their remotes.
314 </p>
315 </div>
316 <form
317 method="POST"
318 action={`/${repo.name}/settings/rename`}
319 class="danger-form"
320 >
321 <div class="form-group">
322 <label for="new_name">New name</label>
323 <input
324 id="new_name"
325 name="new_name"
326 type="text"
327 required
328 pattern="[A-Za-z0-9._\-]+"
329 autocomplete="off"
330 />
331 </div>
332 <details class="confirm-details">
333 <summary class="btn btn-danger">
334 Rename repository
335 </summary>
336 <div class="confirm-popup">
337 Rename <span safe>{repo.name}</span>?
338 <button
339 type="submit"
340 class="btn btn-danger"
341 >
342 Yes, rename
343 </button>
344 </div>
345 </details>
346 </form>
347 </div>
348 <div class="danger-item">
349 <div class="danger-item-info">
350 <strong>Delete this repository</strong>
351 <p class="text-muted">
352 Once deleted, there is no going back.
353 </p>
354 </div>
355 <form
356 method="POST"
357 action={`/${repo.name}/settings/delete`}
358 class="danger-form"
359 >
360 <div class="form-group">
361 <label for="confirm_delete">
362 Type <code safe>{repo.name}</code> to
363 confirm deletion
364 </label>
365 <input
366 id="confirm_delete"
367 name="confirm_name"
368 type="text"
369 required
370 pattern={repo.name.replace(
371 /[.*+?^${}()|[\]\\]/g,
372 "\\$&",
373 )}
374 autocomplete="off"
375 />
376 </div>
377 <details class="confirm-details">
378 <summary class="btn btn-danger">
379 Delete repository
380 </summary>
381 <div class="confirm-popup">
382 Delete <span safe>{repo.name}</span>?
383 This cannot be undone.
384 <button
385 type="submit"
386 class="btn btn-danger"
387 >
388 Yes, delete
389 </button>
390 </div>
391 </details>
392 </form>
393 </div>
394 </div>
395 </div>
396 </div>
397 </Layout>
398 );
399}
400