gitshark

Clone repository

git clone https://gitshark.ha1nz.de/git/workaround/Gitshark.git
git clone git@gitshark.ha1nz.de:workaround/Gitshark.git

← Commits

๐Ÿ’„ (forms): Move the remaining create pages onto the labelled layout

686bcc0dc75938ecf8407b42bf7073a0dc25c243 ยท Phillip Souza Furtner ยท 2026-09-08T12:07:13Z

Changes

7 files changed, +203 -61

MODIFY src/main/resources/META-INF/resources/shark.css +0 -20
diff --git a/src/main/resources/META-INF/resources/shark.css b/src/main/resources/META-INF/resources/shark.css
index fa7a301..d24b4c8 100644
--- a/src/main/resources/META-INF/resources/shark.css
+++ b/src/main/resources/META-INF/resources/shark.css
@@ -1515,26 +1515,6 @@
1515 1515 color: var(--accent-deep);
1516 1516 }
1517 1517
1518 -.mr-branch-picker {
1519 - display: flex;
1520 - align-items: center;
1521 - gap: var(--s2);
1522 - flex-wrap: wrap;
1523 -}
1524 -
1525 -.mr-branch-picker select {
1526 - font: 500 13px/1 var(--mono);
1527 - padding: 6px 8px;
1528 - border: 1px solid var(--border);
1529 - border-radius: 6px;
1530 - background: var(--surface);
1531 - color: var(--ink);
1532 -}
1533 -
1534 -.mr-branch-picker .into {
1535 - color: var(--muted);
1536 -}
1537 -
1538 1518 /* diff view */
1539 1519
1540 1520 .diffstat-summary {
MODIFY src/main/resources/templates/HomeResource/newRepo.html +35 -17
diff --git a/src/main/resources/templates/HomeResource/newRepo.html b/src/main/resources/templates/HomeResource/newRepo.html
index c9c22c2..716c4ef 100644
--- a/src/main/resources/templates/HomeResource/newRepo.html
+++ b/src/main/resources/templates/HomeResource/newRepo.html
@@ -4,25 +4,43 @@
4 4 {#if error}
5 5 <p class="error">{error}</p>
6 6 {/if}
7 -<form method="post" action="/repos">
8 - {#if !orgs.isEmpty()}
9 - <p><label>Owner
10 - <select name="owner">
11 - <option value="">{cdi:currentUser.get.username} (personal)</option>
12 - {#for org in orgs}
13 - <option value="{org.name}">{org.name}</option>
14 - {/for}
15 - </select>
16 - </label></p>
17 - {/if}
18 - <p><label>Name <input class="mono" name="name" required pattern="[a-zA-Z0-9._-]+"></label></p>
19 - <p><label>Visibility
20 - <select name="visibility">
7 +<form class="stack-form" method="post" action="/repos">
8 + <fieldset class="field-group field-row">
9 + <legend>Identity</legend>
10 + {#if !orgs.isEmpty()}
11 + <div class="field field-short">
12 + <label for="repo-owner">Owner</label>
13 + <select id="repo-owner" name="owner">
14 + <option value="">{cdi:currentUser.get.username} (personal)</option>
15 + {#for org in orgs}
16 + <option value="{org.name}">{org.name}</option>
17 + {/for}
18 + </select>
19 + </div>
20 + {/if}
21 + <div class="field field-short">
22 + <label for="repo-name">Name</label>
23 + <p class="hint" id="repo-name-hint">Letters, digits, dot, dash and underscore</p>
24 + <input class="mono" type="text" id="repo-name" name="name" required pattern="[a-zA-Z0-9._-]+"
25 + autocomplete="off" aria-describedby="repo-name-hint">
26 + </div>
27 + </fieldset>
28 + <div class="field field-short">
29 + <label for="repo-visibility">Visibility</label>
30 + <p class="hint" id="repo-visibility-hint">Private repositories are visible only to people you give access to</p>
31 + <select id="repo-visibility" name="visibility" aria-describedby="repo-visibility-hint">
21 32 <option value="PUBLIC">Public</option>
22 33 <option value="PRIVATE">Private</option>
23 34 </select>
24 - </label></p>
25 - <p><label>Description <input name="description"></label></p>
26 - <button class="btn btn-primary">Create repository</button>
35 + </div>
36 + <div class="field field-medium">
37 + <label for="repo-description">Description <span class="optional">(optional)</span></label>
38 + <p class="hint" id="repo-description-hint">One line, shown next to the repository wherever it is listed</p>
39 + <input type="text" id="repo-description" name="description" aria-describedby="repo-description-hint">
40 + </div>
41 + <div class="form-actions">
42 + <button type="submit" class="btn btn-primary">Create repository</button>
43 + <a class="form-cancel" href="/">Cancel</a>
44 + </div>
27 45 </form>
28 46 {/include}
MODIFY src/main/resources/templates/IssueResource/editIssue.html +13 -4
diff --git a/src/main/resources/templates/IssueResource/editIssue.html b/src/main/resources/templates/IssueResource/editIssue.html
index 87b3591..97d6ecc 100644
--- a/src/main/resources/templates/IssueResource/editIssue.html
+++ b/src/main/resources/templates/IssueResource/editIssue.html
@@ -5,12 +5,21 @@
5 5 <section class="repo-main">
6 6 <p><a href="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}">โ† {issue.title}</a></p>
7 7 <h2>Edit issue <span class="issue-no">#{issue.number}</span></h2>
8 - <form class="issue-form" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/edit">
9 - <input type="text" name="title" value="{issue.title}" placeholder="Issue title" required autocomplete="off">
10 - <textarea name="description" placeholder="Description (optional)" rows="6">{issue.description ?: ''}</textarea>
8 + <form class="stack-form" method="post"
9 + action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/edit">
10 + <div class="field field-medium">
11 + <label for="issue-title">Title</label>
12 + <input type="text" id="issue-title" name="title" value="{issue.title}" required autocomplete="off">
13 + </div>
14 + <div class="field">
15 + <label for="issue-description">Description <span class="optional">(optional)</span></label>
16 + <p class="hint" id="issue-description-hint">Markdown is supported โ€” raw HTML is escaped</p>
17 + <textarea id="issue-description" name="description" rows="14"
18 + aria-describedby="issue-description-hint">{issue.description ?: ''}</textarea>
19 + </div>
11 20 <div class="form-actions">
12 21 <button type="submit" class="btn btn-primary">Save changes</button>
13 - <a class="btn btn-secondary" href="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}">Cancel</a>
22 + <a class="form-cancel" href="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}">Cancel</a>
14 23 </div>
15 24 </form>
16 25 </section>
MODIFY src/main/resources/templates/IssueResource/newIssue.html +12 -4
diff --git a/src/main/resources/templates/IssueResource/newIssue.html b/src/main/resources/templates/IssueResource/newIssue.html
index dcf9406..d2986bb 100644
--- a/src/main/resources/templates/IssueResource/newIssue.html
+++ b/src/main/resources/templates/IssueResource/newIssue.html
@@ -5,12 +5,20 @@
5 5 <section class="repo-main">
6 6 <p><a href="/repos/{repo.ownerHandle}/{repo.name}/issues">โ† Issues</a></p>
7 7 <h2>New issue</h2>
8 - <form class="issue-form" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues">
9 - <input type="text" name="title" placeholder="Issue title" required autocomplete="off">
10 - <textarea name="description" placeholder="Description (optional)" rows="6"></textarea>
8 + <form class="stack-form" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues">
9 + <div class="field field-medium">
10 + <label for="issue-title">Title</label>
11 + <input type="text" id="issue-title" name="title" required autocomplete="off">
12 + </div>
13 + <div class="field">
14 + <label for="issue-description">Description <span class="optional">(optional)</span></label>
15 + <p class="hint" id="issue-description-hint">Markdown is supported โ€” raw HTML is escaped</p>
16 + <textarea id="issue-description" name="description" rows="14"
17 + aria-describedby="issue-description-hint"></textarea>
18 + </div>
11 19 <div class="form-actions">
12 20 <button type="submit" class="btn btn-primary">Create issue</button>
13 - <a class="btn btn-secondary" href="/repos/{repo.ownerHandle}/{repo.name}/issues">Cancel</a>
21 + <a class="form-cancel" href="/repos/{repo.ownerHandle}/{repo.name}/issues">Cancel</a>
14 22 </div>
15 23 </form>
16 24 </section>
MODIFY src/main/resources/templates/MergeRequestResource/newMergeRequest.html +22 -12
diff --git a/src/main/resources/templates/MergeRequestResource/newMergeRequest.html b/src/main/resources/templates/MergeRequestResource/newMergeRequest.html
index 65f3189..d28af88 100644
--- a/src/main/resources/templates/MergeRequestResource/newMergeRequest.html
+++ b/src/main/resources/templates/MergeRequestResource/newMergeRequest.html
@@ -5,29 +5,39 @@
5 5 <section class="repo-main">
6 6 <p><a href="/repos/{repo.ownerHandle}/{repo.name}/merge-requests">&larr; Merge requests</a></p>
7 7 <h2>New merge request</h2>
8 - <form class="issue-form" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests">
9 - <input type="text" name="title" placeholder="Merge request title" required autocomplete="off">
10 - <div class="mr-branch-picker">
11 - <label>Merge
12 - <select name="sourceBranch" required>
8 + <form class="stack-form" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/merge-requests">
9 + <fieldset class="field-group field-row">
10 + <legend>Branches</legend>
11 + <div class="field field-short">
12 + <label for="mr-source">Merge</label>
13 + <select class="mono" id="mr-source" name="sourceBranch" required>
13 14 {#for branch in branches}
14 15 <option value="{branch}">{branch}</option>
15 16 {/for}
16 17 </select>
17 - </label>
18 - <span class="into">into</span>
19 - <label>
20 - <select name="targetBranch" required>
18 + </div>
19 + <div class="field field-short">
20 + <label for="mr-target">Into</label>
21 + <select class="mono" id="mr-target" name="targetBranch" required>
21 22 {#for branch in branches}
22 23 <option value="{branch}"{#if branch == defaultBranch} selected{/if}>{branch}</option>
23 24 {/for}
24 25 </select>
25 - </label>
26 + </div>
27 + </fieldset>
28 + <div class="field field-medium">
29 + <label for="mr-title">Title</label>
30 + <input type="text" id="mr-title" name="title" required autocomplete="off">
26 31 </div>
27 - <textarea name="description" placeholder="Description (optional)" rows="6"></textarea>
32 + <div class="field">
33 + <label for="mr-description">Description <span class="optional">(optional)</span></label>
34 + <p class="hint" id="mr-description-hint">Shown as plain text, line breaks are kept</p>
35 + <textarea id="mr-description" name="description" rows="14"
36 + aria-describedby="mr-description-hint"></textarea>
37 + </div>
28 38 <div class="form-actions">
29 39 <button type="submit" class="btn btn-primary">Create merge request</button>
30 - <a class="btn btn-secondary" href="/repos/{repo.ownerHandle}/{repo.name}/merge-requests">Cancel</a>
40 + <a class="form-cancel" href="/repos/{repo.ownerHandle}/{repo.name}/merge-requests">Cancel</a>
31 41 </div>
32 42 </form>
33 43 </section>
MODIFY src/main/resources/templates/OrganisationResource/newOrg.html +16 -4
diff --git a/src/main/resources/templates/OrganisationResource/newOrg.html b/src/main/resources/templates/OrganisationResource/newOrg.html
index 2e05e98..e680eed 100644
--- a/src/main/resources/templates/OrganisationResource/newOrg.html
+++ b/src/main/resources/templates/OrganisationResource/newOrg.html
@@ -5,9 +5,21 @@
5 5 {#if error}
6 6 <p class="error">{error}</p>
7 7 {/if}
8 -<form method="post" action="/orgs">
9 - <p><label>Name <input class="mono" name="name" required pattern="[a-z0-9][a-z0-9-]*"></label></p>
10 - <p><label>Display name <input name="displayName"></label></p>
11 - <button class="btn btn-primary">Create organisation</button>
8 +<form class="stack-form" method="post" action="/orgs">
9 + <div class="field field-short">
10 + <label for="org-name">Name</label>
11 + <p class="hint" id="org-name-hint">Lowercase letters, digits and dashes โ€” this becomes the URL</p>
12 + <input class="mono" type="text" id="org-name" name="name" required pattern="[a-z0-9][a-z0-9-]*"
13 + autocomplete="off" aria-describedby="org-name-hint">
14 + </div>
15 + <div class="field field-medium">
16 + <label for="org-display-name">Display name <span class="optional">(optional)</span></label>
17 + <p class="hint" id="org-display-name-hint">Shown in the UI instead of the handle</p>
18 + <input type="text" id="org-display-name" name="displayName" aria-describedby="org-display-name-hint">
19 + </div>
20 + <div class="form-actions">
21 + <button type="submit" class="btn btn-primary">Create organisation</button>
22 + <a class="form-cancel" href="/">Cancel</a>
23 + </div>
12 24 </form>
13 25 {/include}
ADD src/test/java/de/workaround/web/CreateFormLayoutTest.java +105 -0
diff --git a/src/test/java/de/workaround/web/CreateFormLayoutTest.java b/src/test/java/de/workaround/web/CreateFormLayoutTest.java
new file mode 100644
index 0000000..fa9b844
--- /dev/null
+++ b/src/test/java/de/workaround/web/CreateFormLayoutTest.java
@@ -0,0 +1,105 @@
1 +package de.workaround.web;
2 +
3 +import java.nio.charset.StandardCharsets;
4 +import java.util.List;
5 +import java.util.Map;
6 +
7 +import org.junit.jupiter.api.Test;
8 +
9 +import de.workaround.git.GitRepositoryService;
10 +import de.workaround.git.GitTestSeeder;
11 +import de.workaround.model.Repository;
12 +import de.workaround.model.User;
13 +import io.quarkus.test.junit.QuarkusTest;
14 +import io.quarkus.test.security.TestSecurity;
15 +import jakarta.inject.Inject;
16 +import jakarta.transaction.Transactional;
17 +
18 +import static io.restassured.RestAssured.given;
19 +import static org.junit.jupiter.api.Assertions.assertFalse;
20 +import static org.junit.jupiter.api.Assertions.assertTrue;
21 +
22 +@QuarkusTest
23 +class CreateFormLayoutTest
24 +{
25 + @Inject
26 + GitRepositoryService service;
27 +
28 + @Inject
29 + User.Repo users;
30 +
31 + @Test
32 + @TestSecurity(user = "cf-owner")
33 + void everyCreatePageUsesTheWideLabelledFormLayout() throws Exception
34 + {
35 + User owner = persistUser("cf-owner");
36 + seededRepo(owner, "createforms");
37 + String repo = "/repos/" + owner.username + "/createforms";
38 +
39 + assertLabelledForm(repo + "/issues/new", List.of("issue-title", "issue-description"));
40 + assertLabelledForm(repo + "/merge-requests/new",
41 + List.of("mr-title", "mr-source", "mr-target", "mr-description"));
42 + assertLabelledForm("/repos/new", List.of("repo-name", "repo-visibility", "repo-description"));
43 + assertLabelledForm("/orgs/new", List.of("org-name", "org-display-name"));
44 + }
45 +
46 + @Test
47 + @TestSecurity(user = "cf-placeholder")
48 + void createFormsCarryNoLabelShapedPlaceholders() throws Exception
49 + {
50 + User owner = persistUser("cf-placeholder");
51 + seededRepo(owner, "placeholderfree");
52 + String repo = "/repos/" + owner.username + "/placeholderfree";
53 +
54 + assertNoLabelPlaceholder(repo + "/issues/new", "Issue title", "Description (optional)");
55 + assertNoLabelPlaceholder(repo + "/merge-requests/new", "Merge request title", "Description (optional)");
56 + }
57 +
58 + private void assertLabelledForm(String path, List<String> controlIds)
59 + {
60 + String html = given().when().get(path).then().statusCode(200).extract().body().asString();
61 +
62 + assertTrue(html.contains("class=\"stack-form\""),
63 + path + " does not use the wide labelled form layout");
64 + for (String id : controlIds)
65 + {
66 + assertTrue(html.contains("id=\"" + id + "\""), path + " has no control with id " + id);
67 + assertTrue(html.contains("for=\"" + id + "\""), path + " has no label bound to " + id);
68 + }
69 + assertFalse(html.contains("btn btn-secondary"), path + " still renders cancel as a competing button");
70 + }
71 +
72 + private void assertNoLabelPlaceholder(String path, String... placeholders)
73 + {
74 + String html = given().when().get(path).then().statusCode(200).extract().body().asString();
75 +
76 + for (String placeholder : placeholders)
77 + {
78 + assertFalse(html.contains("placeholder=\"" + placeholder + "\""),
79 + path + " still hides the label of a field in its placeholder: " + placeholder);
80 + }
81 + }
82 +
83 + private Repository seededRepo(User owner, String name) throws Exception
84 + {
85 + Repository repo = service.create(owner, name, Repository.Visibility.PUBLIC, null);
86 + GitTestSeeder.seed(service.repositoryPath(repo),
87 + Map.of("README.md", "# seed\n".getBytes(StandardCharsets.UTF_8)));
88 + return repo;
89 + }
90 +
91 + @Transactional
92 + User persistUser(String name)
93 + {
94 + User existing = users.findByOidcSubOptional(name).orElse(null);
95 + if (existing != null)
96 + {
97 + return existing;
98 + }
99 + User user = new User();
100 + user.oidcSub = name;
101 + user.username = name;
102 + user.persist();
103 + return user;
104 + }
105 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog