@nanocollective/roster 0.1.0-alpha.46 → 0.1.0-alpha.48
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
|
@@ -95,7 +95,8 @@ Where the portal opens. An Ask box at the top, then these, in order.
|
|
|
95
95
|
- a pull request has **Merge**, unless its checks fail, are still running, or it conflicts.
|
|
96
96
|
|
|
97
97
|
Answers go out as you, with the staff member's `@handle`, so they act on them straight away.
|
|
98
|
-
|
|
98
|
+
Once you have had the last word, the card leaves Needs you for Your requests, since it is
|
|
99
|
+
waiting on them now. If they reply, it comes back. When nothing is waiting, it says so.
|
|
99
100
|
- **Unread.** Anything on a staff member's own tracker, or elsewhere, with activity you have not
|
|
100
101
|
seen: a peer's ask, a status issue, their own work. Unread comes from your GitHub
|
|
101
102
|
notifications, as on Trackers. Cards and rows anywhere on Home carry the same blue mark, and
|
|
@@ -105,7 +106,7 @@ Where the portal opens. An Ask box at the top, then these, in order.
|
|
|
105
106
|
they are idle, how their last run ended. Peer and follow-on runs today are counted against
|
|
106
107
|
`max_runs_per_day`. This asks GitHub every few seconds while a run is going and every half
|
|
107
108
|
minute otherwise, and only while Home is on screen.
|
|
108
|
-
- **Your requests.** What you asked for: waiting, being worked on (a run for it is going), or
|
|
109
|
+
- **Your requests.** What you asked for, and asks of theirs you have answered: waiting, being worked on (a run for it is going), or
|
|
109
110
|
answered (a staff member had the last word).
|
|
110
111
|
- **Closed today.** What the staff closed today, with the line they closed it with, and
|
|
111
112
|
**Reopen** beside each. Staff close finished issues themselves, so this is where you check.
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/* Home: what needs you, who is working, your requests, closed today. */
|
|
2
2
|
|
|
3
3
|
.home{max-width:820px}
|
|
4
|
+
/* Secondary text on Home is read, not scanned for numbers: the readable face and colour, not
|
|
5
|
+
the faint monospace the rest of the portal uses for metadata. */
|
|
6
|
+
.home .meta{font:12.5px/1.5 var(--sans);color:var(--ink-dim)}
|
|
4
7
|
.home h2{font:600 13px var(--sans);color:var(--ink-dim);text-transform:none;letter-spacing:0;
|
|
5
8
|
margin:26px 0 10px;display:flex;align-items:center;gap:8px}
|
|
6
9
|
.home h2 .pill{font-size:10.5px}
|
|
@@ -37,7 +40,7 @@
|
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
.homeworking{display:flex;flex-direction:column;padding:4px 16px}
|
|
40
|
-
.homerun{display:grid;grid-template-columns:minmax(150px,220px)
|
|
43
|
+
.homerun{display:grid;grid-template-columns:minmax(150px,220px) 84px 1fr auto;align-items:center;
|
|
41
44
|
gap:12px;padding:12px 0;font-size:13.5px}
|
|
42
45
|
.homerun + .homerun{border-top:1px solid var(--line)}
|
|
43
46
|
.homewho2{display:inline-flex;align-items:center;gap:8px;min-width:0}
|
|
@@ -57,22 +60,16 @@
|
|
|
57
60
|
.homerunwhat{grid-area:what}.homerunlog{grid-area:log}
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
.homerow{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;padding:8px 2px;
|
|
61
|
-
border-bottom:1px solid var(--line);font-size:13px}
|
|
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}
|
|
66
|
-
.homeclosed{display:grid;grid-template-columns:1fr auto;align-items:center;gap:4px 14px}
|
|
67
|
-
.homeclosed .meta{margin-top:3px;line-height:1.5;font-family:var(--sans);font-size:12px}
|
|
68
63
|
.homerest{margin-top:26px}
|
|
64
|
+
.homecard.gone,.homerun.gone{opacity:0;transform:translateX(12px);
|
|
65
|
+
transition:opacity .2s var(--ease),transform .2s var(--ease)}
|
|
69
66
|
|
|
70
67
|
/* Activity you have not seen, as Trackers marks it: a blue edge and a bold title. Opening the
|
|
71
68
|
thread marks it read, here and on GitHub. */
|
|
72
69
|
.homecard.unread{box-shadow:inset 3px 0 0 var(--blue)}
|
|
73
|
-
.
|
|
74
|
-
.unread .homeopen{font-weight:700}
|
|
75
|
-
.unread .homeopen::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
|
|
70
|
+
.homerun.unread{box-shadow:inset 3px 0 0 var(--blue);margin:0 -16px;padding-left:13px;padding-right:16px}
|
|
71
|
+
.unread .homeopen,.unread .homerowtitle{font-weight:700}
|
|
72
|
+
.unread .homeopen::before,.unread .homerowtitle::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
|
|
76
73
|
background:var(--blue);margin:0 7px 1px 0;vertical-align:middle}
|
|
77
74
|
|
|
78
75
|
.homereports{display:flex;flex-direction:column;gap:14px;padding:14px 16px}
|
|
@@ -85,3 +82,9 @@
|
|
|
85
82
|
@media (max-width:640px){
|
|
86
83
|
.homehead{flex-wrap:wrap}
|
|
87
84
|
}
|
|
85
|
+
|
|
86
|
+
/* Rows in the Working now card that open a thread. */
|
|
87
|
+
.homerun.homeclick{cursor:pointer}
|
|
88
|
+
.homerun.homeclick:hover .homerowtitle{color:var(--accent)}
|
|
89
|
+
.homerun.homeclick:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:6px}
|
|
90
|
+
.homerowtitle{font-weight:600;color:var(--ink)}
|
|
@@ -121,17 +121,22 @@ export function placeOf(item, owner, people, markers) {
|
|
|
121
121
|
(item.assignees ?? []).some((a) => people.has(lower(a))) ||
|
|
122
122
|
labels.some((l) => markers.has(l));
|
|
123
123
|
const byPerson = people.has(lower(item.author));
|
|
124
|
+
/* You had the last word on an ask: approved it, answered it, said go with the default. It is
|
|
125
|
+
waiting on them now, so it is one of your requests until they reply or close it. If they
|
|
126
|
+
reply, the last word is theirs and it is back on you. */
|
|
127
|
+
const youAnswered = Boolean(item.lastComment) && people.has(lower(item.lastComment.author));
|
|
124
128
|
|
|
125
129
|
if (!owner) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
if (!onPerson || byPerson) {
|
|
131
|
+
return { place: "elsewhere", why: item.role === "product" ? "contributor" : "other" };
|
|
132
|
+
}
|
|
133
|
+
return youAnswered ? { place: "requests" } : { place: "needs", kind: kindOf(labels) };
|
|
129
134
|
}
|
|
130
135
|
if (item.number === owner.statusIssue || labels.includes("keep-open")) {
|
|
131
136
|
return { place: "staff", why: "status" };
|
|
132
137
|
}
|
|
133
138
|
if (byPerson) return { place: "requests" };
|
|
134
|
-
if (onPerson) return { place: "needs", kind: kindOf(labels) };
|
|
139
|
+
if (onPerson) return youAnswered ? { place: "requests" } : { place: "needs", kind: kindOf(labels) };
|
|
135
140
|
return {
|
|
136
141
|
place: "staff",
|
|
137
142
|
why: labels.some((l) => l.startsWith("from-")) ? "peer" : "own",
|
|
@@ -75,7 +75,7 @@ export function viewHome(m) {
|
|
|
75
75
|
|
|
76
76
|
if (h.unread.length) {
|
|
77
77
|
page.append(section("Unread", h.unread.length));
|
|
78
|
-
|
|
78
|
+
page.append(listBox(h.unread.map(unreadRow)));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
page.append(section("Working now"));
|
|
@@ -83,12 +83,12 @@ export function viewHome(m) {
|
|
|
83
83
|
|
|
84
84
|
if (h.requests.length) {
|
|
85
85
|
page.append(section("Your requests", h.requests.length));
|
|
86
|
-
|
|
86
|
+
page.append(listBox(h.requests.map(requestRow)));
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
if (h.closedToday.length) {
|
|
90
90
|
page.append(section("Closed today", h.closedToday.length));
|
|
91
|
-
|
|
91
|
+
page.append(listBox(h.closedToday.map(closedRow)));
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
page.append(rest(h));
|
|
@@ -246,6 +246,12 @@ async function act(note, actions, payload, ok) {
|
|
|
246
246
|
try {
|
|
247
247
|
await post(payload);
|
|
248
248
|
note.textContent = ok;
|
|
249
|
+
// Done is done: the card goes now, rather than when GitHub next answers.
|
|
250
|
+
const card = actions.closest?.(".homecard, .homerun");
|
|
251
|
+
if (card) {
|
|
252
|
+
card.classList.add("gone");
|
|
253
|
+
setTimeout(() => card.remove(), 220);
|
|
254
|
+
}
|
|
249
255
|
refreshSoon();
|
|
250
256
|
} catch (err) {
|
|
251
257
|
note.textContent = String(err.message || err);
|
|
@@ -338,6 +344,16 @@ function outcome(r) {
|
|
|
338
344
|
return r.conclusion === "success" ? "finished" : r.conclusion === "failure" ? "failed" : (r.conclusion ?? "ended");
|
|
339
345
|
}
|
|
340
346
|
|
|
347
|
+
/**
|
|
348
|
+
* A timer that never keeps a process alive. In the browser this is setTimeout; under the
|
|
349
|
+
* tests' Node shim the polling loop would otherwise hold a test file open forever.
|
|
350
|
+
*/
|
|
351
|
+
function later(fn, ms) {
|
|
352
|
+
const t = setTimeout(fn, ms);
|
|
353
|
+
t?.unref?.();
|
|
354
|
+
return t;
|
|
355
|
+
}
|
|
356
|
+
|
|
341
357
|
function poll() {
|
|
342
358
|
clearTimeout(timer);
|
|
343
359
|
getLive()
|
|
@@ -351,13 +367,50 @@ function poll() {
|
|
|
351
367
|
.finally(() => {
|
|
352
368
|
if (S.view !== "home") return;
|
|
353
369
|
const busy = (S.live?.staff ?? []).some((l) => l.running?.length);
|
|
354
|
-
timer =
|
|
370
|
+
timer = later(poll, busy ? FAST : SLOW);
|
|
355
371
|
});
|
|
356
372
|
}
|
|
357
373
|
|
|
358
374
|
/* ------------------------------ your requests ---------------------------- */
|
|
359
375
|
|
|
360
|
-
|
|
376
|
+
/** A card of rows, laid out like Working now. */
|
|
377
|
+
function listBox(rows) {
|
|
378
|
+
return el("div", { className: "card homeworking" }, rows);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* One row in the shape Working now uses: who, a status chip, the title with one line under it,
|
|
383
|
+
* and a button on the right. The whole row opens the thread, except the button.
|
|
384
|
+
*/
|
|
385
|
+
function listRow(e, { chip, tone = "", sub, action = null }) {
|
|
386
|
+
const s = e.staff;
|
|
387
|
+
const row = el(
|
|
388
|
+
"div",
|
|
389
|
+
{ className: "homerun homeclick" + (e.item.unread ? " unread" : ""), tabIndex: 0, role: "button" },
|
|
390
|
+
[
|
|
391
|
+
el("span", { className: "homewho2" }, [
|
|
392
|
+
icon(s ? staffIcon(s) : "issue-open", "ic"),
|
|
393
|
+
el("b", { textContent: s?.name ?? (e.item.repo.split("/")[1] ?? e.item.repo) }),
|
|
394
|
+
]),
|
|
395
|
+
el("span", { className: "chip homestate " + tone, textContent: chip }),
|
|
396
|
+
el("div", { className: "homerunwhat" }, [
|
|
397
|
+
el("div", { className: "homerowtitle", textContent: e.item.title }),
|
|
398
|
+
el("div", { className: "homerunsub", textContent: sub }),
|
|
399
|
+
]),
|
|
400
|
+
el("div", { className: "homerunlog" }, action ? [action] : []),
|
|
401
|
+
],
|
|
402
|
+
);
|
|
403
|
+
row.onclick = (ev) => {
|
|
404
|
+
if (!ev.target.closest("button, a")) openThread(e.item);
|
|
405
|
+
};
|
|
406
|
+
row.onkeydown = (ev) => {
|
|
407
|
+
if (ev.target === row && (ev.key === "Enter" || ev.key === " ")) {
|
|
408
|
+
ev.preventDefault();
|
|
409
|
+
openThread(e.item);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
return row;
|
|
413
|
+
}
|
|
361
414
|
|
|
362
415
|
const WHY = {
|
|
363
416
|
status: "Status",
|
|
@@ -370,51 +423,34 @@ const WHY = {
|
|
|
370
423
|
|
|
371
424
|
/** Something on a staff member's tracker, or elsewhere, with activity you have not seen. */
|
|
372
425
|
function unreadRow(e) {
|
|
373
|
-
return
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
426
|
+
return listRow(e, {
|
|
427
|
+
chip: WHY[e.why] ?? "Issue",
|
|
428
|
+
tone: "cool",
|
|
429
|
+
sub: `#${e.item.number} · ${ago(e.item.updatedAt)}`,
|
|
430
|
+
});
|
|
378
431
|
}
|
|
379
432
|
|
|
380
|
-
|
|
381
|
-
return clickable(e.item, "homerow", [
|
|
382
|
-
el("span", { className: "chip " + (e.status === "answered" ? "cool" : e.status === "working" ? "warm" : ""), textContent: STATUS[e.status] }),
|
|
383
|
-
openLink(e.item),
|
|
384
|
-
el("span", { className: "meta", textContent: whereOf(e) + " · " + ago(e.item.updatedAt) }),
|
|
385
|
-
]);
|
|
386
|
-
}
|
|
433
|
+
const STATUS = { waiting: "Waiting", working: "Working", answered: "Answered" };
|
|
387
434
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
"
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
);
|
|
395
|
-
row.onclick = (ev) => {
|
|
396
|
-
if (!ev.target.closest("button, a")) openThread(item);
|
|
397
|
-
};
|
|
398
|
-
row.onkeydown = (ev) => {
|
|
399
|
-
if (ev.target === row && (ev.key === "Enter" || ev.key === " ")) {
|
|
400
|
-
ev.preventDefault();
|
|
401
|
-
openThread(item);
|
|
402
|
-
}
|
|
403
|
-
};
|
|
404
|
-
return row;
|
|
435
|
+
function requestRow(e) {
|
|
436
|
+
return listRow(e, {
|
|
437
|
+
chip: STATUS[e.status],
|
|
438
|
+
tone: e.status === "answered" ? "cool" : e.status === "working" ? "ok" : "",
|
|
439
|
+
sub: `#${e.item.number} · ${ago(e.item.updatedAt)}`,
|
|
440
|
+
});
|
|
405
441
|
}
|
|
406
442
|
|
|
407
443
|
function closedRow(e) {
|
|
408
444
|
const note = el("span", { className: "meta" });
|
|
409
|
-
const reopen = button("Reopen", false, () =>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
445
|
+
const reopen = button("Reopen", false, () =>
|
|
446
|
+
act(note, holder, { action: "reopen", repo: e.item.repo, number: e.item.number }, "Reopened."),
|
|
447
|
+
);
|
|
448
|
+
const holder = el("div", { className: "row" }, [note, reopen]);
|
|
449
|
+
return listRow(e, {
|
|
450
|
+
chip: "Closed",
|
|
451
|
+
sub: `#${e.item.number} · ${e.reason || "No reason given."}`,
|
|
452
|
+
action: holder,
|
|
453
|
+
});
|
|
418
454
|
}
|
|
419
455
|
|
|
420
456
|
/* --------------------------------- rest --------------------------------- */
|
|
@@ -491,5 +527,5 @@ function refreshSoon() {
|
|
|
491
527
|
ensureInbox(true)
|
|
492
528
|
.then(() => S.view === "home" && !document.querySelector("dialog[open]") && repaintInPlace())
|
|
493
529
|
.catch(() => {});
|
|
494
|
-
|
|
530
|
+
later(poll, 3_000);
|
|
495
531
|
}
|