@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.
@@ -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
- // Scoping pull requests to pull requests is not a filter, and a decision is not a PR.
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. An issue carried over from the inbox is not on
191
- // this one, so it is dropped rather than opened beside a list it is not in.
192
- if (opts.prs && S.inboxOpen.kind !== "pr") {
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) => !opts.prs || i.kind === "pr");
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
- else if (S.inboxFilter === "pr") items = items.filter((i) => i.kind === "pr");
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 + " waiting on a merge" +
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
- const r = await openReply(item, f);
531
- if (r) b.textContent = r.warning ? "sent, with a warning" : "sent";
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
- if (!r) return;
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
- async function openReply(item, anchor) {
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