proctolog 0.1.1 → 0.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +111 -3
- data/README.md +32 -18
- data/app/controllers/proctolog/application_controller.rb +2 -2
- data/app/controllers/proctolog/bursts_controller.rb +1 -1
- data/app/controllers/proctolog/choices_controller.rb +3 -1
- data/app/controllers/proctolog/decisions_controller.rb +1 -1
- data/app/controllers/proctolog/notes_controller.rb +2 -2
- data/app/controllers/proctolog/prompts_controller.rb +1 -1
- data/app/controllers/proctolog/pull_requests_controller.rb +2 -4
- data/app/controllers/proctolog/review_data_controller.rb +3 -1
- data/app/controllers/proctolog/shares_controller.rb +27 -0
- data/app/helpers/proctolog/decisions_helper.rb +6 -2
- data/app/helpers/proctolog/desk_helper.rb +48 -8
- data/app/helpers/proctolog/rounds_helper.rb +4 -2
- data/app/helpers/proctolog/shares_helper.rb +11 -0
- data/app/helpers/proctolog/standings_helper.rb +5 -4
- data/app/models/proctolog/burst.rb +17 -8
- data/app/models/proctolog/choices.rb +8 -0
- data/app/models/proctolog/doctor.rb +23 -12
- data/app/models/proctolog/github.rb +1 -1
- data/app/models/proctolog/localhost.rb +17 -0
- data/app/models/proctolog/marks.rb +1 -1
- data/app/models/proctolog/note.rb +4 -0
- data/app/models/proctolog/owner.rb +3 -0
- data/app/models/proctolog/pick.rb +12 -0
- data/app/models/proctolog/rounds.rb +37 -0
- data/app/models/proctolog/server.rb +1 -7
- data/app/models/proctolog/setup.rb +190 -0
- data/app/models/proctolog/share/ngrok.rb +120 -0
- data/app/models/proctolog/share/settings.rb +23 -0
- data/app/models/proctolog/share/tailscale.rb +103 -0
- data/app/models/proctolog/share.rb +101 -0
- data/app/models/proctolog/standing.rb +24 -7
- data/app/views/proctolog/bursts/_line.html.erb +2 -2
- data/app/views/proctolog/bursts/_side.html.erb +6 -5
- data/app/views/proctolog/bursts/_side_stop.html.erb +4 -4
- data/app/views/proctolog/notes/_card.html.erb +7 -3
- data/app/views/proctolog/open_pull_requests/_side.html.erb +4 -3
- data/app/views/proctolog/open_pull_requests/_start.html.erb +8 -0
- data/app/views/proctolog/open_pull_requests/index.html.erb +4 -5
- data/app/views/proctolog/open_pull_requests/start/_first_time.html.erb +3 -0
- data/app/views/proctolog/open_pull_requests/start/_nothing_open.html.erb +3 -0
- data/app/views/proctolog/open_pull_requests/start/_nothing_written.html.erb +3 -0
- data/app/views/proctolog/shares/_line.html.erb +5 -0
- data/app/views/proctolog/shares/show.html.erb +48 -0
- data/app/views/proctolog/shares/ways/_ngrok.html.erb +6 -0
- data/app/views/proctolog/shares/ways/_tailscale.html.erb +2 -0
- data/bin/proctolog +154 -27
- data/completions/_proctolog +31 -6
- data/config/application.rb +23 -4
- data/config/initializers/grouse.rb +1 -1
- data/config/routes.rb +3 -0
- data/docs/manual/bursts.md +7 -2
- data/docs/manual/deciding.md +4 -1
- data/docs/manual/handover.md +6 -0
- data/docs/manual/pr-format.md +1 -1
- data/docs/manual/reading.md +54 -27
- data/docs/manual/setup.md +59 -0
- data/docs/manual/sharing.md +77 -0
- data/docs/manual/updates.md +11 -4
- data/lib/proctolog/changelog/news.rb +67 -0
- data/lib/proctolog/changelog.rb +75 -0
- data/lib/proctolog/installation.rb +19 -0
- data/lib/proctolog/version.rb +1 -1
- data/public/asking.js +31 -4
- data/public/decisions.js +4 -4
- data/public/desk.js +10 -6
- data/public/keys.js +6 -0
- data/public/notes.js +164 -58
- data/public/pick.css +21 -0
- data/public/respond.css +6 -0
- data/public/respond.js +51 -7
- data/public/review-drafts.js +47 -9
- data/public/review-rounds.js +27 -3
- data/public/share.css +58 -0
- data/public/side.js +11 -20
- data/public/terminal.css +295 -34
- data/public/viewer.js +700 -173
- metadata +22 -1
data/public/notes.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// written in a small form right under what it is about (respond.js).
|
|
9
9
|
|
|
10
10
|
import { helping, keyOf } from "./keys.js";
|
|
11
|
-
import { respond } from "./respond.js";
|
|
11
|
+
import { putAway, respond, saveInPlace } from "./respond.js";
|
|
12
12
|
import { toast } from "./toast.js";
|
|
13
13
|
import { tryIt } from "./try.js";
|
|
14
14
|
import { viewPictures } from "./viewer.js";
|
|
@@ -195,9 +195,111 @@ const placeOn = (figure, box) => ({
|
|
|
195
195
|
},
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
-
// A note on a
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
// A note on a picture is written under it (under the pair, for one of its
|
|
199
|
+
// halves) and saved without leaving the page, which stays where it was and
|
|
200
|
+
// draws the note there.
|
|
201
|
+
const notePicture = (figure, box = [0, 0, 1, 1]) => {
|
|
202
|
+
const slot = slotAfter(figure.closest("figure.picture"));
|
|
203
|
+
const form = respond(slot, placeOn(figure, box));
|
|
204
|
+
saveInPlace(form, async ({ id }) => {
|
|
205
|
+
await redrawNotes();
|
|
206
|
+
putAway(form);
|
|
207
|
+
lightNote(id);
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// The marks of a note on the page and its card, lit where they stand.
|
|
212
|
+
const lightNote = (id) => {
|
|
213
|
+
for (const part of document.querySelectorAll(
|
|
214
|
+
`[data-note="${id}"], [data-card="${id}"]`,
|
|
215
|
+
))
|
|
216
|
+
light(part);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// The page as the desk has it now, or nothing when the desk does not answer.
|
|
220
|
+
const freshCopy = async () => {
|
|
221
|
+
const address = new URL(location.href);
|
|
222
|
+
// The page as it is, not GitHub read again.
|
|
223
|
+
address.searchParams.delete("fresh");
|
|
224
|
+
address.hash = "";
|
|
225
|
+
const response = await fetch(address, {
|
|
226
|
+
headers: { Accept: "text/html" },
|
|
227
|
+
}).catch(() => undefined);
|
|
228
|
+
if (!response?.ok) return undefined;
|
|
229
|
+
return new DOMParser().parseFromString(await response.text(), "text/html");
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Ways to find a place of the page, which a page and its fresh copy answer
|
|
233
|
+
// alike.
|
|
234
|
+
const selecting = (selector) => (page) => page.querySelector(selector);
|
|
235
|
+
|
|
236
|
+
// The count beside the heading of one of the page's own sections, not the
|
|
237
|
+
// description's: the description may have a section of the same name.
|
|
238
|
+
const sectionCount = (heading) =>
|
|
239
|
+
selecting(`.page > details[data-heading="${heading}"] > summary .count`);
|
|
240
|
+
|
|
241
|
+
// The count on the outline's line for a section: the last line so named, as
|
|
242
|
+
// the description's sections come first and may have the same name.
|
|
243
|
+
const outlineCount = (name) => (page) =>
|
|
244
|
+
[...page.querySelectorAll("[data-outline] > *")]
|
|
245
|
+
.findLast((line) => line.querySelector(".name")?.textContent === name)
|
|
246
|
+
?.querySelector(".count");
|
|
247
|
+
|
|
248
|
+
// Where the page shows what a saved note changes: where the pull request
|
|
249
|
+
// stands, the Standing section, the cards of the notes, and the counts of what
|
|
250
|
+
// is open, beside two headings and on the outline. The outline's other counts
|
|
251
|
+
// are kept by the scripts that own them.
|
|
252
|
+
const CHANGED_BY_NOTES = [
|
|
253
|
+
selecting("#standing"),
|
|
254
|
+
selecting("section.standing"),
|
|
255
|
+
sectionCount("Standing"),
|
|
256
|
+
outlineCount("Standing"),
|
|
257
|
+
selecting("aside.margin"),
|
|
258
|
+
sectionCount("Notes"),
|
|
259
|
+
outlineCount("Notes"),
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
// The fresh copy's element in place of the page's, when both have one.
|
|
263
|
+
const swapIn = (now, was) => {
|
|
264
|
+
if (!now || !was) return;
|
|
265
|
+
was.replaceWith(now);
|
|
266
|
+
readable(now);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// The notes as the desk now has them, drawn without the page going anywhere:
|
|
270
|
+
// the boxes on its pictures, then everything else that notes change, from a
|
|
271
|
+
// fresh copy of the page.
|
|
272
|
+
const redrawNotes = async () => {
|
|
273
|
+
const fresh = await freshCopy();
|
|
274
|
+
if (!fresh) return location.reload();
|
|
275
|
+
redrawRegions(fresh);
|
|
276
|
+
for (const find of CHANGED_BY_NOTES) swapIn(find(fresh), find(document));
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// The pictures of the description that notes are drawn on, by their asset:
|
|
280
|
+
// the same picture shown twice is the same picture twice, in order.
|
|
281
|
+
const figuresByAsset = (page) =>
|
|
282
|
+
Map.groupBy(
|
|
283
|
+
page.querySelectorAll(
|
|
284
|
+
"main.description :is(figure.picture:not(.pair), figure.pair > .half)[data-asset]",
|
|
285
|
+
),
|
|
286
|
+
(figure) => figure.dataset.asset,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const redrawRegions = (fresh) => {
|
|
290
|
+
const drawn = figuresByAsset(fresh);
|
|
291
|
+
for (const [asset, figures] of figuresByAsset(document))
|
|
292
|
+
figures.forEach((figure, index) => {
|
|
293
|
+
const now = drawn.get(asset)?.[index];
|
|
294
|
+
if (!now) return;
|
|
295
|
+
// The region being drawn for a note not yet saved stays.
|
|
296
|
+
for (const region of figure.querySelectorAll(
|
|
297
|
+
":scope > .region:not(.draft)",
|
|
298
|
+
))
|
|
299
|
+
region.remove();
|
|
300
|
+
figure.append(...now.querySelectorAll(":scope > .region"));
|
|
301
|
+
});
|
|
302
|
+
};
|
|
201
303
|
|
|
202
304
|
let pending;
|
|
203
305
|
document.addEventListener("selectionchange", () => {
|
|
@@ -211,11 +313,26 @@ document.addEventListener("selectionchange", () => {
|
|
|
211
313
|
rect.right,
|
|
212
314
|
innerWidth - selectionButton.offsetWidth - 8,
|
|
213
315
|
);
|
|
214
|
-
|
|
215
|
-
selectionButton.style.
|
|
316
|
+
const origin = originOf(selectionButton);
|
|
317
|
+
selectionButton.style.left = `${Math.max(8, left) - origin.left}px`;
|
|
318
|
+
selectionButton.style.top = `${rect.top - origin.top - selectionButton.offsetHeight - 6}px`;
|
|
216
319
|
}, 120);
|
|
217
320
|
});
|
|
218
321
|
|
|
322
|
+
// Where an element placed absolutely counts its left and top from, on the
|
|
323
|
+
// screen: the box of the nearest ancestor laid out on its own (the column
|
|
324
|
+
// beside the burst's pane), or else the page.
|
|
325
|
+
const originOf = (element) => {
|
|
326
|
+
const parent = element.offsetParent;
|
|
327
|
+
if (!parent || getComputedStyle(parent).position === "static")
|
|
328
|
+
return { left: -scrollX, top: -scrollY };
|
|
329
|
+
const box = parent.getBoundingClientRect();
|
|
330
|
+
return {
|
|
331
|
+
left: box.left + parent.clientLeft - parent.scrollLeft,
|
|
332
|
+
top: box.top + parent.clientTop - parent.scrollTop,
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
|
|
219
336
|
// Keeps the selection alive while the button is pressed.
|
|
220
337
|
selectionButton.addEventListener("mousedown", (event) =>
|
|
221
338
|
event.preventDefault(),
|
|
@@ -353,14 +470,18 @@ document.addEventListener("pointercancel", () => {
|
|
|
353
470
|
drag = null;
|
|
354
471
|
});
|
|
355
472
|
|
|
356
|
-
// Reading has its own cursor; Tab still reaches every link and control.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
473
|
+
// Reading has its own cursor; Tab still reaches every link and control. What
|
|
474
|
+
// the page draws again, a card or a condition, is read the same way.
|
|
475
|
+
const readable = (root) => {
|
|
476
|
+
for (const element of root.querySelectorAll(
|
|
477
|
+
"h1, h2, h3, h4, summary, .conditions > li, li.scenario, [data-card]",
|
|
478
|
+
)) {
|
|
479
|
+
if (element.closest("[data-desk]")) continue;
|
|
480
|
+
element.dataset.reading = "";
|
|
481
|
+
if (element.localName !== "summary") element.tabIndex = -1;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
readable(document);
|
|
364
485
|
for (const body of document.querySelectorAll("[data-quotable]")) {
|
|
365
486
|
for (const block of blocksIn(body)) {
|
|
366
487
|
if (!block.matches("figure.picture") && !hasWords(block)) continue;
|
|
@@ -498,7 +619,10 @@ const keys = {
|
|
|
498
619
|
G: () => {
|
|
499
620
|
const link = document.querySelector('.whole a[data-key="G"]');
|
|
500
621
|
if (link) link.click();
|
|
501
|
-
else
|
|
622
|
+
else
|
|
623
|
+
toast(
|
|
624
|
+
"Nobody has opened this pull request yet: GitHub has nothing of it.",
|
|
625
|
+
);
|
|
502
626
|
},
|
|
503
627
|
};
|
|
504
628
|
|
|
@@ -539,10 +663,9 @@ const linkedShown = (preview) => {
|
|
|
539
663
|
// A note on a place marked in the viewer is written beside it, and saved
|
|
540
664
|
// without leaving it, so that the owner goes on looking where they were. A
|
|
541
665
|
// note drawn there opens beside it the same way, with its words, to be
|
|
542
|
-
// written over or withdrawn.
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
let notedAt;
|
|
666
|
+
// written over or withdrawn. Once the viewer closes, the page draws them
|
|
667
|
+
// where it is.
|
|
668
|
+
let notedHere = false;
|
|
546
669
|
const notesAddress = document
|
|
547
670
|
.querySelector("#note-form")
|
|
548
671
|
?.content.querySelector("form")
|
|
@@ -552,7 +675,13 @@ const savedHere = new Map();
|
|
|
552
675
|
const writtenNote = (id) => {
|
|
553
676
|
const card = document.querySelector(`[data-card="${id}"]`);
|
|
554
677
|
const said = savedHere.get(id) ?? card?.dataset;
|
|
555
|
-
return
|
|
678
|
+
return (
|
|
679
|
+
said && {
|
|
680
|
+
address: `${notesAddress}/${id}`,
|
|
681
|
+
kind: said.kind,
|
|
682
|
+
text: said.text,
|
|
683
|
+
}
|
|
684
|
+
);
|
|
556
685
|
};
|
|
557
686
|
const noteInViewer = ({ figure }, box, beside, id) => {
|
|
558
687
|
if (!figure?.dataset.asset) {
|
|
@@ -564,46 +693,23 @@ const noteInViewer = ({ figure }, box, beside, id) => {
|
|
|
564
693
|
toast("That note is not on this page, so it cannot be opened here.");
|
|
565
694
|
return false;
|
|
566
695
|
}
|
|
567
|
-
respond(beside, { ...placeOn(figure, box), written });
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const response = await fetch(form.action, {
|
|
574
|
-
method: "POST",
|
|
575
|
-
body: new FormData(form, event.submitter),
|
|
576
|
-
headers: { Accept: "application/json" },
|
|
577
|
-
}).catch(() => undefined);
|
|
578
|
-
if (!response?.headers.get("content-type")?.includes("json"))
|
|
579
|
-
return form.submit();
|
|
580
|
-
const saved = await response.json();
|
|
581
|
-
const withdrawn = event.submitter?.matches("[data-withdraw]") ?? false;
|
|
582
|
-
notedAt = saved.at;
|
|
583
|
-
savedHere.set(saved.id, {
|
|
584
|
-
kind: form.elements.kind.value,
|
|
585
|
-
text: form.elements.text.value.trim(),
|
|
586
|
-
});
|
|
587
|
-
beside.dispatchEvent(
|
|
588
|
-
new CustomEvent("noted", { detail: { id: saved.id, withdrawn } }),
|
|
589
|
-
);
|
|
696
|
+
const form = respond(beside, { ...placeOn(figure, box), written });
|
|
697
|
+
saveInPlace(form, ({ id: saved, withdrawn }) => {
|
|
698
|
+
notedHere = true;
|
|
699
|
+
savedHere.set(saved, {
|
|
700
|
+
kind: form.elements.kind.value,
|
|
701
|
+
text: form.elements.text.value.trim(),
|
|
590
702
|
});
|
|
591
|
-
|
|
703
|
+
beside.dispatchEvent(
|
|
704
|
+
new CustomEvent("noted", { detail: { id: saved, withdrawn } }),
|
|
705
|
+
);
|
|
706
|
+
});
|
|
592
707
|
return true;
|
|
593
708
|
};
|
|
594
|
-
const
|
|
595
|
-
if (!
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// A decision being decided keeps the page where it is: the page shows
|
|
599
|
-
// the note the next time it loads.
|
|
600
|
-
if (document.body.dataset.focus)
|
|
601
|
-
return toast(
|
|
602
|
-
"Your note is kept. The description shows it the next time the page loads.",
|
|
603
|
-
);
|
|
604
|
-
if (at.pathname !== location.pathname) return location.assign(at);
|
|
605
|
-
history.replaceState(history.state, "", at);
|
|
606
|
-
location.reload();
|
|
709
|
+
const redrawSavedNotes = () => {
|
|
710
|
+
if (!notedHere) return;
|
|
711
|
+
notedHere = false;
|
|
712
|
+
redrawNotes();
|
|
607
713
|
};
|
|
608
714
|
// The pictures Space would open here, and which of them first; nothing when
|
|
609
715
|
// the owner is not at a picture, and Space is the browser's to scroll with.
|
|
@@ -633,7 +739,7 @@ const lookClosely = (found = picturesHere(), first = found?.first) => {
|
|
|
633
739
|
if (found)
|
|
634
740
|
viewPictures(found.pictures, first, {
|
|
635
741
|
note: noteInViewer,
|
|
636
|
-
closed:
|
|
742
|
+
closed: redrawSavedNotes,
|
|
637
743
|
});
|
|
638
744
|
};
|
|
639
745
|
keys[" "] = lookClosely;
|
data/public/pick.css
CHANGED
|
@@ -97,3 +97,24 @@
|
|
|
97
97
|
margin-left: 10ch;
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/* What to do here, before anything to pick: framed, so a first look lands on it. */
|
|
102
|
+
.pick .start {
|
|
103
|
+
margin: 0 0 24px;
|
|
104
|
+
padding: 12px 16px;
|
|
105
|
+
border: 1px solid var(--rule-strong);
|
|
106
|
+
background: var(--surface);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.pick .start h2 {
|
|
110
|
+
margin-top: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.pick .start p {
|
|
114
|
+
max-width: 72ch;
|
|
115
|
+
margin: 0 0 8px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.pick .start p:last-child {
|
|
119
|
+
margin-bottom: 0;
|
|
120
|
+
}
|
data/public/respond.css
CHANGED
|
@@ -109,6 +109,12 @@ li.card p {
|
|
|
109
109
|
font-size: 0.85rem;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/* Where a note answered in a handover is judged, in place of the buttons. */
|
|
113
|
+
.card .moves .judged {
|
|
114
|
+
margin: 0;
|
|
115
|
+
color: var(--quiet);
|
|
116
|
+
}
|
|
117
|
+
|
|
112
118
|
.card[data-state="confirmed"],
|
|
113
119
|
.card[data-state="withdrawn"] {
|
|
114
120
|
opacity: 0.7;
|
data/public/respond.js
CHANGED
|
@@ -15,10 +15,11 @@ const fill = (form, name, values) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// Opens the form in `slot`, or turns the one already there to this note,
|
|
18
|
-
// keeping what was typed. `quote` and `picture` are
|
|
19
|
-
// `back` is the address to come back to once the note is
|
|
20
|
-
// is not the pull request's own. `written`, a note
|
|
21
|
-
// `address`, `kind` and `text`, opens it again to be
|
|
18
|
+
// keeping what was typed, and gives the form back. `quote` and `picture` are
|
|
19
|
+
// the parts of a place; `back` is the address to come back to once the note is
|
|
20
|
+
// saved, when the page is not the pull request's own. `written`, a note
|
|
21
|
+
// already there by its `address`, `kind` and `text`, opens it again to be
|
|
22
|
+
// written over or withdrawn.
|
|
22
23
|
export const respond = (
|
|
23
24
|
slot,
|
|
24
25
|
{ kind = "change", action, on, quote, picture, back, written },
|
|
@@ -54,17 +55,60 @@ export const respond = (
|
|
|
54
55
|
// Written again from where it ends.
|
|
55
56
|
const end = form.elements.text.value.length;
|
|
56
57
|
if (written) form.elements.text.setSelectionRange(end, end);
|
|
58
|
+
return form;
|
|
57
59
|
};
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
// The form taken away, its note saved or not: its slot says so by "closed".
|
|
62
|
+
// It is given the form, or anything in it.
|
|
63
|
+
export const putAway = (inside) => {
|
|
64
|
+
const holder = inside.closest(".respond");
|
|
60
65
|
const slot = holder.parentElement;
|
|
61
66
|
holder.remove();
|
|
62
67
|
slot.dispatchEvent(new CustomEvent("closed", { bubbles: true }));
|
|
63
68
|
};
|
|
64
69
|
|
|
70
|
+
// A note sent from `form` without leaving the page: `saved` is called with
|
|
71
|
+
// what the desk answers, the note's `id` and `at`, the address that shows it,
|
|
72
|
+
// and whether it was withdrawn. A note the desk would not take is sent as the
|
|
73
|
+
// page sends it, which says why.
|
|
74
|
+
export const saveInPlace = (form, saved) => {
|
|
75
|
+
// The form already in a slot is turned to each note opened there: it is
|
|
76
|
+
// listened to once.
|
|
77
|
+
if (form.dataset.inPlace !== undefined) return;
|
|
78
|
+
form.dataset.inPlace = "";
|
|
79
|
+
form.addEventListener("submit", async (event) => {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
const response = await fetch(form.action, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: new FormData(form, event.submitter),
|
|
84
|
+
headers: { Accept: "application/json" },
|
|
85
|
+
}).catch(() => undefined);
|
|
86
|
+
if (!response?.headers.get("content-type")?.includes("json"))
|
|
87
|
+
return sendAsThePage(form, event.submitter);
|
|
88
|
+
saved({
|
|
89
|
+
...(await response.json()),
|
|
90
|
+
withdrawn: event.submitter?.matches("[data-withdraw]") ?? false,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// The form sent as the page sends it, with the button pressed, which
|
|
96
|
+
// form.submit() leaves out: without it a withdrawal would go as an edit.
|
|
97
|
+
const sendAsThePage = (form, button) => {
|
|
98
|
+
if (button?.name)
|
|
99
|
+
form.append(
|
|
100
|
+
Object.assign(document.createElement("input"), {
|
|
101
|
+
type: "hidden",
|
|
102
|
+
name: button.name,
|
|
103
|
+
value: button.value,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
form.submit();
|
|
107
|
+
};
|
|
108
|
+
|
|
65
109
|
document.addEventListener("click", (event) => {
|
|
66
110
|
const cancel = event.target.closest("[data-cancel]");
|
|
67
|
-
if (cancel) return
|
|
111
|
+
if (cancel) return putAway(cancel);
|
|
68
112
|
|
|
69
113
|
const action = event.target.closest("[data-respond]");
|
|
70
114
|
if (!action) return;
|
|
@@ -91,5 +135,5 @@ document.addEventListener("keydown", (event) => {
|
|
|
91
135
|
|
|
92
136
|
if (event.key === "Enter" && (event.metaKey || event.ctrlKey))
|
|
93
137
|
holder.querySelector("form").requestSubmit();
|
|
94
|
-
if (event.key === "Escape" && !event.target.value)
|
|
138
|
+
if (event.key === "Escape" && !event.target.value) putAway(holder);
|
|
95
139
|
});
|
data/public/review-drafts.js
CHANGED
|
@@ -447,11 +447,14 @@ export const progress = (pr) => {
|
|
|
447
447
|
const mine = (entry) => of(entry.decision.pr);
|
|
448
448
|
const picked = items.filter(chosen).length;
|
|
449
449
|
const ticked = items.filter((item) => !chosen(item) && item.ticked).length;
|
|
450
|
+
const blocking = items.filter((item) => item.grade !== "non_blocking");
|
|
450
451
|
return {
|
|
451
452
|
total: items.length,
|
|
452
453
|
chosen: picked,
|
|
453
454
|
ticked,
|
|
454
455
|
left: items.length - picked - ticked,
|
|
456
|
+
blocking: blocking.length,
|
|
457
|
+
blockingLeft: blocking.filter((item) => !reviewed(item)).length,
|
|
455
458
|
drafts: state.pending.filter(mine).length,
|
|
456
459
|
again: state.choices.filter(
|
|
457
460
|
(entry) =>
|
|
@@ -485,20 +488,55 @@ const changed = () => {
|
|
|
485
488
|
};
|
|
486
489
|
|
|
487
490
|
// The pane at the left counts the same choices: its rows follow them as they
|
|
488
|
-
// are made, in
|
|
491
|
+
// are made, in beads as the server draws them (DeskHelper#decision_beads).
|
|
492
|
+
const reviewedInWords = ({ name, total, left }) => {
|
|
493
|
+
const decisions = `${name} ${total === 1 ? "decision" : "decisions"}`;
|
|
494
|
+
const reviewed = total - left;
|
|
495
|
+
if (!left)
|
|
496
|
+
return total === 1
|
|
497
|
+
? `its one ${decisions} reviewed`
|
|
498
|
+
: `all ${total} ${decisions} reviewed`;
|
|
499
|
+
if (!reviewed) return `none of ${total} ${decisions} reviewed yet`;
|
|
500
|
+
return `${reviewed} of ${total} ${decisions} reviewed`;
|
|
501
|
+
};
|
|
502
|
+
const span = (properties, ...children) => {
|
|
503
|
+
const element = Object.assign(document.createElement("span"), properties);
|
|
504
|
+
element.append(...children);
|
|
505
|
+
return element;
|
|
506
|
+
};
|
|
507
|
+
const beadsOf = (now) =>
|
|
508
|
+
span(
|
|
509
|
+
{ className: "beads" },
|
|
510
|
+
...[
|
|
511
|
+
{ name: "blocking", total: now.blocking, left: now.blockingLeft },
|
|
512
|
+
{
|
|
513
|
+
name: "non-blocking",
|
|
514
|
+
total: now.total - now.blocking,
|
|
515
|
+
left: now.left - now.blockingLeft,
|
|
516
|
+
},
|
|
517
|
+
]
|
|
518
|
+
.filter((grade) => grade.total)
|
|
519
|
+
.map((grade) =>
|
|
520
|
+
span(
|
|
521
|
+
{
|
|
522
|
+
className: grade.name,
|
|
523
|
+
role: "img",
|
|
524
|
+
ariaLabel: reviewedInWords(grade),
|
|
525
|
+
},
|
|
526
|
+
...Array.from({ length: grade.total }, (_, at) =>
|
|
527
|
+
span({
|
|
528
|
+
className: at < grade.total - grade.left ? "bead reviewed" : "bead",
|
|
529
|
+
}),
|
|
530
|
+
),
|
|
531
|
+
),
|
|
532
|
+
),
|
|
533
|
+
);
|
|
489
534
|
const side = () => {
|
|
490
535
|
const count = (number) => number.toLocaleString("en-US");
|
|
491
536
|
for (const row of document.querySelectorAll("[data-side-pr]")) {
|
|
492
537
|
const now = progress(prNumber(row.dataset.sidePr));
|
|
493
538
|
if (!now.total) continue;
|
|
494
|
-
|
|
495
|
-
row.querySelector("[data-side-mark]").textContent = !now.left
|
|
496
|
-
? "x"
|
|
497
|
-
: done
|
|
498
|
-
? "~"
|
|
499
|
-
: "";
|
|
500
|
-
row.querySelector("[data-side-fraction]").textContent =
|
|
501
|
-
`${done}/${now.total}`;
|
|
539
|
+
row.querySelector("[data-side-decisions]").replaceChildren(beadsOf(now));
|
|
502
540
|
}
|
|
503
541
|
const overall = document.querySelector("[data-side-overall]");
|
|
504
542
|
if (!overall || !inventory.length) return;
|
data/public/review-rounds.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { helping, keyOf } from "./keys.js";
|
|
1
|
+
import { helping, keyOf, overThePage } from "./keys.js";
|
|
2
2
|
import { prNumber } from "./pr-number.js";
|
|
3
3
|
import {
|
|
4
4
|
announce,
|
|
5
5
|
handOver,
|
|
6
6
|
draftsState,
|
|
7
7
|
registerRounds,
|
|
8
|
+
settled,
|
|
8
9
|
stale,
|
|
9
10
|
updateDrafts,
|
|
10
11
|
} from "./review-drafts.js";
|
|
@@ -28,6 +29,25 @@ const reviewData = async () => {
|
|
|
28
29
|
if (!response.ok) throw new Error(`Desk returned ${response.status}`);
|
|
29
30
|
return response.json();
|
|
30
31
|
};
|
|
32
|
+
// The pane's ↩, on each pull request something came back on, in the words
|
|
33
|
+
// the desk gives (DeskHelper#side_mark).
|
|
34
|
+
const markThePane = (back = {}) => {
|
|
35
|
+
for (const row of document.querySelectorAll("[data-side-pr]")) {
|
|
36
|
+
const said = back[row.dataset.sidePr];
|
|
37
|
+
Object.assign(row.querySelector("[data-side-mark]"), {
|
|
38
|
+
textContent: said ? "↩" : "",
|
|
39
|
+
role: said ? "img" : null,
|
|
40
|
+
ariaLabel: said ?? null,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
// Once the desk has the owner's word on an answer, it has moved the notes the
|
|
45
|
+
// answer was to (Rounds#move_notes), and the pane is marked again from it. A
|
|
46
|
+
// desk that does not answer leaves the pane as the page drew it.
|
|
47
|
+
const followThePane = async () => {
|
|
48
|
+
await settled();
|
|
49
|
+
markThePane((await reviewData()).back);
|
|
50
|
+
};
|
|
31
51
|
// What GitHub draws of each piece of markdown, by its words, asked once while
|
|
32
52
|
// the page is open: returned work is laid out again at every change of the
|
|
33
53
|
// drafts, and must not fall back to markdown each time. A piece that cannot
|
|
@@ -589,6 +609,7 @@ const mark = (item, resolution, latest, kind, text) => {
|
|
|
589
609
|
},
|
|
590
610
|
};
|
|
591
611
|
});
|
|
612
|
+
followThePane().catch(() => {});
|
|
592
613
|
selectResponse(
|
|
593
614
|
visibleResponses().find((node) => node.dataset.request === next) ||
|
|
594
615
|
visibleResponses()[0],
|
|
@@ -1275,6 +1296,7 @@ registerRounds({
|
|
|
1275
1296
|
context = await reviewData();
|
|
1276
1297
|
importServerBatches();
|
|
1277
1298
|
importServerResponses();
|
|
1299
|
+
markThePane(context.back);
|
|
1278
1300
|
},
|
|
1279
1301
|
});
|
|
1280
1302
|
|
|
@@ -1290,6 +1312,7 @@ tools
|
|
|
1290
1312
|
? "Agent response imported. Confirm or reply to its existing requests."
|
|
1291
1313
|
: "The desk has this answer already.",
|
|
1292
1314
|
);
|
|
1315
|
+
followThePane().catch(() => {});
|
|
1293
1316
|
} catch (error) {
|
|
1294
1317
|
announce(`Response could not be imported: ${error.message}`);
|
|
1295
1318
|
} finally {
|
|
@@ -1361,8 +1384,9 @@ document
|
|
|
1361
1384
|
.querySelector("[data-enter-responses]")
|
|
1362
1385
|
?.addEventListener("click", enterResponses);
|
|
1363
1386
|
document.addEventListener("focusin", (event) => {
|
|
1364
|
-
// A picture looked at closely
|
|
1365
|
-
|
|
1387
|
+
// A picture looked at closely, or a note written in Grouse's overlay, leaves
|
|
1388
|
+
// the response marked: Esc comes back to it.
|
|
1389
|
+
if (overThePage(event.target)) return;
|
|
1366
1390
|
const node = event.target.closest("[data-request]");
|
|
1367
1391
|
if (node) markResponse(node);
|
|
1368
1392
|
else activeResponse?.removeAttribute("aria-current");
|
data/public/share.css
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* Sharing the desk: where it is now, then each way to share it, in the
|
|
2
|
+
route's own frame, with what it needs first or its button under its words. */
|
|
3
|
+
|
|
4
|
+
.share .now {
|
|
5
|
+
margin-block: 0 0.5lh;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.share .ways {
|
|
9
|
+
border: 1px solid var(--rule);
|
|
10
|
+
border-top: 0;
|
|
11
|
+
background: var(--surface);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.share .way {
|
|
15
|
+
display: grid;
|
|
16
|
+
row-gap: 8px;
|
|
17
|
+
padding: var(--row-pad) 12px;
|
|
18
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.share .way:last-child {
|
|
22
|
+
border-bottom: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.share .way h3 {
|
|
26
|
+
margin: 0;
|
|
27
|
+
font-size: 1rem;
|
|
28
|
+
color: var(--ink);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.share .way p {
|
|
32
|
+
margin: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.share .way .facts {
|
|
36
|
+
color: var(--quiet);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.share .way form {
|
|
40
|
+
display: grid;
|
|
41
|
+
row-gap: 8px;
|
|
42
|
+
justify-items: start;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.share .allow {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
gap: 4px 2ch;
|
|
49
|
+
align-items: baseline;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.share .allow input {
|
|
53
|
+
width: min(40ch, 100%);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.share code {
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
data/public/side.js
CHANGED
|
@@ -14,32 +14,23 @@ const made = (name, properties = {}, ...children) => {
|
|
|
14
14
|
return element;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
// "3 of 19 decisions reviewed", from the row's "~" and "3/19".
|
|
18
|
-
const progressInWords = (mark, fraction) => {
|
|
19
|
-
if (!fraction) return "no decisions to review";
|
|
20
|
-
const [reviewed, total] = fraction.split("/").map(Number);
|
|
21
|
-
const decisions = total === 1 ? "decision" : "decisions";
|
|
22
|
-
if (mark === "x")
|
|
23
|
-
return total === 1
|
|
24
|
-
? "its one decision reviewed"
|
|
25
|
-
: `all ${total} decisions reviewed`;
|
|
26
|
-
if (!reviewed) return `none of ${total} ${decisions} reviewed yet`;
|
|
27
|
-
return `${reviewed} of ${total} ${decisions} reviewed`;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
17
|
const MOVED = {
|
|
31
18
|
changed: "changed on GitHub since it was read",
|
|
32
19
|
closed: "no longer open on GitHub",
|
|
33
20
|
};
|
|
34
21
|
|
|
22
|
+
// The row's marks said in words: what came back from the agent, then each
|
|
23
|
+
// grade's beads with what they count, in the words the server labelled them with.
|
|
35
24
|
const marksOf = (row) => {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
25
|
+
const back = row.querySelector("[data-side-mark]");
|
|
26
|
+
const grades = row.querySelectorAll("[data-side-decisions] [role=img]");
|
|
38
27
|
return [
|
|
39
|
-
[
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
back.textContent && [back.textContent, back.ariaLabel],
|
|
29
|
+
...[...grades].map((grade) => [
|
|
30
|
+
made("span", { className: "beads" }, grade.cloneNode(true)),
|
|
31
|
+
grade.ariaLabel,
|
|
32
|
+
]),
|
|
33
|
+
!grades.length && ["", "no decisions to review"],
|
|
43
34
|
row.dataset.moved && ["*", MOVED[row.dataset.moved]],
|
|
44
35
|
].filter(Boolean);
|
|
45
36
|
};
|
|
@@ -62,7 +53,7 @@ const cardOf = (row) => {
|
|
|
62
53
|
made(
|
|
63
54
|
"div",
|
|
64
55
|
{},
|
|
65
|
-
made("dt", {
|
|
56
|
+
made("dt", {}, shown),
|
|
66
57
|
made("dd", { textContent: said }),
|
|
67
58
|
),
|
|
68
59
|
),
|