proctolog 0.1.1 → 0.2.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 +77 -3
- data/README.md +15 -4
- data/app/controllers/proctolog/application_controller.rb +1 -1
- 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 +9 -1
- data/app/models/proctolog/github.rb +1 -1
- 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/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 +92 -6
- data/completions/_proctolog +28 -4
- 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/reading.md +54 -27
- data/docs/manual/sharing.md +77 -0
- data/lib/proctolog/changelog.rb +50 -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 +18 -1
data/public/asking.js
CHANGED
|
@@ -18,11 +18,25 @@ const quoted = (text) =>
|
|
|
18
18
|
.join("\n");
|
|
19
19
|
|
|
20
20
|
// Where a decision stands, its words, and the pictures it rests on that
|
|
21
|
-
// the desk holds on this Mac.
|
|
22
|
-
|
|
21
|
+
// the desk holds on this Mac. One nobody has opened yet has no address on
|
|
22
|
+
// GitHub: its branch and the file its description is kept in stand there.
|
|
23
|
+
const about = ({
|
|
24
|
+
pr,
|
|
25
|
+
title,
|
|
26
|
+
url,
|
|
27
|
+
branch,
|
|
28
|
+
base,
|
|
29
|
+
file,
|
|
30
|
+
head,
|
|
31
|
+
text,
|
|
32
|
+
pictures: shots,
|
|
33
|
+
}) => {
|
|
23
34
|
const here = shots.filter((picture) => picture.file);
|
|
35
|
+
const where = file
|
|
36
|
+
? `not opened, on ${branch} at ${head.slice(0, 7)}, into ${base}, described in ${file}`
|
|
37
|
+
: `(${url}), on ${branch} at ${head.slice(0, 7)}`;
|
|
24
38
|
return [
|
|
25
|
-
`#${pr}, "${title}"
|
|
39
|
+
`#${pr}, "${title}" ${where}:`,
|
|
26
40
|
quoted(text),
|
|
27
41
|
here.length &&
|
|
28
42
|
`Its pictures, on this Mac:\n${here.map((picture) => `- ${picture.name}: ${pictures}/${picture.file}`).join("\n")}`,
|
|
@@ -31,6 +45,19 @@ const about = ({ pr, title, url, branch, head, text, pictures: shots }) => {
|
|
|
31
45
|
.join("\n\n");
|
|
32
46
|
};
|
|
33
47
|
|
|
48
|
+
// How to read the pull requests asked about: GitHub has the opened ones,
|
|
49
|
+
// git and the store the rest.
|
|
50
|
+
const reading = (decisions) =>
|
|
51
|
+
[
|
|
52
|
+
decisions.some((each) => !each.file) &&
|
|
53
|
+
"Read a pull request with `gh pr view` and `gh pr diff`.",
|
|
54
|
+
decisions.some((each) => each.file) &&
|
|
55
|
+
"A pull request not opened yet is not on GitHub: read its code with `git log` and `git diff` from its base to its branch, and its description in the file named here.",
|
|
56
|
+
"If one has moved on since the head named here, say so.",
|
|
57
|
+
]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join(" ");
|
|
60
|
+
|
|
34
61
|
export const askingPrompt = (ids, question) => {
|
|
35
62
|
const decisions = ids.map((id) => inventory.find((each) => each.id === id));
|
|
36
63
|
const which =
|
|
@@ -38,7 +65,7 @@ export const askingPrompt = (ids, question) => {
|
|
|
38
65
|
? "one of their decisions"
|
|
39
66
|
: `${decisions.length} of their decisions`;
|
|
40
67
|
return [
|
|
41
|
-
`I am reviewing pull requests in PRoctolog and have a question on ${which} before I choose. Answer it plainly, with evidence. Change nothing while you do: no edits, no commits, no checkout, nothing on GitHub.
|
|
68
|
+
`I am reviewing pull requests in PRoctolog and have a question on ${which} before I choose. Answer it plainly, with evidence. Change nothing while you do: no edits, no commits, no checkout, nothing on GitHub. ${reading(decisions)}`,
|
|
42
69
|
...decisions.map(about),
|
|
43
70
|
`My question:\n${question.trim()}`,
|
|
44
71
|
].join("\n\n");
|
data/public/decisions.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
prepareScreenshots,
|
|
10
10
|
toggleScreenshots,
|
|
11
11
|
} from "./decision-screenshots.js";
|
|
12
|
-
import { helping, keyOf, mode } from "./keys.js";
|
|
12
|
+
import { helping, keyOf, mode, overThePage } from "./keys.js";
|
|
13
13
|
import { withMentions } from "./mentions.js";
|
|
14
14
|
import { prNumber } from "./pr-number.js";
|
|
15
15
|
import {
|
|
@@ -1092,9 +1092,9 @@ document.addEventListener("click", (event) => {
|
|
|
1092
1092
|
});
|
|
1093
1093
|
|
|
1094
1094
|
document.addEventListener("focusin", (event) => {
|
|
1095
|
-
// A
|
|
1096
|
-
// without leaving the decision: its Esc comes back to it.
|
|
1097
|
-
if (event.target
|
|
1095
|
+
// A picture looked at closely, or a note written in Grouse's overlay, takes
|
|
1096
|
+
// the focus without leaving the decision: its Esc comes back to it.
|
|
1097
|
+
if (overThePage(event.target)) return;
|
|
1098
1098
|
const box = event.target.closest("[data-decision]");
|
|
1099
1099
|
if (inline) {
|
|
1100
1100
|
// Deciding begins only when it is asked for (f, Enter, a click, a choice's
|
data/public/desk.js
CHANGED
|
@@ -27,14 +27,18 @@ const follow = (key) => {
|
|
|
27
27
|
if (link) location.assign(link.href + location.hash);
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
// Opens the next pull request
|
|
31
|
-
//
|
|
32
|
-
// the
|
|
30
|
+
// Opens the next pull request that asks for the owner, with blocking
|
|
31
|
+
// decisions to review or something back from the agent (Standing#asks_for_owner?):
|
|
32
|
+
// the one the cursor rests on when it asks, else the first after it, going
|
|
33
|
+
// round to the top of the route. With none left anywhere, the cursor's row
|
|
34
|
+
// says so.
|
|
33
35
|
const openUnreviewed = () => {
|
|
34
36
|
const ahead = [...stops.slice(at), ...stops.slice(0, at)];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
const next = ahead.find((stop) => stop.dataset.asks === "true");
|
|
38
|
+
if (!next)
|
|
39
|
+
return said(
|
|
40
|
+
"Nothing asks for you: every blocking decision is reviewed, and nothing is back from the agent.",
|
|
41
|
+
);
|
|
38
42
|
restOn(stops.indexOf(next));
|
|
39
43
|
location.assign(next.querySelector("a").href);
|
|
40
44
|
};
|
data/public/keys.js
CHANGED
|
@@ -201,6 +201,12 @@ export const thisBlock = (hints = []) => {
|
|
|
201
201
|
export const helping = () =>
|
|
202
202
|
!!document.querySelector("dialog[open], :popover-open");
|
|
203
203
|
|
|
204
|
+
// Whether the focus went over the page rather than along it: into a picture
|
|
205
|
+
// looked at closely, or into Grouse's overlay to write a note on the desk.
|
|
206
|
+
// Either leaves the page where it was, a decision being decided included.
|
|
207
|
+
export const overThePage = (target) =>
|
|
208
|
+
!!target.closest?.("dialog, grouse-widget");
|
|
209
|
+
|
|
204
210
|
// `hints` are pairs of keys and what they do: [["j k", "move"], ["a", "leave as is"]].
|
|
205
211
|
let shown;
|
|
206
212
|
export const mode = (name, hints = []) => {
|
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;
|