๐ (releases): Give the release forms labels, hints and room
Changes
5 files changed, +258 -49
MODIFY
docs/users/releases.md
+10 -8
@@ -25,15 +25,17 @@
25
25
## Publishing a release
26
26
27
27
1. Open **Releases โ New release**.
28
-2. **Tag** โ type the tag name, e.g. `v1.0.0`. The field suggests tags that
29
- already exist and have no release yet.
30
-3. **Create the tag from** โ pick a branch when the tag does not exist yet.
31
- git-shark then creates an annotated tag on that branch's current commit as
32
- part of publishing. When the tag already exists, this choice is ignored.
33
-4. **Title** โ optional; defaults to the tag name.
34
-5. **Release notes** โ optional Markdown (headings, lists, tables, links).
28
+2. In the **Tag** group:
29
+ - **Tag name** โ the tag to publish, e.g. `v1.0.0`. The field suggests tags
30
+ that already exist and have no release yet.
31
+ - **Create the tag from** โ pick a branch when the tag does not exist yet.
32
+ git-shark then creates an annotated tag on that branch's current commit as
33
+ part of publishing. When the tag already exists, this choice is ignored.
34
+3. **Title** (optional) โ defaults to the tag name.
35
+4. **Release notes** (optional) โ Markdown (headings, lists, tables, links).
35
36
Embedded raw HTML is escaped, so notes can never inject markup into the page.
36
-6. **This is a pre-release** โ tick for a release candidate or beta.
37
+5. **This is a pre-release** โ tick for a release candidate or beta. It is
38
+ listed like any other release but never gets the **Latest** badge.
37
39
38
40
Publishing takes you straight to the new release page.
39
41
MODIFY
src/main/resources/META-INF/resources/shark.css
+116 -12
@@ -942,27 +942,131 @@
942
942
gap: var(--s2);
943
943
}
944
944
945
-/* release form: the target select and the pre-release checkbox sit inline with their label text,
946
- instead of stretching to the full field width like the text inputs above them */
947
-.release-target,
948
-.release-flag {
945
+/* Labelled single-column form (release pages). Every control carries a visible label plus optional
946
+ hint text tied to it via aria-describedby; field width signals the expected input length instead
947
+ of stretching everything to the column. */
948
+.stack-form {
949
949
display: flex;
950
- align-items: center;
951
- gap: var(--s2);
952
- font: 14px/1.4 var(--font);
950
+ flex-direction: column;
951
+ gap: var(--s5);
952
+ /* wide enough to make the notes box worth typing in, capped so the short inputs above it don't
953
+ stretch into fields far longer than the values they hold */
954
+ max-width: 1200px;
955
+ margin: 0 0 var(--s6);
956
+}
957
+
958
+.stack-form .field {
959
+ display: flex;
960
+ flex-direction: column;
961
+ gap: var(--s1);
962
+}
963
+
964
+.stack-form label {
965
+ font: 600 14px/1.4 var(--font);
966
+ color: var(--ink);
967
+}
968
+
969
+.stack-form .optional {
970
+ font-weight: 400;
953
971
color: var(--muted);
954
972
}
955
973
956
-.release-target select,
957
-.release-flag input[type="checkbox"] {
958
- width: auto;
959
- margin-top: 0;
974
+.stack-form .hint {
975
+ margin: 0;
976
+ font: 13px/1.45 var(--font);
977
+ color: var(--muted);
960
978
}
961
979
962
-.release-flag input[type="checkbox"] {
980
+.stack-form input,
981
+.stack-form textarea,
982
+.stack-form select {
983
+ margin-top: var(--s1);
984
+}
985
+
986
+.stack-form .field-short input,
987
+.stack-form .field-short select {
988
+ max-width: 15rem;
989
+}
990
+
991
+.stack-form .field-medium input {
992
+ max-width: 32rem;
993
+}
994
+
995
+/* the tag name and the ref it is cut from answer one question, so they share a group */
996
+.stack-form .field-group {
997
+ display: flex;
998
+ flex-direction: column;
999
+ gap: var(--s4);
1000
+ /* hug the fields instead of stretching the box across the whole form width */
1001
+ align-self: flex-start;
1002
+ margin: 0;
1003
+ padding: var(--s4) var(--s4) var(--s5);
1004
+ border: 1px solid var(--border);
1005
+ border-radius: var(--radius);
1006
+ background: var(--surface);
1007
+}
1008
+
1009
+.stack-form .field-row {
1010
+ flex-direction: row;
1011
+ flex-wrap: wrap;
1012
+ gap: var(--s5);
1013
+}
1014
+
1015
+/* the basis has to sit on the field, not the control: the hint paragraph would otherwise size the
1016
+ column and blow the row apart */
1017
+.stack-form .field-row > .field {
1018
+ flex: 1 1 15rem;
1019
+ max-width: 26rem;
1020
+}
1021
+
1022
+/* hints of different lengths leave the controls at different heights โ pushing them down lines the
1023
+ row of inputs up along its bottom edge */
1024
+.stack-form .field-row > .field input,
1025
+.stack-form .field-row > .field select {
1026
+ margin-top: auto;
1027
+ max-width: none;
1028
+}
1029
+
1030
+.stack-form .field-group legend {
1031
+ padding: 0 var(--s2);
1032
+ font: 600 13px/1 var(--font);
1033
+ color: var(--ink);
1034
+}
1035
+
1036
+.stack-form .check {
1037
+ display: flex;
1038
+ align-items: center;
1039
+ gap: var(--s2);
1040
+ /* the label wraps the box so the whole row is a target of at least 24px (WCAG 2.5.8) */
1041
+ min-height: 24px;
1042
+ cursor: pointer;
1043
+}
1044
+
1045
+.stack-form .check input[type="checkbox"] {
1046
+ flex: none;
1047
+ width: 16px;
1048
+ height: 16px;
1049
+ margin-top: 0;
963
1050
accent-color: var(--accent);
964
1051
}
965
1052
1053
+.stack-form .check-hint {
1054
+ padding-left: calc(16px + var(--s2));
1055
+}
1056
+
1057
+.stack-form .form-actions {
1058
+ margin-top: var(--s1);
1059
+}
1060
+
1061
+.form-cancel {
1062
+ font: 500 14px/1 var(--font);
1063
+ color: var(--muted);
1064
+}
1065
+
1066
+.form-cancel:hover {
1067
+ color: var(--accent-deep);
1068
+}
1069
+
966
1070
/* the discussion comment box and its submit button would otherwise touch */
967
1071
.comment-form .form-actions {
968
1072
margin-top: var(--s3);
MODIFY
src/main/resources/templates/ReleaseResource/editRelease.html
+24 -7
@@ -5,16 +5,33 @@
5
5
<section class="repo-main">
6
6
<p><a href="/repos/{repo.ownerHandle}/{repo.name}/releases/tag/{release.tagName}">โ {release.tagName}</a></p>
7
7
<h2>Edit release</h2>
8
- <form class="issue-form" method="post"
8
+ <form class="stack-form" method="post"
9
9
action="/repos/{repo.ownerHandle}/{repo.name}/releases/tag/{release.tagName}/edit">
10
- <input type="text" name="title" value="{release.title}" placeholder="Release title" autocomplete="off">
11
- <textarea name="body" placeholder="Release notes (Markdown, optional)" rows="10">{release.body ?: ''}</textarea>
12
- <label class="release-flag">
13
- <input type="checkbox" name="prerelease" {#if release.prerelease}checked{/if}> This is a pre-release
14
- </label>
10
+ <div class="field field-medium">
11
+ <label for="release-title">Title <span class="optional">(optional)</span></label>
12
+ <p class="hint" id="release-title-hint">Defaults to the tag name</p>
13
+ <input type="text" id="release-title" name="title" value="{release.title}" autocomplete="off"
14
+ aria-describedby="release-title-hint">
15
+ </div>
16
+ <div class="field">
17
+ <label for="release-body">Release notes <span class="optional">(optional)</span></label>
18
+ <p class="hint" id="release-body-hint">Markdown is supported โ raw HTML is escaped</p>
19
+ <textarea id="release-body" name="body" rows="18"
20
+ aria-describedby="release-body-hint">{release.body ?: ''}</textarea>
21
+ </div>
22
+ <div class="field">
23
+ <label class="check" for="release-prerelease">
24
+ <input type="checkbox" id="release-prerelease" name="prerelease"
25
+ aria-describedby="release-prerelease-hint" {#if release.prerelease}checked{/if}>
26
+ <span>This is a pre-release</span>
27
+ </label>
28
+ <p class="hint check-hint" id="release-prerelease-hint">Listed like any other release, but never gets
29
+ the Latest badge</p>
30
+ </div>
15
31
<div class="form-actions">
16
32
<button type="submit" class="btn btn-primary">Save release</button>
17
- <a class="btn btn-secondary" href="/repos/{repo.ownerHandle}/{repo.name}/releases/tag/{release.tagName}">Cancel</a>
33
+ <a class="form-cancel"
34
+ href="/repos/{repo.ownerHandle}/{repo.name}/releases/tag/{release.tagName}">Cancel</a>
18
35
</div>
19
36
</form>
20
37
</section>
MODIFY
src/main/resources/templates/ReleaseResource/newRelease.html
+45 -22
@@ -5,30 +5,53 @@
5
5
<section class="repo-main">
6
6
<p><a href="/repos/{repo.ownerHandle}/{repo.name}/releases">โ Releases</a></p>
7
7
<h2>New release</h2>
8
- <form class="issue-form" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/releases">
9
- <input type="text" name="tagName" placeholder="Tag (e.g. v1.0.0)" required autocomplete="off"
10
- list="release-tags">
11
- <datalist id="release-tags">
12
- {#for tag in untaggedTags}
13
- <option value="{tag}"></option>
14
- {/for}
15
- </datalist>
16
- <label class="release-target">
17
- Create the tag from
18
- <select name="target">
19
- {#for target in targets}
20
- <option value="{target}">{target}</option>
21
- {/for}
22
- </select>
23
- </label>
24
- <input type="text" name="title" placeholder="Release title (defaults to the tag)" autocomplete="off">
25
- <textarea name="body" placeholder="Release notes (Markdown, optional)" rows="10"></textarea>
26
- <label class="release-flag">
27
- <input type="checkbox" name="prerelease"> This is a pre-release
28
- </label>
8
+ <form class="stack-form" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/releases">
9
+ <fieldset class="field-group field-row">
10
+ <legend>Tag</legend>
11
+ <div class="field field-short">
12
+ <label for="release-tag">Tag name</label>
13
+ <p class="hint" id="release-tag-hint">Existing tags without a release are suggested as you type</p>
14
+ <input type="text" id="release-tag" name="tagName" placeholder="v1.0.0" required
15
+ autocomplete="off" aria-describedby="release-tag-hint" list="release-tags">
16
+ <datalist id="release-tags">
17
+ {#for tag in untaggedTags}
18
+ <option value="{tag}"></option>
19
+ {/for}
20
+ </datalist>
21
+ </div>
22
+ <div class="field field-short">
23
+ <label for="release-target">Create the tag from</label>
24
+ <p class="hint" id="release-target-hint">Only used when the tag does not exist yet</p>
25
+ <select id="release-target" name="target" aria-describedby="release-target-hint">
26
+ {#for target in targets}
27
+ <option value="{target}">{target}</option>
28
+ {/for}
29
+ </select>
30
+ </div>
31
+ </fieldset>
32
+ <div class="field field-medium">
33
+ <label for="release-title">Title <span class="optional">(optional)</span></label>
34
+ <p class="hint" id="release-title-hint">Defaults to the tag name</p>
35
+ <input type="text" id="release-title" name="title" autocomplete="off"
36
+ aria-describedby="release-title-hint">
37
+ </div>
38
+ <div class="field">
39
+ <label for="release-body">Release notes <span class="optional">(optional)</span></label>
40
+ <p class="hint" id="release-body-hint">Markdown is supported โ raw HTML is escaped</p>
41
+ <textarea id="release-body" name="body" rows="18" aria-describedby="release-body-hint"></textarea>
42
+ </div>
43
+ <div class="field">
44
+ <label class="check" for="release-prerelease">
45
+ <input type="checkbox" id="release-prerelease" name="prerelease"
46
+ aria-describedby="release-prerelease-hint">
47
+ <span>This is a pre-release</span>
48
+ </label>
49
+ <p class="hint check-hint" id="release-prerelease-hint">Listed like any other release, but never gets
50
+ the Latest badge</p>
51
+ </div>
29
52
<div class="form-actions">
30
53
<button type="submit" class="btn btn-primary">Publish release</button>
31
- <a class="btn btn-secondary" href="/repos/{repo.ownerHandle}/{repo.name}/releases">Cancel</a>
54
+ <a class="form-cancel" href="/repos/{repo.ownerHandle}/{repo.name}/releases">Cancel</a>
32
55
</div>
33
56
</form>
34
57
</section>
MODIFY
src/test/java/de/workaround/web/ReleaseUiTest.java
+63 -0
@@ -7,6 +7,8 @@
7
7
import java.util.Map;
8
8
import java.util.zip.GZIPInputStream;
9
9
import java.util.zip.ZipEntry;
10
+import java.util.regex.Matcher;
11
+import java.util.regex.Pattern;
10
12
import java.util.zip.ZipInputStream;
11
13
12
14
import org.junit.jupiter.api.Test;
@@ -25,6 +27,7 @@
25
27
import static org.hamcrest.CoreMatchers.containsString;
26
28
import static org.hamcrest.CoreMatchers.not;
27
29
import static org.junit.jupiter.api.Assertions.assertEquals;
30
+import static org.junit.jupiter.api.Assertions.assertFalse;
28
31
import static org.junit.jupiter.api.Assertions.assertTrue;
29
32
30
33
@QuarkusTest
@@ -40,6 +43,66 @@
40
43
User.Repo users;
41
44
42
45
@Test
46
+ @TestSecurity(user = "rel-form-owner")
47
+ void newReleaseFormLabelsEveryControlAndKeepsPlaceholdersAsExamplesOnly() throws Exception
48
+ {
49
+ User owner = persistUser("rel-form-owner");
50
+ seededRepo(owner, "formrepo");
51
+
52
+ String html = given().when().get("/repos/" + owner.username + "/formrepo/releases/new")
53
+ .then().statusCode(200)
54
+ .extract().body().asString();
55
+
56
+ for (String id : List.of("release-tag", "release-target", "release-title", "release-body", "release-prerelease"))
57
+ {
58
+ assertTrue(html.contains("for=\"" + id + "\""), "no label bound to " + id);
59
+ assertTrue(html.contains("id=\"" + id + "\""), "no control with id " + id);
60
+ }
61
+
62
+ assertTrue(html.contains("id=\"release-body-hint\"") && html.contains("aria-describedby=\"release-body-hint\""),
63
+ "the release-notes hint must be tied to the textarea via aria-describedby");
64
+
65
+ assertTrue(html.contains("placeholder=\"v1.0.0\""), "the tag field keeps a format example as placeholder");
66
+ assertFalse(html.contains("placeholder=\"Release title"), "the title label must not live in a placeholder");
67
+ assertFalse(html.contains("placeholder=\"Release notes"), "the notes label must not live in a placeholder");
68
+ assertFalse(html.contains("placeholder=\"Tag (e.g."), "the tag label must not live in a placeholder");
69
+
70
+ assertTrue(html.contains("<legend>"), "tag name and target belong to one labelled group");
71
+
72
+ assertTrue(html.contains("class=\"btn btn-primary\""), "the form needs exactly one primary action");
73
+ assertFalse(html.contains("btn btn-secondary"), "cancel must be a link, not a second competing button");
74
+ }
75
+
76
+ @Test
77
+ @TestSecurity(user = "rel-width-owner")
78
+ void newReleaseFormSpreadsAcrossTheContentColumn() throws Exception
79
+ {
80
+ User owner = persistUser("rel-width-owner");
81
+ seededRepo(owner, "widerepo");
82
+
83
+ String html = given().when().get("/repos/" + owner.username + "/widerepo/releases/new")
84
+ .then().statusCode(200)
85
+ .extract().body().asString();
86
+
87
+ assertTrue(html.contains("field-group field-row"),
88
+ "tag name and target must sit next to each other instead of stacking in a narrow column");
89
+
90
+ String css = given().when().get("/shark.css")
91
+ .then().statusCode(200)
92
+ .extract().body().asString();
93
+
94
+ int start = css.indexOf(".stack-form {");
95
+ assertTrue(start >= 0, ".stack-form rule missing from the stylesheet");
96
+ String rule = css.substring(start, css.indexOf('}', start));
97
+ Matcher cap = Pattern.compile("max-width:\\s*(\\d+)px").matcher(rule);
98
+ assertTrue(cap.find(), "the form declares no max-width: " + rule);
99
+ assertTrue(Integer.parseInt(cap.group(1)) >= 1000,
100
+ "the form must span the content column, not a narrow reading column: " + rule);
101
+
102
+ assertTrue(css.contains(".stack-form .field-row"), "the row layout for grouped fields is not defined");
103
+ }
104
+
105
+ @Test
43
106
@TestSecurity(user = "rel-ui-owner")
44
107
void ownerCanPublishBrowseEditAndDeleteARelease() throws Exception
45
108
{