@nanocollective/roster 0.1.0-alpha.44 → 0.1.0-alpha.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/portal.md
CHANGED
|
@@ -87,9 +87,9 @@ Where the portal opens. An Ask box at the top, then these, in order.
|
|
|
87
87
|
- **Latest reports.** Each staff member's run report from the last day: the three lines they
|
|
88
88
|
post on their status issue at the end of a daily run. Nothing shows until there is one.
|
|
89
89
|
- **Needs you.** Every ask a staff member has put on you, and every pull request ready to
|
|
90
|
-
merge, oldest first. Each card has
|
|
91
|
-
- a `decision` has
|
|
92
|
-
|
|
90
|
+
merge, oldest first. Each card has one action on its right:
|
|
91
|
+
- a `decision` has **Reply**, and **Go with the default** when it carries one. The default
|
|
92
|
+
and its date are shown, and turn amber once the date has passed.
|
|
93
93
|
- a `review` has **Approve**.
|
|
94
94
|
- a `chore` has **Done**, which closes it.
|
|
95
95
|
- a pull request has **Merge**, unless its checks fail, are still running, or it conflicts.
|
|
@@ -106,10 +106,14 @@ Where the portal opens. An Ask box at the top, then these, in order.
|
|
|
106
106
|
- **Closed today.** What the staff closed today, with the line they closed it with, and
|
|
107
107
|
**Reopen** beside each. Staff close finished issues themselves, so this is where you check.
|
|
108
108
|
|
|
109
|
+
**Every item opens in a side sheet.** Click anywhere on a card or a row and its thread opens
|
|
110
|
+
on the right: the conversation, Reply, Close or Reopen, Merge for a pull request, and Open in
|
|
111
|
+
GitHub. Escape or the × closes it, and Home repaints where you were.
|
|
112
|
+
|
|
109
113
|
**Nothing is missed.** Every open issue and pull request has exactly one place: on Home, or on
|
|
110
114
|
the staff member's own tracker (their status issue, a peer's ask, their own work), or elsewhere
|
|
111
115
|
(draft pull requests, contributor issues). The line at the bottom counts the last two, with a
|
|
112
|
-
link to Trackers.
|
|
116
|
+
link to Trackers.
|
|
113
117
|
|
|
114
118
|
## Trackers
|
|
115
119
|
|
package/package.json
CHANGED
|
@@ -155,6 +155,7 @@ svg.ic{width:var(--ic,15px);height:var(--ic,15px);display:block;flex:none}
|
|
|
155
155
|
.chip.hot{color:var(--err);background:color-mix(in srgb,var(--err) 12%,transparent)}
|
|
156
156
|
.chip.warm{color:var(--warn);background:color-mix(in srgb,var(--warn) 13%,transparent)}
|
|
157
157
|
.chip.cool{color:var(--ruled);background:color-mix(in srgb,var(--ruled) 12%,transparent)}
|
|
158
|
+
.chip.ok{color:var(--accent);background:var(--accent-soft)}
|
|
158
159
|
.pill{font:11px var(--mono);color:var(--ink-dim);border:1px solid transparent;
|
|
159
160
|
background:color-mix(in srgb,var(--ink) 4%,transparent);
|
|
160
161
|
border-radius:999px;padding:2px 9px;font-variant-numeric:tabular-nums}
|
|
@@ -284,6 +285,16 @@ dialog.sheet{border:1px solid var(--border-strong);border-radius:var(--r-lg);bac
|
|
|
284
285
|
dialog.sheet[open]{animation:dlgin .18s var(--ease)}
|
|
285
286
|
dialog.sheet::backdrop{background:rgba(0,0,0,.35);backdrop-filter:blur(8px)}
|
|
286
287
|
.sheetbody{padding:22px 24px 26px;overflow:auto;max-height:calc(100vh - 50px)}
|
|
288
|
+
|
|
289
|
+
/* The side sheet: a thread opened from Home, full height on the right. */
|
|
290
|
+
dialog.sheet.side{margin:0 0 0 auto;height:100vh;max-height:100vh;width:min(780px,100vw);
|
|
291
|
+
border-radius:0;border-width:0 0 0 1px}
|
|
292
|
+
dialog.sheet.side[open]{animation:sidein .22s var(--ease)}
|
|
293
|
+
dialog.sheet.side .sheetbody{max-height:100vh;height:100%;box-sizing:border-box;padding:18px 22px 40px}
|
|
294
|
+
@keyframes sidein{from{transform:translateX(24px);opacity:0}}
|
|
295
|
+
@media (prefers-reduced-motion:reduce){dialog.sheet.side[open]{animation:none}}
|
|
296
|
+
.sidebar-x{position:sticky;top:0;display:flex;justify-content:flex-end;margin:-6px -6px 4px;z-index:1}
|
|
297
|
+
dialog.sheet.side .viewer{border:0;box-shadow:none;padding:0;min-height:0;background:none}
|
|
287
298
|
.sheettitle{margin:0 0 14px;font:600 18px var(--sans);letter-spacing:-.02em}
|
|
288
299
|
.sheetloading{display:grid;place-items:center;min-height:260px}
|
|
289
300
|
.spinner{width:22px;height:22px;border-radius:50%;border:2px solid var(--line);border-top-color:var(--accent);
|
|
@@ -15,31 +15,54 @@
|
|
|
15
15
|
.homewho{font:500 12.5px var(--sans);color:var(--ink);background:var(--surface-2);border:0;
|
|
16
16
|
border-radius:8px;padding:5px 8px}
|
|
17
17
|
|
|
18
|
-
.homecard{padding:14px 16px
|
|
18
|
+
.homecard{padding:14px 16px;cursor:pointer;display:grid;grid-template-columns:1fr auto;
|
|
19
|
+
align-items:center;gap:10px 18px}
|
|
20
|
+
.homeabout{min-width:0}
|
|
21
|
+
.homecard:hover{border-color:var(--border-strong)}
|
|
22
|
+
.homecard:focus-visible{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
|
|
19
23
|
.homehead{display:flex;align-items:baseline;gap:8px}
|
|
20
24
|
.homehead .chip{flex:none}
|
|
21
25
|
.homeopen{all:unset;cursor:pointer;font:600 14px/1.35 var(--sans);color:var(--ink);letter-spacing:-.01em}
|
|
22
26
|
.homeopen:hover{text-decoration:underline}
|
|
23
27
|
.homeopen:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
|
|
24
|
-
.
|
|
28
|
+
.homeabout > .meta{margin-top:4px}
|
|
25
29
|
.homedue{margin:8px 0 0;font-size:12.5px;color:var(--ink-dim)}
|
|
26
30
|
.homedue.late{color:var(--warn)}
|
|
27
|
-
.homeacts{
|
|
31
|
+
.homeacts{justify-content:flex-end;flex-wrap:nowrap}
|
|
32
|
+
.homeacts .meta{white-space:normal;text-align:right;max-width:220px}
|
|
33
|
+
@media (max-width:640px){
|
|
34
|
+
.homecard{grid-template-columns:1fr}
|
|
35
|
+
.homeacts{justify-content:flex-start;flex-wrap:wrap}
|
|
36
|
+
.homeacts .meta{text-align:left}
|
|
37
|
+
}
|
|
28
38
|
|
|
29
|
-
.homeworking{display:flex;flex-direction:column;
|
|
30
|
-
.homerun{display:
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
.homeworking{display:flex;flex-direction:column;padding:4px 16px}
|
|
40
|
+
.homerun{display:grid;grid-template-columns:minmax(150px,220px) auto 1fr auto;align-items:center;
|
|
41
|
+
gap:12px;padding:12px 0;font-size:13.5px}
|
|
42
|
+
.homerun + .homerun{border-top:1px solid var(--line)}
|
|
43
|
+
.homewho2{display:inline-flex;align-items:center;gap:8px;min-width:0}
|
|
44
|
+
.homewho2 b{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
45
|
+
.homewho2 .ic{--ic:15px;color:var(--ink-dim);flex:none}
|
|
46
|
+
.homestate{justify-self:start}
|
|
47
|
+
.homestate.ok::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;
|
|
48
|
+
margin-right:6px;animation:homepulse 1.6s var(--ease) infinite}
|
|
49
|
+
@keyframes homepulse{50%{opacity:.3}}
|
|
50
|
+
@media (prefers-reduced-motion:reduce){.homestate.ok::before{animation:none}}
|
|
51
|
+
.homerunwhat{color:var(--ink);line-height:1.45;min-width:0}
|
|
52
|
+
.homerunsub{color:var(--ink-dim);font-size:12.5px;margin-top:2px}
|
|
53
|
+
.homerunlog .ghbtn{min-height:28px;padding:3px 12px}
|
|
54
|
+
@media (max-width:640px){
|
|
55
|
+
.homerun{grid-template-columns:1fr auto;grid-template-areas:"who state" "what what" "log log"}
|
|
56
|
+
.homewho2{grid-area:who}.homestate{grid-area:state;justify-self:end}
|
|
57
|
+
.homerunwhat{grid-area:what}.homerunlog{grid-area:log}
|
|
58
|
+
}
|
|
39
59
|
|
|
40
60
|
.homerow{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;padding:8px 2px;
|
|
41
61
|
border-bottom:1px solid var(--line);font-size:13px}
|
|
42
62
|
.homerow .homeopen{font-weight:500;font-size:13px}
|
|
63
|
+
.homeclick{cursor:pointer;border-radius:8px;padding-left:8px;padding-right:8px;margin:0 -8px}
|
|
64
|
+
.homeclick:hover{background:var(--hover)}
|
|
65
|
+
.homeclick:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
|
43
66
|
.homeclosed{display:grid;grid-template-columns:1fr auto;align-items:center;gap:4px 14px}
|
|
44
67
|
.homeclosed .meta{margin-top:3px;line-height:1.5;font-family:var(--sans);font-size:12px}
|
|
45
68
|
.homerest{margin-top:26px}
|
|
@@ -52,6 +75,5 @@
|
|
|
52
75
|
@media (max-width:640px){.homereport{grid-template-columns:1fr;gap:4px}}
|
|
53
76
|
|
|
54
77
|
@media (max-width:640px){
|
|
55
|
-
.homewho2{min-width:0}
|
|
56
78
|
.homehead{flex-wrap:wrap}
|
|
57
79
|
}
|
|
@@ -167,10 +167,11 @@ export function askYes({ title, hint, confirm = "Continue" }) {
|
|
|
167
167
|
* @returns {{set: (node: Node) => void, close: () => void, isOpen: () => boolean} | null}
|
|
168
168
|
* null where there is no dialog element (the test shim), and the caller draws inline instead.
|
|
169
169
|
*/
|
|
170
|
-
export function sheet({ node, onClose }) {
|
|
170
|
+
export function sheet({ node, onClose, side = false }) {
|
|
171
171
|
const box = document.createElement("dialog");
|
|
172
172
|
if (typeof box.showModal !== "function") return null;
|
|
173
|
-
|
|
173
|
+
// `side` slides in from the right at full height, for reading a thread beside the page.
|
|
174
|
+
box.className = side ? "sheet side" : "sheet";
|
|
174
175
|
const body = el("div", { className: "sheetbody" });
|
|
175
176
|
body.append(node);
|
|
176
177
|
box.append(body);
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/* Home: what needs you, who is working, what you asked for, and what was closed today.
|
|
2
2
|
*
|
|
3
3
|
* Built from the inbox list and /api/live, sorted by homesort.js, which is where the rule that
|
|
4
|
-
* every open item has exactly one place lives. Clicking an item opens its thread
|
|
5
|
-
*
|
|
4
|
+
* every open item has exactly one place lives. Clicking an item opens its thread in a side
|
|
5
|
+
* sheet, the same thread view Trackers has, with the reply box in it. */
|
|
6
6
|
|
|
7
7
|
import { getLive, post } from "../api.js";
|
|
8
|
+
import { sheet } from "../dialog.js";
|
|
8
9
|
import { ago, el, skeleton } from "../dom.js";
|
|
9
10
|
import { daysUntil, sortHome } from "../homesort.js";
|
|
10
|
-
import { icon, staffIcon } from "../icons.js";
|
|
11
|
+
import { icon, iconHTML, staffIcon } from "../icons.js";
|
|
11
12
|
import { ensureInbox, stampCounts } from "../refresh.js";
|
|
12
13
|
import { go, render } from "../router.js";
|
|
14
|
+
import { threadInto } from "./inbox.js";
|
|
13
15
|
import { humansOf, S } from "../state.js";
|
|
14
16
|
|
|
15
17
|
const KIND_LABEL = {
|
|
@@ -143,26 +145,37 @@ function titleOf(said) {
|
|
|
143
145
|
|
|
144
146
|
function needCard(e) {
|
|
145
147
|
const { item, kind } = e;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
/* The whole card opens the thread; the one quick action on it does not. Answering in words
|
|
149
|
+
happens in the thread, where what was asked is in front of you. */
|
|
150
|
+
const card = el("div", { className: "card homecard", tabIndex: 0, role: "button" });
|
|
151
|
+
card.onclick = (ev) => {
|
|
152
|
+
if (!ev.target.closest("button, a, textarea")) openThread(item);
|
|
153
|
+
};
|
|
154
|
+
card.onkeydown = (ev) => {
|
|
155
|
+
if (ev.target === card && (ev.key === "Enter" || ev.key === " ")) {
|
|
156
|
+
ev.preventDefault();
|
|
157
|
+
openThread(item);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
// What it is on the left, what you can do about it on the right.
|
|
161
|
+
const about = el("div", { className: "homeabout" }, [
|
|
148
162
|
el("div", { className: "homehead" }, [
|
|
149
163
|
el("span", { className: "chip " + (KIND_TONE[kind] ?? ""), textContent: KIND_LABEL[kind] }),
|
|
150
164
|
openLink(item),
|
|
151
165
|
]),
|
|
152
166
|
el("div", { className: "meta", textContent: whereOf(e) + " · " + ago(item.createdAt) }),
|
|
153
|
-
);
|
|
167
|
+
]);
|
|
168
|
+
card.append(about);
|
|
154
169
|
|
|
155
|
-
if (kind === "decision" && item.due)
|
|
156
|
-
if (kind === "merge")
|
|
170
|
+
if (kind === "decision" && item.due) about.append(dueLine(item.due));
|
|
171
|
+
if (kind === "merge") about.append(checksLine(item));
|
|
157
172
|
|
|
173
|
+
// The note comes first so that what happened sits just left of the button that did it.
|
|
158
174
|
const note = el("span", { className: "meta" });
|
|
159
|
-
const actions = el("div", { className: "row homeacts" });
|
|
175
|
+
const actions = el("div", { className: "row homeacts" }, [note]);
|
|
160
176
|
|
|
161
177
|
if (kind === "decision" || kind === "ask") {
|
|
162
|
-
|
|
163
|
-
const send = button("Send", true, () => say(e, box.value, note, actions));
|
|
164
|
-
card.append(box);
|
|
165
|
-
actions.append(send);
|
|
178
|
+
actions.append(button("Reply", true, () => openThread(item)));
|
|
166
179
|
if (item.due) actions.append(button("Go with the default", false, () => say(e, "Go with your default.", note, actions)));
|
|
167
180
|
} else if (kind === "review") {
|
|
168
181
|
actions.append(button("Approve", true, () => say(e, "Approved.", note, actions)));
|
|
@@ -174,7 +187,6 @@ function needCard(e) {
|
|
|
174
187
|
merge.disabled = blocked || item.checks === "pending";
|
|
175
188
|
actions.append(merge);
|
|
176
189
|
}
|
|
177
|
-
actions.append(button("Open", false, () => openThread(item)), note);
|
|
178
190
|
card.append(actions);
|
|
179
191
|
return card;
|
|
180
192
|
}
|
|
@@ -248,45 +260,69 @@ function whatRun(r) {
|
|
|
248
260
|
function working(staff) {
|
|
249
261
|
const box = el("div", { className: "card homeworking" });
|
|
250
262
|
if (!S.live) {
|
|
251
|
-
box.append(el("p", { className: "
|
|
263
|
+
box.append(el("p", { className: "homerunwhat", textContent: "Asking GitHub…" }));
|
|
252
264
|
return box;
|
|
253
265
|
}
|
|
254
266
|
for (const s of staff) {
|
|
255
267
|
const l = (S.live.staff ?? []).find((x) => x.handle === s.handle);
|
|
256
|
-
const
|
|
257
|
-
|
|
268
|
+
const runs = l && !l.error ? (l.running ?? []) : [];
|
|
269
|
+
const busy = runs.length > 0;
|
|
270
|
+
const last = l?.finished?.[0];
|
|
271
|
+
|
|
272
|
+
const what = el("div", { className: "homerunwhat" });
|
|
273
|
+
let log = null;
|
|
258
274
|
if (!l || l.error) {
|
|
259
|
-
|
|
260
|
-
} else if (
|
|
261
|
-
for (const r of
|
|
275
|
+
what.append(el("span", { className: "err", textContent: l?.error ?? "Could not read their runs." }));
|
|
276
|
+
} else if (busy) {
|
|
277
|
+
for (const r of runs) {
|
|
278
|
+
what.append(el("div", { textContent: `${cap(whatRun(r))} · ${minutes(r.createdAt)} so far` }));
|
|
279
|
+
}
|
|
280
|
+
log = runs[0].url;
|
|
281
|
+
} else if (last) {
|
|
282
|
+
what.append(
|
|
283
|
+
el("div", { textContent: `${cap(whatRun(last))}, ${outcome(last)} ${ago(last.updatedAt)}` }),
|
|
284
|
+
);
|
|
285
|
+
log = last.url;
|
|
262
286
|
} else {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
287
|
+
what.append(el("div", { textContent: "No runs in the last few hours." }));
|
|
288
|
+
}
|
|
289
|
+
if (l && !l.error && l.automatic) {
|
|
290
|
+
what.append(
|
|
291
|
+
el("div", {
|
|
292
|
+
className: "homerunsub",
|
|
293
|
+
textContent: `${l.automatic} of ${l.limit} runs today that nobody asked for`,
|
|
268
294
|
}),
|
|
269
295
|
);
|
|
270
|
-
if (last) row.append(el("a", { className: "meta", href: last.url, target: "_blank", rel: "noopener", textContent: "log" }));
|
|
271
296
|
}
|
|
272
|
-
|
|
273
|
-
|
|
297
|
+
|
|
298
|
+
const state = el("span", {
|
|
299
|
+
className: "chip homestate " + (busy ? "ok" : ""),
|
|
300
|
+
textContent: busy ? (runs[0].status === "queued" ? "Queued" : "Working") : "Idle",
|
|
301
|
+
});
|
|
302
|
+
const actions = el("div", { className: "homerunlog" });
|
|
303
|
+
if (log) {
|
|
304
|
+
actions.append(
|
|
305
|
+
el("a", { className: "ghbtn", href: log, target: "_blank", rel: "noopener", textContent: "Log" }),
|
|
306
|
+
);
|
|
274
307
|
}
|
|
275
|
-
|
|
308
|
+
|
|
309
|
+
box.append(
|
|
310
|
+
el("div", { className: "homerun" + (busy ? " busy" : "") }, [
|
|
311
|
+
el("span", { className: "homewho2" }, [icon(staffIcon(s), "ic"), el("b", { textContent: s.name })]),
|
|
312
|
+
state,
|
|
313
|
+
what,
|
|
314
|
+
actions,
|
|
315
|
+
]),
|
|
316
|
+
);
|
|
276
317
|
}
|
|
277
318
|
return box;
|
|
278
319
|
}
|
|
279
320
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
href: r.url,
|
|
286
|
-
target: "_blank",
|
|
287
|
-
rel: "noopener",
|
|
288
|
-
textContent: (live && r.status === "queued" ? "Queued: " : "Working: ") + what + ` · ${mins} min · log`,
|
|
289
|
-
});
|
|
321
|
+
const cap = (t) => t.charAt(0).toUpperCase() + t.slice(1);
|
|
322
|
+
|
|
323
|
+
function minutes(since) {
|
|
324
|
+
const m = Math.max(1, Math.round((Date.now() - new Date(since).getTime()) / 60000));
|
|
325
|
+
return m < 60 ? `${m} min` : `${Math.floor(m / 60)} h ${m % 60} min`;
|
|
290
326
|
}
|
|
291
327
|
|
|
292
328
|
function outcome(r) {
|
|
@@ -297,8 +333,10 @@ function poll() {
|
|
|
297
333
|
clearTimeout(timer);
|
|
298
334
|
getLive()
|
|
299
335
|
.then((data) => {
|
|
336
|
+
// Only a change repaints: a poll every few seconds must not move the page under you.
|
|
337
|
+
const changed = JSON.stringify(data.staff) !== JSON.stringify(S.live?.staff);
|
|
300
338
|
S.live = data;
|
|
301
|
-
if (S.view === "home")
|
|
339
|
+
if (changed && S.view === "home" && !document.querySelector("dialog[open]")) repaintInPlace();
|
|
302
340
|
})
|
|
303
341
|
.catch(() => {})
|
|
304
342
|
.finally(() => {
|
|
@@ -313,17 +351,32 @@ function poll() {
|
|
|
313
351
|
const STATUS = { waiting: "Waiting", working: "Being worked on", answered: "Answered" };
|
|
314
352
|
|
|
315
353
|
function requestRow(e) {
|
|
316
|
-
return
|
|
354
|
+
return clickable(e.item, "homerow", [
|
|
317
355
|
el("span", { className: "chip " + (e.status === "answered" ? "cool" : e.status === "working" ? "warm" : ""), textContent: STATUS[e.status] }),
|
|
318
356
|
openLink(e.item),
|
|
319
357
|
el("span", { className: "meta", textContent: whereOf(e) + " · " + ago(e.item.updatedAt) }),
|
|
320
358
|
]);
|
|
321
359
|
}
|
|
322
360
|
|
|
361
|
+
/** A row that opens its thread from anywhere on it, except a button inside it. */
|
|
362
|
+
function clickable(item, className, kids) {
|
|
363
|
+
const row = el("div", { className: className + " homeclick", tabIndex: 0, role: "button" }, kids);
|
|
364
|
+
row.onclick = (ev) => {
|
|
365
|
+
if (!ev.target.closest("button, a")) openThread(item);
|
|
366
|
+
};
|
|
367
|
+
row.onkeydown = (ev) => {
|
|
368
|
+
if (ev.target === row && (ev.key === "Enter" || ev.key === " ")) {
|
|
369
|
+
ev.preventDefault();
|
|
370
|
+
openThread(item);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
return row;
|
|
374
|
+
}
|
|
375
|
+
|
|
323
376
|
function closedRow(e) {
|
|
324
377
|
const note = el("span", { className: "meta" });
|
|
325
378
|
const reopen = button("Reopen", false, () => act(note, row, { action: "reopen", repo: e.item.repo, number: e.item.number }, "Reopened."));
|
|
326
|
-
const row =
|
|
379
|
+
const row = clickable(e.item, "homerow homeclosed", [
|
|
327
380
|
el("div", {}, [
|
|
328
381
|
openLink(e.item),
|
|
329
382
|
el("div", { className: "meta", textContent: whereOf(e) + " · " + (e.reason || "No reason given.") }),
|
|
@@ -362,13 +415,33 @@ function openLink(item) {
|
|
|
362
415
|
return b;
|
|
363
416
|
}
|
|
364
417
|
|
|
418
|
+
/** Repaint Home where you were: the sheet may have changed what is on it, not where you are. */
|
|
419
|
+
function repaintInPlace() {
|
|
420
|
+
const main = document.querySelector("#main");
|
|
421
|
+
const at = { win: window.scrollY, main: main?.scrollTop ?? 0 };
|
|
422
|
+
render();
|
|
423
|
+
window.scrollTo(0, at.win);
|
|
424
|
+
if (main) main.scrollTop = at.main;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The thread in a side sheet: its header, actions, reply box and conversation. Closing it
|
|
429
|
+
* repaints Home, since a reply or a close there changes what belongs where. Where there is no
|
|
430
|
+
* <dialog> (the test shim), it falls back to the thread on Trackers.
|
|
431
|
+
*/
|
|
365
432
|
function openThread(item) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
433
|
+
const ref = { repo: item.repo, number: item.number, kind: item.kind };
|
|
434
|
+
const body = el("div");
|
|
435
|
+
const x = el("button", { className: "iconbtn", title: "Close (Esc)", ariaLabel: "Close" });
|
|
436
|
+
x.innerHTML = iconHTML("close");
|
|
437
|
+
body.append(el("div", { className: "sidebar-x" }, [x]));
|
|
438
|
+
threadInto(body, ref);
|
|
439
|
+
const box = sheet({ node: body, side: true, onClose: () => S.view === "home" && repaintInPlace() });
|
|
440
|
+
if (!box) {
|
|
441
|
+
go({ view: item.kind === "pr" ? "prs" : "inbox", inboxStaff: "", inboxFilter: "", inboxOpen: ref });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
x.onclick = () => box.close();
|
|
372
445
|
}
|
|
373
446
|
|
|
374
447
|
function whereOf(e) {
|
|
@@ -385,7 +458,7 @@ function button(label, primary, onclick) {
|
|
|
385
458
|
/** After a write: re-read the inbox, and look for the run it starts. */
|
|
386
459
|
function refreshSoon() {
|
|
387
460
|
ensureInbox(true)
|
|
388
|
-
.then(() => S.view === "home" &&
|
|
461
|
+
.then(() => S.view === "home" && !document.querySelector("dialog[open]") && repaintInPlace())
|
|
389
462
|
.catch(() => {});
|
|
390
463
|
setTimeout(poll, 3_000);
|
|
391
464
|
}
|
|
@@ -108,6 +108,18 @@ export const viewInbox = (m) => inboxScreen(m, {});
|
|
|
108
108
|
*/
|
|
109
109
|
export const viewPrs = (m) => inboxScreen(m, { prs: true });
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* One thread, drawn into `host` on its own: the header, its actions, the reply box and the
|
|
113
|
+
* conversation, with no list beside it. Home opens a card this way, in a side sheet.
|
|
114
|
+
*
|
|
115
|
+
* It is the same screen as Trackers, built off-page, with only its viewer kept. Replying,
|
|
116
|
+
* closing and merging repaint a list nobody can see and then reopen this thread, which is
|
|
117
|
+
* what they do on Trackers too.
|
|
118
|
+
*/
|
|
119
|
+
export function threadInto(host, ref) {
|
|
120
|
+
host.append(inboxScreen(el("div"), { prs: ref.kind === "pr", only: ref }));
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
function inboxScreen(m, opts) {
|
|
112
124
|
/* "Pending work", not "Pull requests". Every one of these is a piece of work a staff member
|
|
113
125
|
has finished and cannot land on their own, which is the thing you are being asked about.
|
|
@@ -166,10 +178,15 @@ function inboxScreen(m, opts) {
|
|
|
166
178
|
refresh.onclick = () => { refresh.classList.add("spin"); load(true); };
|
|
167
179
|
|
|
168
180
|
if (S.inbox) { stampCounts(); paint(); restore(); } else load(false);
|
|
181
|
+
if (opts.only) return viewer;
|
|
169
182
|
|
|
170
183
|
/** A `?t=` in the URL names a thread. It used to be read into the state and then never
|
|
171
184
|
acted on, because only the already-loaded branch opened one. */
|
|
172
185
|
function restore() {
|
|
186
|
+
if (opts.only) {
|
|
187
|
+
openThread(opts.only);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
173
190
|
if (!S.inboxOpen) return;
|
|
174
191
|
// The two screens share one open thread, and each holds half the org. Whatever was open on
|
|
175
192
|
// the other one is not in this list, so it is dropped rather than opened beside it.
|