๐ (forms): Move the remaining create pages onto the labelled layout
Changes
7 files changed, +203 -61
MODIFY
src/main/resources/META-INF/resources/shark.css
+0 -20
@@ -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
@@ -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
@@ -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
@@ -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
@@ -5,29 +5,39 @@
5
5
<section class="repo-main">
6
6
<p><a href="/repos/{repo.ownerHandle}/{repo.name}/merge-requests">← 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
@@ -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
@@ -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
+}