@nanocollective/roster 0.1.0-alpha.26 → 0.1.0-alpha.28

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.
@@ -5,13 +5,40 @@
5
5
  * and nothing happens until you say so.
6
6
  */
7
7
 
8
- import { getStaffProgress, post } from "../api.js";
8
+ import { post } from "../api.js";
9
9
  import { askYes, sheet } from "../dialog.js";
10
+ import { pending } from "../inflight.js";
10
11
  import { ago, el, esc } from "../dom.js";
12
+ import { whatsLeft, sentence } from "../readiness.js";
11
13
  import { refreshAll } from "../refresh.js";
12
14
  import { S } from "../state.js";
13
15
  import { hireFlow, line, rolePicker } from "./hire.js";
14
16
 
17
+ /**
18
+ * Resolves once nothing is loading and `node` has stopped changing for a moment, or after a few
19
+ * seconds regardless. Quiet alone was not enough: while GitHub was still answering, nothing
20
+ * changed, and the half-built list was shown.
21
+ */
22
+ function settled(node, quiet = 250, most = 6000) {
23
+ return new Promise((resolve) => {
24
+ if (typeof MutationObserver !== "function") return resolve();
25
+ let lastChange = Date.now();
26
+ const obs = new MutationObserver(() => {
27
+ lastChange = Date.now();
28
+ });
29
+ obs.observe(node, { childList: true, subtree: true, attributes: true, characterData: true });
30
+ const started = Date.now();
31
+ const tick = setInterval(() => {
32
+ const now = Date.now();
33
+ if ((pending() === 0 && now - lastChange >= quiet) || now - started >= most) {
34
+ clearInterval(tick);
35
+ obs.disconnect();
36
+ resolve();
37
+ }
38
+ }, 50);
39
+ });
40
+ }
41
+
15
42
  /* The open dialog, if any. It lives on <body>, so it outlives a repaint of this screen, and a
16
43
  background refresh leaves whatever was typed into it alone. */
17
44
  let current = null;
@@ -30,18 +57,27 @@ export function viewStaff(m, o = {}) {
30
57
  /* Tasks open in a dialog. Where there is no dialog element (the test shim) they draw into
31
58
  the pane instead, which is how this screen always drew them. */
32
59
  function present(node) {
33
- if (current?.isOpen()) {
34
- current.set(node);
35
- return;
60
+ const spin = el("div", { className: "sheetloading" }, [el("span", { className: "spinner" })]);
61
+ if (current?.isOpen()) current.set(spin);
62
+ else {
63
+ current = sheet({
64
+ node: spin,
65
+ onClose: () => {
66
+ current = null;
67
+ S.staffOpen = null;
68
+ },
69
+ });
70
+ if (!current) {
71
+ pane.replaceChildren(node);
72
+ return;
73
+ }
36
74
  }
37
- current = sheet({
38
- node,
39
- onClose: () => {
40
- current = null;
41
- S.staffOpen = null;
42
- },
75
+ /* The list is built from half a dozen answers arriving one by one, and showing each as it
76
+ landed meant the dialog reflowed four times. It is shown once it has stopped changing. */
77
+ const mine = current;
78
+ settled(node).then(() => {
79
+ if (mine.isOpen() && current === mine) mine.set(node);
43
80
  });
44
- if (!current) pane.replaceChildren(node);
45
81
  }
46
82
  function dismiss() {
47
83
  if (current) current.close();
@@ -88,10 +124,11 @@ export function viewStaff(m, o = {}) {
88
124
  if (!empty) {
89
125
  const list = el("div", { className: "grid", style: "margin-bottom:18px" });
90
126
  for (const s of S.data.staff) list.append(card(s));
91
- m.append(list);
92
- const hire = el("button", { className: "ghbtn primary", textContent: "Hire someone" });
127
+ // In the grid, where the next person would go, rather than a button under it.
128
+ const hire = el("button", { className: "hirecard", textContent: "Hire someone" });
93
129
  hire.onclick = pick;
94
- m.append(el("div", { className: "row", style: "margin-bottom:14px" }, [hire]));
130
+ list.append(hire);
131
+ m.append(list);
95
132
  }
96
133
  m.append(pane);
97
134
 
@@ -128,18 +165,16 @@ export function viewStaff(m, o = {}) {
128
165
  from disk at once, and the App's secrets and a first run from GitHub when it answers. */
129
166
  const finish = el("button", { className: "ghbtn", textContent: "Set up" });
130
167
  finish.onclick = () => open(roleOf(s));
131
- const unfinished = () => {
168
+ const warn = el("p", { className: "warnline", hidden: true });
169
+ whatsLeft(s).then((left) => {
170
+ if (!left.length) return;
132
171
  finish.textContent = "Finish setting up";
133
172
  finish.className = "ghbtn primary";
134
- };
135
- if (s.rig?.charterStub) unfinished();
136
- else {
137
- getStaffProgress(s.handle)
138
- .then((p) => {
139
- if (p.app === false || p.ran === false) unfinished();
140
- })
141
- .catch(() => {});
142
- }
173
+ d.classList.add("notready");
174
+ warn.hidden = false;
175
+ warn.textContent = "Not ready to run. Still to do: " + sentence(left) + ".";
176
+ });
177
+ d.append(warn);
143
178
 
144
179
  d.append(
145
180
  el("div", { className: "row", style: "margin-top:10px" }, [finish, go, status]),