style fixes of repo danger zone
Msrc/routes/repos.tsx
| @@ -1073,15 +1073,11 @@ export const repoRoutes = new Elysia() | |||
|---|---|---|---|
| 1073 | 1073 | ||
| 1074 | 1074 | const oldName = repo.name; | |
| 1075 | 1075 | const newName = body.new_name?.trim() ?? ""; | |
| 1076 | - | const confirm = body.confirm_name?.trim() ?? ""; | |
| 1077 | 1076 | const back = (msg: string) => | |
| 1078 | 1077 | redirect( | |
| 1079 | 1078 | `/${oldName}/settings?error=${encodeURIComponent(msg)}`, | |
| 1080 | 1079 | ); | |
| 1081 | 1080 | ||
| 1082 | - | if (confirm !== oldName) { | |
| 1083 | - | return back("Confirmation did not match the current name."); | |
| 1084 | - | } | |
| 1085 | 1081 | if (newName === oldName) { | |
| 1086 | 1082 | return back("New name is the same as the current name."); | |
| 1087 | 1083 | } | |
| @@ -1141,7 +1137,6 @@ export const repoRoutes = new Elysia() | |||
|---|---|---|---|
| 1141 | 1137 | { | |
| 1142 | 1138 | body: t.Object({ | |
| 1143 | 1139 | new_name: t.String(), | |
| 1144 | - | confirm_name: t.String(), | |
| 1145 | 1140 | }), | |
| 1146 | 1141 | }, | |
| 1147 | 1142 | ) | |
Msrc/styles/components.css
| @@ -1168,14 +1168,39 @@ | |||
|---|---|---|---|
| 1168 | 1168 | } | |
| 1169 | 1169 | .danger-item { | |
| 1170 | 1170 | display: flex; | |
| 1171 | - | align-items: center; | |
| 1172 | - | justify-content: space-between; | |
| 1173 | - | gap: var(--space-4); | |
| 1174 | - | flex-wrap: wrap; | |
| 1171 | + | flex-direction: column; | |
| 1172 | + | gap: var(--space-3); | |
| 1173 | + | padding: var(--space-4) 0; | |
| 1174 | + | } | |
| 1175 | + | .danger-item:first-child { | |
| 1176 | + | padding-top: 0; | |
| 1177 | + | } | |
| 1178 | + | .danger-item:last-child { | |
| 1179 | + | padding-bottom: 0; | |
| 1180 | + | } | |
| 1181 | + | .danger-item + .danger-item { | |
| 1182 | + | border-top: 1px solid var(--color-danger); | |
| 1175 | 1183 | } | |
| 1176 | - | .danger-item p { | |
| 1184 | + | .danger-item-info p { | |
| 1177 | 1185 | margin: var(--space-1) 0 0; | |
| 1178 | 1186 | } | |
| 1187 | + | .danger-form { | |
| 1188 | + | display: flex; | |
| 1189 | + | flex-direction: column; | |
| 1190 | + | align-items: flex-start; | |
| 1191 | + | gap: var(--space-3); | |
| 1192 | + | } | |
| 1193 | + | .danger-form .form-group { | |
| 1194 | + | width: min(360px, 100%); | |
| 1195 | + | } | |
| 1196 | + | .danger-form .form-group input { | |
| 1197 | + | width: 100%; | |
| 1198 | + | } | |
| 1199 | + | .danger-form .confirm-popup { | |
| 1200 | + | left: calc(100% + var(--space-2)); | |
| 1201 | + | right: auto; | |
| 1202 | + | top: 0; | |
| 1203 | + | } | |
| 1179 | 1204 | .btn-danger { | |
| 1180 | 1205 | background: var(--color-danger); | |
| 1181 | 1206 | border-color: transparent; | |
Msrc/views/repos/RepoSettings.tsx
| @@ -305,7 +305,7 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 305 | 305 | <h2 class="section-title danger-title">Danger zone</h2> | |
| 306 | 306 | <div class="form-card danger-card"> | |
| 307 | 307 | <div class="danger-item"> | |
| 308 | - | <div> | |
| 308 | + | <div class="danger-item-info"> | |
| 309 | 309 | <strong>Rename this repository</strong> | |
| 310 | 310 | <p class="text-muted"> | |
| 311 | 311 | Changing the name breaks existing clone | |
| @@ -313,80 +313,83 @@ export function RepoSettings({ | |||
|---|---|---|---|
| 313 | 313 | will need to update their remotes. | |
| 314 | 314 | </p> | |
| 315 | 315 | </div> | |
| 316 | - | <details class="confirm-details"> | |
| 317 | - | <summary class="btn btn-danger"> | |
| 318 | - | Rename repository | |
| 319 | - | </summary> | |
| 320 | - | <div class="confirm-popup"> | |
| 321 | - | <form | |
| 322 | - | method="POST" | |
| 323 | - | action={`/${repo.name}/settings/rename`} | |
| 324 | - | class="inline-form" | |
| 325 | - | > | |
| 326 | - | <div class="form-group"> | |
| 327 | - | <label for="new_name"> | |
| 328 | - | New name | |
| 329 | - | </label> | |
| 330 | - | <input | |
| 331 | - | id="new_name" | |
| 332 | - | name="new_name" | |
| 333 | - | type="text" | |
| 334 | - | required | |
| 335 | - | pattern="[A-Za-z0-9._\-]+" | |
| 336 | - | autocomplete="off" | |
| 337 | - | /> | |
| 338 | - | </div> | |
| 339 | - | <div class="form-group"> | |
| 340 | - | <label for="confirm_name"> | |
| 341 | - | Type <code safe>{repo.name}</code>{" "} | |
| 342 | - | to confirm | |
| 343 | - | </label> | |
| 344 | - | <input | |
| 345 | - | id="confirm_name" | |
| 346 | - | name="confirm_name" | |
| 347 | - | type="text" | |
| 348 | - | required | |
| 349 | - | autocomplete="off" | |
| 350 | - | /> | |
| 351 | - | </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>? | |
| 352 | 338 | <button | |
| 353 | 339 | type="submit" | |
| 354 | 340 | class="btn btn-danger" | |
| 355 | 341 | > | |
| 356 | - | Rename repository | |
| 342 | + | Yes, rename | |
| 357 | 343 | </button> | |
| 358 | - | </form> | |
| 359 | - | </div> | |
| 360 | - | </details> | |
| 344 | + | </div> | |
| 345 | + | </details> | |
| 346 | + | </form> | |
| 361 | 347 | </div> | |
| 362 | 348 | <div class="danger-item"> | |
| 363 | - | <div> | |
| 349 | + | <div class="danger-item-info"> | |
| 364 | 350 | <strong>Delete this repository</strong> | |
| 365 | 351 | <p class="text-muted"> | |
| 366 | 352 | Once deleted, there is no going back. | |
| 367 | 353 | </p> | |
| 368 | 354 | </div> | |
| 369 | - | <details class="confirm-details"> | |
| 370 | - | <summary class="btn btn-danger"> | |
| 371 | - | Delete repository | |
| 372 | - | </summary> | |
| 373 | - | <div class="confirm-popup"> | |
| 374 | - | Delete <span safe>{repo.name}</span>? This | |
| 375 | - | cannot be undone. | |
| 376 | - | <form | |
| 377 | - | method="POST" | |
| 378 | - | action={`/${repo.name}/settings/delete`} | |
| 379 | - | class="inline-form" | |
| 380 | - | > | |
| 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. | |
| 381 | 384 | <button | |
| 382 | 385 | type="submit" | |
| 383 | 386 | class="btn btn-danger" | |
| 384 | 387 | > | |
| 385 | 388 | Yes, delete | |
| 386 | 389 | </button> | |
| 387 | - | </form> | |
| 388 | - | </div> | |
| 389 | - | </details> | |
| 390 | + | </div> | |
| 391 | + | </details> | |
| 392 | + | </form> | |
| 390 | 393 | </div> | |
| 391 | 394 | </div> | |
| 392 | 395 | </div> | |
Mtests/e2e.settings.test.ts
| @@ -362,23 +362,9 @@ describe('repository rename', () => { | |||
|---|---|---|---|
| 362 | 362 | ||
| 363 | 363 | afterAll(async () => { await adminCtx.close(); }); | |
| 364 | 364 | ||
| 365 | - | test('rejects wrong confirmation', async () => { | |
| 366 | - | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 367 | - | form: { new_name: 'whatever', confirm_name: 'wrong' }, | |
| 368 | - | maxRedirects: 0, | |
| 369 | - | }); | |
| 370 | - | expect(resp.status()).toBe(302); | |
| 371 | - | const loc = resp.headers()['location']!; | |
| 372 | - | expect(loc).toContain('/renameme-repo/settings?error='); | |
| 373 | - | expect(decodeURIComponent(loc)).toContain('Confirmation'); | |
| 374 | - | ||
| 375 | - | const check = await adminCtx.request.get(`${BASE}/renameme-repo`); | |
| 376 | - | expect(check.status()).toBe(200); | |
| 377 | - | }); | |
| 378 | - | ||
| 379 | 365 | test('rejects invalid name', async () => { | |
| 380 | 366 | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 381 | - | form: { new_name: 'bad name', confirm_name: 'renameme-repo' }, | |
| 367 | + | form: { new_name: 'bad name' }, | |
| 382 | 368 | maxRedirects: 0, | |
| 383 | 369 | }); | |
| 384 | 370 | expect(resp.status()).toBe(302); | |
| @@ -389,7 +375,7 @@ describe('repository rename', () => { | |||
|---|---|---|---|
| 389 | 375 | ||
| 390 | 376 | test('rejects no-op rename', async () => { | |
| 391 | 377 | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 392 | - | form: { new_name: 'renameme-repo', confirm_name: 'renameme-repo' }, | |
| 378 | + | form: { new_name: 'renameme-repo' }, | |
| 393 | 379 | maxRedirects: 0, | |
| 394 | 380 | }); | |
| 395 | 381 | expect(resp.status()).toBe(302); | |
| @@ -398,7 +384,7 @@ describe('repository rename', () => { | |||
|---|---|---|---|
| 398 | 384 | ||
| 399 | 385 | test('rejects duplicate name', async () => { | |
| 400 | 386 | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 401 | - | form: { new_name: 'rename-other', confirm_name: 'renameme-repo' }, | |
| 387 | + | form: { new_name: 'rename-other' }, | |
| 402 | 388 | maxRedirects: 0, | |
| 403 | 389 | }); | |
| 404 | 390 | expect(resp.status()).toBe(302); | |
| @@ -409,7 +395,7 @@ describe('repository rename', () => { | |||
|---|---|---|---|
| 409 | 395 | const aliceCtx = await loggedInContext('alice', 'password123'); | |
| 410 | 396 | try { | |
| 411 | 397 | const resp = await aliceCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 412 | - | form: { new_name: 'hijack', confirm_name: 'renameme-repo' }, | |
| 398 | + | form: { new_name: 'hijack' }, | |
| 413 | 399 | maxRedirects: 0, | |
| 414 | 400 | }); | |
| 415 | 401 | expect(resp.status()).toBe(403); | |
| @@ -418,7 +404,7 @@ describe('repository rename', () => { | |||
|---|---|---|---|
| 418 | 404 | ||
| 419 | 405 | test('admin can rename repository', async () => { | |
| 420 | 406 | const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, { | |
| 421 | - | form: { new_name: 'renamed-repo', confirm_name: 'renameme-repo' }, | |
| 407 | + | form: { new_name: 'renamed-repo' }, | |
| 422 | 408 | maxRedirects: 0, | |
| 423 | 409 | }); | |
| 424 | 410 | expect(resp.status()).toBe(302); | |