@nanocollective/roster 0.1.0-alpha.4 → 0.1.0-alpha.6
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/dist/cli.js +8 -5
- package/docs/images/brain.jpg +0 -0
- package/docs/images/org.jpg +0 -0
- package/docs/images/prompt.jpg +0 -0
- package/docs/images/setup-org.jpg +0 -0
- package/docs/images/setup-plan.jpg +0 -0
- package/docs/images/staff.jpg +0 -0
- package/package.json +1 -1
- package/templates/portal/css/base.css +133 -73
- package/templates/portal/css/brain.css +23 -20
- package/templates/portal/css/diff.css +10 -9
- package/templates/portal/css/graph.css +12 -7
- package/templates/portal/css/health.css +12 -10
- package/templates/portal/css/inbox.css +36 -21
- package/templates/portal/css/layout.css +70 -41
- package/templates/portal/css/markdown.css +17 -14
- package/templates/portal/css/setup.css +38 -32
- package/templates/portal/index.html +6 -2
- package/templates/portal/js/app.js +40 -0
- package/templates/portal/js/state.js +6 -3
- package/templates/portal/js/views/graph.js +1 -1
- package/templates/portal/js/views/inbox.js +70 -20
|
@@ -113,7 +113,7 @@ function inboxScreen(m, opts) {
|
|
|
113
113
|
className: "sub",
|
|
114
114
|
textContent: opts.prs
|
|
115
115
|
? "Work the staff have finished and cannot land themselves."
|
|
116
|
-
: "Everything open across the org.",
|
|
116
|
+
: "Everything open across the org. Pull requests are in Pending work.",
|
|
117
117
|
});
|
|
118
118
|
m.append(sub);
|
|
119
119
|
|
|
@@ -126,13 +126,8 @@ function inboxScreen(m, opts) {
|
|
|
126
126
|
scope.append(
|
|
127
127
|
el("option", { value: "", textContent: "Everything" }),
|
|
128
128
|
el("option", { value: "mine", textContent: "On " + humanLabel() }),
|
|
129
|
-
//
|
|
130
|
-
...(opts.prs
|
|
131
|
-
? []
|
|
132
|
-
: [
|
|
133
|
-
el("option", { value: "decision", textContent: "Decisions" }),
|
|
134
|
-
el("option", { value: "pr", textContent: "Open work (PRs)" }),
|
|
135
|
-
]),
|
|
129
|
+
// A decision is not a PR, so this one is only ever worth offering on the inbox.
|
|
130
|
+
...(opts.prs ? [] : [el("option", { value: "decision", textContent: "Decisions" })]),
|
|
136
131
|
);
|
|
137
132
|
scope.value = opts.prs && S.inboxFilter !== "mine" ? "" : S.inboxFilter;
|
|
138
133
|
|
|
@@ -187,9 +182,9 @@ function inboxScreen(m, opts) {
|
|
|
187
182
|
acted on, because only the already-loaded branch opened one. */
|
|
188
183
|
function restore() {
|
|
189
184
|
if (!S.inboxOpen) return;
|
|
190
|
-
// The two screens share one open thread
|
|
191
|
-
// this
|
|
192
|
-
if (opts.prs
|
|
185
|
+
// The two screens share one open thread, and each holds half the org. Whatever was open on
|
|
186
|
+
// the other one is not in this list, so it is dropped rather than opened beside it.
|
|
187
|
+
if (!!opts.prs !== (S.inboxOpen.kind === "pr")) {
|
|
193
188
|
S.inboxOpen = null;
|
|
194
189
|
writeHash(false);
|
|
195
190
|
return;
|
|
@@ -251,9 +246,12 @@ function inboxScreen(m, opts) {
|
|
|
251
246
|
const onAHuman = (i) => (i.assignees ?? []).some(isHuman);
|
|
252
247
|
|
|
253
248
|
const whoseStaff = S.inboxStaff ? S.data.staff.find((s) => s.handle === S.inboxStaff) : null;
|
|
249
|
+
/* The two screens partition the org rather than overlap on it. Pending work is every pull
|
|
250
|
+
request; the inbox is everything else. An inbox that also listed the PRs said the same
|
|
251
|
+
thing twice and made the badge beside it a number you could not act on. */
|
|
254
252
|
const mine = S.inbox.items
|
|
255
253
|
.filter((i) => belongsTo(i, whoseStaff))
|
|
256
|
-
.filter((i) =>
|
|
254
|
+
.filter((i) => (i.kind === "pr") === !!opts.prs);
|
|
257
255
|
const isOpen = (i) => i.state === "OPEN";
|
|
258
256
|
const scoped = mine.filter(
|
|
259
257
|
S.inboxState === "closed" ? (i) => !isOpen(i) : S.inboxState === "all" ? () => true : isOpen,
|
|
@@ -265,12 +263,17 @@ function inboxScreen(m, opts) {
|
|
|
265
263
|
);
|
|
266
264
|
if (S.inboxFilter === "mine") items = items.filter(onAHuman);
|
|
267
265
|
else if (S.inboxFilter === "decision") items = items.filter((i) => i.labels.includes("decision"));
|
|
268
|
-
|
|
266
|
+
/* `s=pr` was the inbox's "Open work (PRs)" scope, and Pending work is now that screen. An
|
|
267
|
+
old bookmark carrying it is ignored rather than honoured into an empty list. */
|
|
269
268
|
|
|
270
269
|
const open = mine.filter(isOpen);
|
|
271
270
|
const onYou = open.filter(onAHuman).length;
|
|
272
|
-
const prs = open.filter((i) => i.kind === "pr").length;
|
|
273
271
|
const shut = mine.length - open.length;
|
|
272
|
+
/* Pending work owns the pull requests now, so this is a pointer to that screen rather than
|
|
273
|
+
a count of anything on this list. Scoped the same way, or the two would not add up. */
|
|
274
|
+
const prs = S.inbox.items.filter(
|
|
275
|
+
(i) => i.kind === "pr" && isOpen(i) && belongsTo(i, whoseStaff),
|
|
276
|
+
).length;
|
|
274
277
|
const where = whoseStaff
|
|
275
278
|
? " for <b>" + esc(whoseStaff.name) + "</b>"
|
|
276
279
|
: " across " + (S.inbox.repos ?? []).length + " repos";
|
|
@@ -286,7 +289,7 @@ function inboxScreen(m, opts) {
|
|
|
286
289
|
(failing ? " · " + failing + " failing" : "") +
|
|
287
290
|
(S.inboxState === "all" ? " · " + shut + " closed or merged" : "")
|
|
288
291
|
: open.length + " open" + where + " · <b>" + onYou + " on " +
|
|
289
|
-
esc(humanLabel()) + "</b> · " + prs + "
|
|
292
|
+
esc(humanLabel()) + "</b> · " + prs + " in Pending work" +
|
|
290
293
|
(S.inboxState === "all" ? " · " + shut + " closed" : "")) +
|
|
291
294
|
(S.inbox.fetchedAt ? ' <span class="meta">· checked ' + ago(S.inbox.fetchedAt) + "</span>" : "");
|
|
292
295
|
|
|
@@ -324,6 +327,12 @@ function inboxScreen(m, opts) {
|
|
|
324
327
|
'<span class="num">' + (i.kind === "pr" ? "PR " : "") + "#" + i.number + "</span>" +
|
|
325
328
|
(i.checks && i.checks !== "none"
|
|
326
329
|
? '<span class="ck ' + i.checks + '">' + checkGlyph(i.checks) + "</span>" : "") +
|
|
330
|
+
/* Green checks on a branch that no longer applies is the one case where the row
|
|
331
|
+
reads as ready and is not. It has to say so here, not three clicks in. */
|
|
332
|
+
(i.mergeable === "CONFLICTING"
|
|
333
|
+
? '<span class="chip conflict" title="This branch conflicts with its base">' +
|
|
334
|
+
"conflicts</span>"
|
|
335
|
+
: "") +
|
|
327
336
|
// How much conversation is on a thread, which is most of what tells a live one from
|
|
328
337
|
// something that was filed and never answered.
|
|
329
338
|
(i.comments.length
|
|
@@ -365,6 +374,10 @@ function inboxScreen(m, opts) {
|
|
|
365
374
|
' · <span class="' + (item.state === "OPEN" ? "ok" : "") + '">' + esc(item.state) + "</span>" +
|
|
366
375
|
" · " + esc(item.author) + " · " + ago(item.updatedAt) + "</div>" +
|
|
367
376
|
"<h3>" + esc(item.title) + "</h3>" +
|
|
377
|
+
(item.mergeable === "CONFLICTING"
|
|
378
|
+
? '<p class="tconflict">This branch conflicts with its base and cannot be merged ' +
|
|
379
|
+
"until someone rebases or merges the base into it.</p>"
|
|
380
|
+
: "") +
|
|
368
381
|
(item.labels.length
|
|
369
382
|
? '<div class="row tlabels">' +
|
|
370
383
|
item.labels.map((l) => '<span class="chip ' + tone(l) + '">' + esc(l) + "</span>").join("") +
|
|
@@ -527,8 +540,11 @@ function inboxScreen(m, opts) {
|
|
|
527
540
|
b.onclick = async () => {
|
|
528
541
|
b.disabled = true;
|
|
529
542
|
try {
|
|
530
|
-
|
|
531
|
-
|
|
543
|
+
// Same reason as the header's Reply: the wait is after the box closes, not during it.
|
|
544
|
+
const r = await openReply(item, f, () => {
|
|
545
|
+
b.textContent = "Replying…";
|
|
546
|
+
});
|
|
547
|
+
b.textContent = r ? (r.warning ? "sent, with a warning" : "sent") : "Reply";
|
|
532
548
|
} catch (e) {
|
|
533
549
|
b.textContent = e.message;
|
|
534
550
|
}
|
|
@@ -590,24 +606,43 @@ function inboxScreen(m, opts) {
|
|
|
590
606
|
/* One button, because there was no way to tell the two apart. Reply reaches whoever you
|
|
591
607
|
named in it; see `openReply`. */
|
|
592
608
|
const reply = el("button", { className: "ghbtn primary", textContent: "Reply" });
|
|
609
|
+
|
|
610
|
+
/* Posting a comment is a round trip to GitHub through `gh`, and on a cold connection that
|
|
611
|
+
is seconds of a button that still says "Reply" and still takes clicks. Saying nothing
|
|
612
|
+
reads as a page that ignored you, which is how the same comment gets sent twice. */
|
|
613
|
+
const sending = (on) => {
|
|
614
|
+
busy(on, on ? "sending…" : "");
|
|
615
|
+
reply.textContent = on ? "Replying…" : "Reply";
|
|
616
|
+
};
|
|
617
|
+
|
|
593
618
|
reply.onclick = async () => {
|
|
594
619
|
let r;
|
|
595
620
|
try {
|
|
596
|
-
r = await openReply(item, null);
|
|
621
|
+
r = await openReply(item, null, () => sending(true));
|
|
597
622
|
} catch (e) {
|
|
623
|
+
sending(false);
|
|
598
624
|
failed(e);
|
|
599
625
|
return;
|
|
600
626
|
}
|
|
601
|
-
|
|
627
|
+
// Closed the box without sending: nothing ever started, so there is nothing to put back.
|
|
628
|
+
if (!r) {
|
|
629
|
+
sending(false);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
602
632
|
/* Repaint only when something was said on this thread, which is always true here: `ask`
|
|
603
633
|
posts the comment too. A warning means the tracker issue went up and the comment did
|
|
604
634
|
not, and then the status line is the only thing that says so. */
|
|
605
635
|
if (r.warning) {
|
|
636
|
+
sending(false);
|
|
606
637
|
status.className = "meta err";
|
|
607
638
|
status.textContent = r.warning;
|
|
608
639
|
return;
|
|
609
640
|
}
|
|
641
|
+
/* Held across the reload as well as the post. The reply is not on screen until the
|
|
642
|
+
thread repaints, and a button that said "Reply" again before then would be claiming
|
|
643
|
+
it was done over a thread that still did not show it. */
|
|
610
644
|
await reloadThread(item);
|
|
645
|
+
sending(false);
|
|
611
646
|
};
|
|
612
647
|
buttons.push(reply);
|
|
613
648
|
|
|
@@ -651,6 +686,12 @@ function inboxScreen(m, opts) {
|
|
|
651
686
|
own settings. The server reads it from there. */
|
|
652
687
|
if (item.kind === "pr" && item.state === "OPEN") {
|
|
653
688
|
const merge = el("button", { className: "ghbtn", textContent: "Merge" });
|
|
689
|
+
/* A conflicted branch cannot be merged by anyone, so the button says why instead of
|
|
690
|
+
spending a round trip to come back with GitHub's version of the same sentence. */
|
|
691
|
+
if (item.mergeable === "CONFLICTING") {
|
|
692
|
+
merge.disabled = true;
|
|
693
|
+
merge.title = "This branch conflicts with its base";
|
|
694
|
+
}
|
|
654
695
|
merge.onclick = async () => {
|
|
655
696
|
if (!confirm("Merge " + item.repo + " #" + item.number + " into its base branch?")) return;
|
|
656
697
|
busy(true, "merging…");
|
|
@@ -714,7 +755,15 @@ function inboxScreen(m, opts) {
|
|
|
714
755
|
* @param anchor the file this is about, when it was opened from a diff. Its hunk rides along.
|
|
715
756
|
* @returns the result of the write, or null if nothing was written.
|
|
716
757
|
*/
|
|
717
|
-
|
|
758
|
+
/**
|
|
759
|
+
* @param onSend Called once, the moment the box is submitted and the writing starts.
|
|
760
|
+
*
|
|
761
|
+
* The dialog and the send are one `await` to whoever called this, so a caller cannot tell the
|
|
762
|
+
* two apart by itself — and the two want opposite things from a button. While the box is open
|
|
763
|
+
* you are still typing and the button has to stay as it was; once it closes there is a request
|
|
764
|
+
* in flight against GitHub that can take seconds, and that is what needs saying.
|
|
765
|
+
*/
|
|
766
|
+
async function openReply(item, anchor, onSend) {
|
|
718
767
|
const key = item.repo + "#" + item.number + (anchor ? " · " + anchor.path : "");
|
|
719
768
|
let deaf = null;
|
|
720
769
|
|
|
@@ -737,6 +786,7 @@ async function openReply(item, anchor) {
|
|
|
737
786
|
},
|
|
738
787
|
});
|
|
739
788
|
if (!body) return null;
|
|
789
|
+
onSend?.();
|
|
740
790
|
|
|
741
791
|
/* One press, both writes. `ask` posts the comment itself when told to, so these are not two
|
|
742
792
|
requests racing: the tracker issue goes up first because it is the half that actually
|