gitshark

Clone repository

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

← Commits

πŸ’„ (issues): Redesign issue page with a GitHub-style assignee picker

a7ab65ce670a0aa9ba745238273ae832086a13b1 Β· Phillip Souza Furtner Β· 2026-07-14T08:26:47Z

Changes

5 files changed, +438 -88

ADD src/main/java/de/workaround/web/Assets.java +22 -0
diff --git a/src/main/java/de/workaround/web/Assets.java b/src/main/java/de/workaround/web/Assets.java
new file mode 100644
index 0000000..7d5dcd9
--- /dev/null
+++ b/src/main/java/de/workaround/web/Assets.java
@@ -0,0 +1,22 @@
1 +package de.workaround.web;
2 +
3 +import jakarta.enterprise.context.ApplicationScoped;
4 +import jakarta.inject.Named;
5 +
6 +/**
7 + * Cache-busting token for static assets (CSS/JS). Fixed once at application startup, so the value
8 + * changes on every deploy β€” and on every dev live-reload restart β€” but stays stable while the app
9 + * runs. Appended as {@code ?v=...} to asset links in the layout so browsers fetch a changed file
10 + * instead of serving a stale cached copy, without disabling caching between deploys.
11 + */
12 +@Named("assets")
13 +@ApplicationScoped
14 +public class Assets
15 +{
16 + private final String version = Long.toString(System.currentTimeMillis(), 36);
17 +
18 + public String version()
19 + {
20 + return version;
21 + }
22 +}
MODIFY src/main/resources/META-INF/resources/shark.css +289 -36
diff --git a/src/main/resources/META-INF/resources/shark.css b/src/main/resources/META-INF/resources/shark.css
index bbdee1b..939f6ed 100644
--- a/src/main/resources/META-INF/resources/shark.css
+++ b/src/main/resources/META-INF/resources/shark.css
@@ -915,25 +915,6 @@
915 915 font-size: 13px;
916 916 }
917 917
918 -/* single management bar under the issue body: edit, status transitions, delete pushed right */
919 -.issue-manage {
920 - display: flex;
921 - align-items: center;
922 - flex-wrap: wrap;
923 - gap: var(--s3);
924 - margin-top: var(--s5);
925 - padding-top: var(--s4);
926 - border-top: 1px solid var(--border-soft);
927 -}
928 -
929 -.issue-manage .issue-actions {
930 - margin: 0;
931 -}
932 -
933 -.issue-manage .issue-remove {
934 - margin-left: auto;
935 -}
936 -
937 918 /* the issue description sits in its own surface card (same look as the README card) so the
938 919 content stands out from the page background */
939 920 .issue-desc {
@@ -958,8 +939,51 @@
958 939 font: 500 12.5px/1 var(--mono);
959 940 }
960 941
961 -/* issue detail meta line: status badge plus who opened and who is assigned, each with an avatar */
962 -.issue-meta {
942 +/* Issue detail: main column + a narrow metadata sidebar (status, assignee), the layout
943 + pattern people expect from an issue tracker. Collapses to one column on narrow screens. */
944 +.issue-back {
945 + margin: 0 0 var(--s4);
946 +}
947 +
948 +.issue-detail {
949 + display: grid;
950 + grid-template-columns: minmax(0, 1fr) 244px;
951 + gap: var(--s6);
952 + align-items: start;
953 +}
954 +
955 +.issue-content {
956 + min-width: 0;
957 +}
958 +
959 +/* title sits opposite the edit/delete cluster; both share one baseline row */
960 +.issue-header {
961 + display: flex;
962 + align-items: flex-start;
963 + gap: var(--s4);
964 +}
965 +
966 +.issue-heading {
967 + margin: 0;
968 + flex: 1;
969 + min-width: 0;
970 + line-height: 1.2;
971 + overflow-wrap: anywhere;
972 +}
973 +
974 +.issue-heading .issue-no {
975 + margin-left: 6px;
976 + vertical-align: middle;
977 +}
978 +
979 +.issue-header-actions {
980 + display: flex;
981 + align-items: center;
982 + gap: var(--s2);
983 + flex: none;
984 +}
985 +
986 +.issue-byline {
963 987 display: flex;
964 988 align-items: center;
965 989 flex-wrap: wrap;
@@ -975,6 +999,245 @@
975 999 gap: 6px;
976 1000 }
977 1001
1002 +.issue-person strong {
1003 + color: var(--ink);
1004 + font-weight: 600;
1005 +}
1006 +
1007 +.issue-nodesc {
1008 + margin: var(--s4) 0 var(--s5);
1009 +}
1010 +
1011 +/* metadata sidebar: quiet labelled blocks separated by hairlines */
1012 +.issue-side {
1013 + display: flex;
1014 + flex-direction: column;
1015 +}
1016 +
1017 +.issue-side-block {
1018 + padding: var(--s4) 0;
1019 + border-bottom: 1px solid var(--border-soft);
1020 +}
1021 +
1022 +.issue-side-block:first-child {
1023 + padding-top: 0;
1024 +}
1025 +
1026 +.issue-side-block:last-child {
1027 + border-bottom: none;
1028 +}
1029 +
1030 +.issue-side-label {
1031 + margin: 0 0 var(--s3);
1032 + font: 600 11px/1 var(--font);
1033 + text-transform: uppercase;
1034 + letter-spacing: .07em;
1035 + color: var(--muted);
1036 +}
1037 +
1038 +/* status as a segmented control: the current state reads as pressed, the others as choices */
1039 +.status-picker {
1040 + display: inline-flex;
1041 + padding: 3px;
1042 + gap: 2px;
1043 + background: var(--canvas);
1044 + border: 1px solid var(--border);
1045 + border-radius: var(--radius-sm);
1046 +}
1047 +
1048 +.status-opt {
1049 + flex: 1;
1050 + padding: 6px 10px;
1051 + border: 1px solid transparent;
1052 + border-radius: 6px;
1053 + background: transparent;
1054 + color: var(--muted);
1055 + font: 600 12px/1 var(--font);
1056 + white-space: nowrap;
1057 + cursor: pointer;
1058 + transition: background .12s, color .12s, box-shadow .12s;
1059 +}
1060 +
1061 +.status-opt:hover {
1062 + color: var(--accent-deep);
1063 + background: var(--accent-soft);
1064 +}
1065 +
1066 +.status-opt.is-current {
1067 + background: var(--surface);
1068 + color: var(--ink);
1069 + border-color: var(--border);
1070 + box-shadow: 0 1px 2px rgba(20, 32, 30, .06);
1071 + cursor: default;
1072 +}
1073 +
1074 +.status-opt.is-current:hover {
1075 + background: var(--surface);
1076 +}
1077 +
1078 +.status-opt:focus-visible {
1079 + outline: 2px solid var(--accent);
1080 + outline-offset: 1px;
1081 +}
1082 +
1083 +/* current assignee (avatar + handle, or an empty-state line) β€” used read-only for non-owners */
1084 +.assignee-current {
1085 + display: flex;
1086 + align-items: center;
1087 + gap: var(--s2);
1088 + font-size: 14px;
1089 +}
1090 +
1091 +.assignee-name {
1092 + font-weight: 600;
1093 + color: var(--ink);
1094 +}
1095 +
1096 +/* Assignee popover (GitHub-style): the row is the trigger; clicking it opens a floating menu of
1097 + people to pick from. JS-free via <details> β€” the menu is absolutely positioned so it overlays
1098 + the page instead of pushing content down. */
1099 +.assignee-pop {
1100 + position: relative;
1101 +}
1102 +
1103 +.assignee-trigger {
1104 + display: flex;
1105 + align-items: center;
1106 + gap: var(--s2);
1107 + font-size: 14px;
1108 + list-style: none;
1109 + cursor: pointer;
1110 + padding: 6px 8px;
1111 + margin: 0 -8px;
1112 + border-radius: var(--radius-sm);
1113 + transition: background .12s;
1114 +}
1115 +
1116 +.assignee-trigger::-webkit-details-marker {
1117 + display: none;
1118 +}
1119 +
1120 +.assignee-trigger:hover {
1121 + background: var(--accent-soft);
1122 +}
1123 +
1124 +.assignee-trigger:focus-visible {
1125 + outline: 2px solid var(--accent);
1126 + outline-offset: 1px;
1127 +}
1128 +
1129 +.assignee-caret {
1130 + margin-left: auto;
1131 + color: var(--faint);
1132 + font-size: 12px;
1133 + transition: transform .12s, color .12s;
1134 +}
1135 +
1136 +.assignee-trigger:hover .assignee-caret {
1137 + color: var(--accent-deep);
1138 +}
1139 +
1140 +.assignee-pop[open] .assignee-caret {
1141 + transform: rotate(180deg);
1142 + color: var(--accent-deep);
1143 +}
1144 +
1145 +.assignee-menu {
1146 + position: absolute;
1147 + z-index: 30;
1148 + top: calc(100% + 6px);
1149 + left: 0;
1150 + right: 0;
1151 + min-width: 240px;
1152 + padding: var(--s2);
1153 + background: var(--surface);
1154 + border: 1px solid var(--border);
1155 + border-radius: var(--radius);
1156 + box-shadow: 0 8px 28px rgba(20, 32, 30, .14);
1157 +}
1158 +
1159 +.assignee-menu-head {
1160 + margin: 4px 8px 8px;
1161 + font: 600 12px/1 var(--font);
1162 + color: var(--muted);
1163 +}
1164 +
1165 +.assignee-search {
1166 + margin-bottom: var(--s2);
1167 +}
1168 +
1169 +.assignee-search input {
1170 + width: 100%;
1171 + padding: 7px 10px;
1172 + border: 1px solid var(--border);
1173 + border-radius: var(--radius-sm);
1174 + background: var(--surface);
1175 + color: var(--ink);
1176 + font: 14px/1.2 var(--font);
1177 +}
1178 +
1179 +.assignee-search input:focus-visible {
1180 + outline: none;
1181 + border-color: var(--accent);
1182 + box-shadow: 0 0 0 3px var(--accent-soft);
1183 +}
1184 +
1185 +.assignee-options {
1186 + list-style: none;
1187 + margin: 0;
1188 + padding: 0;
1189 + border-top: 1px solid var(--border-soft);
1190 + padding-top: var(--s1);
1191 + max-height: 260px;
1192 + overflow-y: auto;
1193 +}
1194 +
1195 +/* each person is a full-width click target that assigns on submit */
1196 +.assignee-option {
1197 + display: flex;
1198 + align-items: center;
1199 + gap: var(--s2);
1200 + width: 100%;
1201 + padding: 6px 8px;
1202 + border: none;
1203 + background: none;
1204 + border-radius: var(--radius-sm);
1205 + color: var(--ink);
1206 + font: 14px/1.2 var(--font);
1207 + text-align: left;
1208 + cursor: pointer;
1209 +}
1210 +
1211 +.assignee-option:hover {
1212 + background: var(--accent-soft);
1213 +}
1214 +
1215 +.assignee-option.is-selected {
1216 + font-weight: 600;
1217 +}
1218 +
1219 +.assignee-check {
1220 + width: 14px;
1221 + flex: none;
1222 + color: var(--success-deep);
1223 + font-size: 12px;
1224 +}
1225 +
1226 +/* clear sits below the list, set apart by a hairline and a quiet danger tone */
1227 +.assignee-clear {
1228 + border-top: 1px solid var(--border-soft);
1229 + margin-top: var(--s1);
1230 + padding-top: var(--s1);
1231 +}
1232 +
1233 +.assignee-clear-btn {
1234 + color: var(--danger);
1235 +}
1236 +
1237 +.assignee-clear-btn:hover {
1238 + background: var(--danger-bg);
1239 +}
1240 +
978 1241 /* assignee avatar pinned to the right of an issue list row */
979 1242 .frow-assignee {
980 1243 margin-left: auto;
@@ -984,21 +1247,11 @@
984 1247 flex: none;
985 1248 }
986 1249
987 -/* assignee autocomplete control inside the management bar */
988 -.issue-assign {
989 - display: flex;
990 - align-items: center;
991 - gap: var(--s2);
992 -}
993 -
994 -.issue-assign .lbl {
995 - color: var(--muted);
996 - font-size: 13px;
997 -}
998 -
999 -.issue-assign input {
1000 - width: 150px;
1001 - max-width: 40vw;
1250 +@media (max-width: 900px) {
1251 + .issue-detail {
1252 + grid-template-columns: 1fr;
1253 + gap: var(--s5);
1254 + }
1002 1255 }
1003 1256
1004 1257 /* merge requests */
MODIFY src/main/resources/templates/IssueResource/issue.html +100 -43
diff --git a/src/main/resources/templates/IssueResource/issue.html b/src/main/resources/templates/IssueResource/issue.html
index 06a1435..65b9de0 100644
--- a/src/main/resources/templates/IssueResource/issue.html
+++ b/src/main/resources/templates/IssueResource/issue.html
@@ -3,50 +3,107 @@
3 3 <div class="repo-layout">
4 4 {#include RepositoryResource/sidebar nav=nav active='issues' /}
5 5 <section class="repo-main">
6 - <p><a href="/repos/{repo.ownerHandle}/{repo.name}/issues">← Issues</a></p>
7 - <h2>{issue.title} <span class="issue-no">#{issue.number}</span></h2>
8 - <div class="issue-meta">
9 - <span class="badge status-{issue.status}">{issue.status.label}</span>
10 - <span class="issue-person">opened by {#avatar user=issue.author /} <span>{issue.author.username}</span></span>
11 - {#if issue.assignee}
12 - <span class="issue-person">assigned to {#avatar user=issue.assignee /} <span>{issue.assignee.username}</span></span>
13 - {#else}
14 - <span class="issue-person muted">unassigned</span>
15 - {/if}
6 + <p class="issue-back"><a href="/repos/{repo.ownerHandle}/{repo.name}/issues">← Issues</a></p>
7 + <div class="issue-detail">
8 + <div class="issue-content">
9 + <header class="issue-header">
10 + <h2 class="issue-heading">{issue.title} <span class="issue-no">#{issue.number}</span></h2>
11 + {#if owner}
12 + <div class="issue-header-actions">
13 + <a class="btn btn-secondary btn-sm" href="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/edit">Edit</a>
14 + <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/delete"
15 + onsubmit="return confirm('Delete this issue? This cannot be undone.')">
16 + <button type="submit" class="btn btn-danger btn-sm">Delete</button>
17 + </form>
18 + </div>
19 + {/if}
20 + </header>
21 + <div class="issue-byline">
22 + <span class="badge status-{issue.status}">{issue.status.label}</span>
23 + <span class="issue-person">{#avatar user=issue.author /} <span><strong>{issue.author.username}</strong> opened this issue</span></span>
24 + </div>
25 + {#if descriptionHtml}
26 + <div class="panel issue-desc">
27 + <div class="readme-body">{descriptionHtml.raw}</div>
28 + </div>
29 + {#else}
30 + <p class="issue-nodesc muted">No description provided.</p>
31 + {/if}
32 + </div>
33 +
34 + <aside class="issue-side">
35 + <section class="issue-side-block">
36 + <h3 class="issue-side-label">Status</h3>
37 + {#if owner}
38 + <form class="status-picker" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/status">
39 + {#for status in statuses}
40 + <button type="submit" name="status" value="{status}"
41 + class="status-opt status-opt-{status}{#if status == issue.status} is-current{/if}"
42 + {#if status == issue.status}aria-current="true" disabled{/if}>{status.label}</button>
43 + {/for}
44 + </form>
45 + {#else}
46 + <span class="badge status-{issue.status}">{issue.status.label}</span>
47 + {/if}
48 + </section>
49 +
50 + <section class="issue-side-block">
51 + <h3 class="issue-side-label">Assignee</h3>
52 + {#if owner}
53 + {! GitHub-style: the row is the trigger; clicking it opens a floating pick-a-person menu !}
54 + <details class="assignee-pop">
55 + <summary class="assignee-trigger">
56 + {#if issue.assignee}
57 + {#avatar user=issue.assignee /} <span class="assignee-name">{issue.assignee.username}</span>
58 + {#else}
59 + <span class="muted">No one assigned</span>
60 + {/if}
61 + <span class="assignee-caret" aria-hidden="true">β–Ύ</span>
62 + </summary>
63 + <div class="assignee-menu">
64 + <p class="assignee-menu-head">Assign this issue</p>
65 + {! free text handles any user; Enter submits. datalist offers the same names as the list !}
66 + <form class="assignee-search" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/assign">
67 + <input type="text" name="assignee" list="assignees" placeholder="Type a username…" autocomplete="off">
68 + <datalist id="assignees">
69 + {#for candidate in assignees}
70 + <option value="{candidate.username}">
71 + {/for}
72 + </datalist>
73 + </form>
74 + <ul class="assignee-options">
75 + {#for candidate in assignees}
76 + <li>
77 + <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/assign">
78 + <button type="submit" name="assignee" value="{candidate.username}"
79 + class="assignee-option{#if issue.assignee && candidate.username == issue.assignee.username} is-selected{/if}">
80 + <span class="assignee-check" aria-hidden="true">{#if issue.assignee && candidate.username == issue.assignee.username}βœ“{/if}</span>
81 + {#avatar user=candidate /} <span>{candidate.username}</span>
82 + </button>
83 + </form>
84 + </li>
85 + {/for}
86 + </ul>
87 + {#if issue.assignee}
88 + {! separate form with no assignee field: posts blank, which unassigns the issue !}
89 + <form class="assignee-clear" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/assign">
90 + <button type="submit" class="assignee-option assignee-clear-btn">Clear assignee</button>
91 + </form>
92 + {/if}
93 + </div>
94 + </details>
95 + {#else}
96 + <div class="assignee-current">
97 + {#if issue.assignee}
98 + {#avatar user=issue.assignee /} <span class="assignee-name">{issue.assignee.username}</span>
99 + {#else}
100 + <span class="muted">No one assigned</span>
101 + {/if}
102 + </div>
103 + {/if}
104 + </section>
105 + </aside>
16 106 </div>
17 - {#if descriptionHtml}
18 - <div class="panel issue-desc">
19 - <div class="readme-body">{descriptionHtml.raw}</div>
20 - </div>
21 - {#else}
22 - <p class="muted">No description provided.</p>
23 - {/if}
24 - {#if owner}
25 - <div class="issue-manage">
26 - <a class="btn btn-secondary btn-sm" href="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/edit">Edit issue</a>
27 - <form class="issue-actions" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/status">
28 - <span class="lbl">Move to</span>
29 - {#for status in statuses}
30 - <button type="submit" class="btn btn-secondary btn-sm" name="status" value="{status}"{#if status == issue.status} disabled{/if}>{status.label}</button>
31 - {/for}
32 - </form>
33 - <form class="issue-assign" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/assign">
34 - <span class="lbl">Assignee</span>
35 - <input type="text" name="assignee" list="assignees" placeholder="username" autocomplete="off"
36 - value="{issue.assignee.username ?: ''}">
37 - <datalist id="assignees">
38 - {#for candidate in assignees}
39 - <option value="{candidate.username}">
40 - {/for}
41 - </datalist>
42 - <button type="submit" class="btn btn-secondary btn-sm">Assign</button>
43 - </form>
44 - <form class="issue-remove" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/issues/{issue.number}/delete"
45 - onsubmit="return confirm('Delete this issue? This cannot be undone.')">
46 - <button type="submit" class="btn btn-danger btn-sm">Delete issue</button>
47 - </form>
48 - </div>
49 - {/if}
50 107 </section>
51 108 </div>
52 109 {/include}
MODIFY src/main/resources/templates/layout.html +2 -2
diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html
index ac77942..3ec3ab6 100644
--- a/src/main/resources/templates/layout.html
+++ b/src/main/resources/templates/layout.html
@@ -5,8 +5,8 @@
5 5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 6 <title>{#insert title}git-shark{/}</title>
7 7 <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8 - <link rel="stylesheet" href="/shark.css">
9 - <script defer src="/shark-hotkeys.js"></script>
8 + <link rel="stylesheet" href="/shark.css?v={cdi:assets.version}">
9 + <script defer src="/shark-hotkeys.js?v={cdi:assets.version}"></script>
10 10 </head>
11 11 <body class="{cdi:currentUser.contentWidthClass}">
12 12 <header class="site">
MODIFY src/test/java/de/workaround/web/IssueUiTest.java +25 -7
diff --git a/src/test/java/de/workaround/web/IssueUiTest.java b/src/test/java/de/workaround/web/IssueUiTest.java
index 12ad70f..608194d 100644
--- a/src/test/java/de/workaround/web/IssueUiTest.java
+++ b/src/test/java/de/workaround/web/IssueUiTest.java
@@ -87,9 +87,12 @@
87 87 collaboratorService.add(owner, repo, collab.username);
88 88 Issue issue = issueService.create(owner, repo, "Assign me", null);
89 89
90 - // the assign input is backed by a datalist offering the owner and every collaborator
90 + // the assignee is a floating popover (click the row to open) with a click-to-assign people list
91 + // of the owner + collaborators, plus a free-text field backed by a datalist
91 92 given().when().get("/repos/" + owner.username + "/assignable/issues/" + issue.number)
92 93 .then().statusCode(200)
94 + .body(containsString("<details class=\"assignee-pop\""))
95 + .body(containsString("class=\"assignee-option"))
93 96 .body(containsString("<datalist id=\"assignees\""))
94 97 .body(containsString("list=\"assignees\""))
95 98 .body(containsString("value=\"iss-assign\""))
@@ -105,10 +108,11 @@
105 108 Issue issue = issueService.create(owner, repo, "Has an owner", null);
106 109 issueService.assign(owner, issue, owner.username);
107 110
108 - // the assignee is named and rendered through the avatar tag (initials fallback here, no uploaded image)
111 + // the assignee is named in the metadata sidebar and rendered through the avatar tag
112 + // (initials fallback here, since the test user has no uploaded image)
109 113 given().when().get("/repos/" + owner.username + "/avassign/issues/" + issue.number)
110 114 .then().statusCode(200)
111 - .body(containsString("assigned to"))
115 + .body(containsString("class=\"assignee-name\""))
112 116 .body(containsString("class=\"av-fallback\""));
113 117 }
114 118
@@ -130,7 +134,7 @@
130 134 .header("Location", org.hamcrest.Matchers.endsWith(url));
131 135 given().when().get(url)
132 136 .then().statusCode(200)
133 - .body(containsString("assigned to"))
137 + .body(containsString("class=\"assignee-name\""))
134 138 .body(containsString(helper.username));
135 139
136 140 // posting a blank username clears the assignment again
@@ -138,7 +142,7 @@
138 142 .contentType("application/x-www-form-urlencoded").formParam("assignee", "")
139 143 .when().post(url + "/assign")
140 144 .then().statusCode(303);
141 - given().when().get(url).then().statusCode(200).body(containsString("unassigned"));
145 + given().when().get(url).then().statusCode(200).body(containsString("No one assigned"));
142 146 }
143 147
144 148 @Test
@@ -185,6 +189,20 @@
185 189 }
186 190
187 191 @Test
192 + @TestSecurity(user = "iss-assets")
193 + void stylesheetLinkIsCacheBusted()
194 + {
195 + // every page shares layout.html; the shark.css link must carry a ?v= token so browsers pick up
196 + // CSS changes after a deploy instead of serving a stale cached copy
197 + User owner = persistUser("iss-assets");
198 + service.create(owner, "assetsrepo", Repository.Visibility.PUBLIC, null);
199 +
200 + given().when().get("/repos/" + owner.username + "/assetsrepo/issues")
201 + .then().statusCode(200)
202 + .body(containsString("/shark.css?v="));
203 + }
204 +
205 + @Test
188 206 @TestSecurity(user = "iss-md")
189 207 void issueDescriptionRendersMarkdown()
190 208 {
@@ -337,11 +355,11 @@
337 355 String base = "/repos/" + owner.username + "/editable/issues";
338 356 String editUrl = base + "/" + issue.number + "/edit";
339 357
340 - // the detail page links to the edit form from a single management bar
358 + // the detail page links to the edit form from the header action cluster
341 359 given().when().get(base + "/" + issue.number)
342 360 .then().statusCode(200)
343 361 .body(containsString(editUrl))
344 - .body(containsString("class=\"issue-manage\""));
362 + .body(containsString("class=\"issue-header-actions\""));
345 363
346 364 // the edit form is pre-filled with the current title and description
347 365 given().when().get(editUrl)

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog