@skillcdn/console 0.3.1 → 0.4.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.
- package/README.md +4 -4
- package/dist/api.d.ts +4 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +201 -25
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +15 -11
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +59 -26
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/pack.d.ts +7 -2
- package/dist/cli/pack.d.ts.map +1 -1
- package/dist/cli/pack.js +16 -2
- package/dist/cli/pack.js.map +1 -1
- package/dist/client.d.ts +20 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +17 -3
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +28 -6
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +55 -14
- package/dist/compat.js.map +1 -1
- package/dist/components/board.d.ts +12 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +57 -7
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +17 -7
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +89 -38
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-form.d.ts +12 -4
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +24 -6
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +4 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +9 -6
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/documents.d.ts.map +1 -1
- package/dist/components/documents.js +3 -3
- package/dist/components/documents.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +63 -8
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/my-decisions.d.ts +20 -0
- package/dist/components/my-decisions.d.ts.map +1 -0
- package/dist/components/my-decisions.js +36 -0
- package/dist/components/my-decisions.js.map +1 -0
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +1 -1
- package/dist/components/people.js.map +1 -1
- package/dist/components/picker.d.ts +39 -0
- package/dist/components/picker.d.ts.map +1 -0
- package/dist/components/picker.js +155 -0
- package/dist/components/picker.js.map +1 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -3
- package/dist/components/projects.js.map +1 -1
- package/dist/components/roadmap.js +1 -1
- package/dist/components/roadmap.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +1 -1
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +36 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/task-form.d.ts +18 -4
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +56 -13
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.js +2 -2
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.d.ts +7 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +7 -6
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +17 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +26 -11
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +5 -3
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +1794 -433
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +93 -10
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +21 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +24 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +45 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +52 -10
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +54 -12
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +9 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +12 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +3 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +3 -1
- package/dist/routes.js.map +1 -1
- package/dist/rules.js +1 -1
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +252 -12
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +27 -10
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +2 -0
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +2 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +14 -13
- package/docs/specs/rest.md +24 -19
- package/package.json +2 -2
- package/skills/building-a-console/SKILL.md +4 -4
- package/skills/building-a-console/references/the-default-console.md +4 -4
- package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
- package/skills/deploying-a-console/SKILL.md +2 -2
- package/skills/deploying-a-console/references/resources.md +1 -1
- package/skills/working-the-board/SKILL.md +8 -6
- package/skills/working-the-board/references/terms.md +3 -3
- package/src/api.ts +7 -0
- package/src/cli/cli.ts +254 -25
- package/src/cli/format.ts +22 -11
- package/src/cli/help.ts +63 -26
- package/src/cli/main.ts +1 -0
- package/src/cli/pack.ts +17 -1
- package/src/client.ts +44 -11
- package/src/compat.ts +63 -14
- package/src/components/board.tsx +124 -25
- package/src/components/calendar.tsx +258 -117
- package/src/components/decision-form.tsx +71 -23
- package/src/components/decision-list.tsx +30 -3
- package/src/components/documents.tsx +27 -11
- package/src/components/event-feed.tsx +116 -33
- package/src/components/my-decisions.tsx +138 -0
- package/src/components/people.tsx +3 -1
- package/src/components/picker.tsx +297 -0
- package/src/components/projects.tsx +7 -5
- package/src/components/roadmap.tsx +1 -1
- package/src/components/runs.tsx +5 -1
- package/src/components/shell.tsx +36 -1
- package/src/components/task-form.tsx +149 -63
- package/src/components/task-list.tsx +3 -3
- package/src/components/task-view.tsx +71 -56
- package/src/components/ui.tsx +89 -11
- package/src/components/work.tsx +6 -4
- package/src/console.tsx +157 -29
- package/src/data.ts +61 -1
- package/src/i18n/en.ts +54 -10
- package/src/i18n/ko.ts +56 -12
- package/src/index.ts +19 -1
- package/src/limits.ts +2 -0
- package/src/router.ts +18 -1
- package/src/routes.ts +3 -1
- package/src/rules.ts +1 -1
- package/src/schemas.ts +29 -9
- package/src/styles/console.css +1794 -433
- package/src/vocabulary.ts +3 -0
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { useWords } from "../i18n/index.js";
|
|
13
13
|
import { Markdown } from "./markdown.js";
|
|
14
14
|
import { Pager, pageOf, RECORD_PAGE_SIZE } from "./paged.js";
|
|
15
|
-
import { Badge, Button, cx, PersonChip, Time } from "./ui.js";
|
|
15
|
+
import { Badge, Button, cx, PersonChip, personName, Time } from "./ui.js";
|
|
16
16
|
|
|
17
17
|
// The decisions: the ones that wait, each a card with the way to answer it, since they are what
|
|
18
18
|
// a person came for; and the ones answered, the record, a row each, since they are read back
|
|
@@ -29,6 +29,8 @@ export interface DecisionCardProps {
|
|
|
29
29
|
readonly docHref?: ((path: string) => string) | undefined;
|
|
30
30
|
/** Where the decision's own page is; given, the question leads to it. */
|
|
31
31
|
readonly href?: string | undefined;
|
|
32
|
+
/** The project the decision is in, named where a page shows the decisions of several. */
|
|
33
|
+
readonly project?: React.ReactNode;
|
|
32
34
|
/** Called with the person's answer. Left out, a waiting decision cannot be answered here. */
|
|
33
35
|
readonly onAnswer?: ((decision: RestDecision, input: RestAnswerInput) => void) | undefined;
|
|
34
36
|
/** Called to grow the record, with what followed. Left out, it cannot be written here. */
|
|
@@ -36,7 +38,10 @@ export interface DecisionCardProps {
|
|
|
36
38
|
readonly busy?: boolean | undefined;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Who asked, and when: the person, through their agent when a run asked; and whom the decision
|
|
43
|
+
* is for, where that is someone else (ADR-0042).
|
|
44
|
+
*/
|
|
40
45
|
function Asked(props: { readonly decision: RestDecision; readonly day?: boolean }) {
|
|
41
46
|
const words = useWords();
|
|
42
47
|
const { decision } = props;
|
|
@@ -47,6 +52,20 @@ function Asked(props: { readonly decision: RestDecision; readonly day?: boolean
|
|
|
47
52
|
? words.decisions.asked
|
|
48
53
|
: words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
|
|
49
54
|
<Time iso={decision.createdAt} precision={props.day === true ? "day" : "minute"} />
|
|
55
|
+
{decision.decider.id !== decision.raisedBy.id && (
|
|
56
|
+
<span className="sc-decision-for">
|
|
57
|
+
<span className="sc-visually-hidden">
|
|
58
|
+
{words.decisions.peopleLabel(
|
|
59
|
+
personName(decision.raisedBy),
|
|
60
|
+
personName(decision.decider),
|
|
61
|
+
)}
|
|
62
|
+
</span>
|
|
63
|
+
<span className="sc-decision-arrow" aria-hidden="true">
|
|
64
|
+
→
|
|
65
|
+
</span>{" "}
|
|
66
|
+
<PersonChip person={decision.decider} />
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
50
69
|
</>
|
|
51
70
|
);
|
|
52
71
|
}
|
|
@@ -264,6 +283,7 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
264
283
|
)}
|
|
265
284
|
</>
|
|
266
285
|
)}
|
|
286
|
+
{props.project}
|
|
267
287
|
</p>
|
|
268
288
|
</header>
|
|
269
289
|
{decision.body.length > 0 && (
|
|
@@ -288,7 +308,9 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
288
308
|
</p>
|
|
289
309
|
</div>
|
|
290
310
|
) : props.onAnswer === undefined ? (
|
|
291
|
-
<p className="sc-decision-waiting">
|
|
311
|
+
<p className="sc-decision-waiting">
|
|
312
|
+
{words.decisions.waitingFor(personName(decision.decider))}
|
|
313
|
+
</p>
|
|
292
314
|
) : (
|
|
293
315
|
<AnswerForm decision={decision} onAnswer={props.onAnswer} busy={props.busy} />
|
|
294
316
|
)}
|
|
@@ -334,6 +356,8 @@ export interface DecisionLogProps {
|
|
|
334
356
|
readonly page?: number | undefined;
|
|
335
357
|
/** Where each page of the record is, so that a page has an address; left out, the pages turn in place. */
|
|
336
358
|
readonly pageHref?: ((page: number) => string) | undefined;
|
|
359
|
+
/** The project a decision is in, named where the record holds the decisions of several. */
|
|
360
|
+
readonly projectOf?: ((decision: RestDecision) => React.ReactNode) | undefined;
|
|
337
361
|
}
|
|
338
362
|
|
|
339
363
|
/** One row of the record: what was asked, by whom and when; what was answered, with the word given, by whom and when. */
|
|
@@ -343,6 +367,7 @@ function RecordRow(props: {
|
|
|
343
367
|
readonly taskHref: ((task: RestTask) => string) | undefined;
|
|
344
368
|
readonly href: string | undefined;
|
|
345
369
|
readonly docHref: ((path: string) => string) | undefined;
|
|
370
|
+
readonly project?: React.ReactNode;
|
|
346
371
|
}) {
|
|
347
372
|
const words = useWords();
|
|
348
373
|
const { decision, task } = props;
|
|
@@ -367,6 +392,7 @@ function RecordRow(props: {
|
|
|
367
392
|
</p>
|
|
368
393
|
<p className="sc-decision-record-meta">
|
|
369
394
|
<Asked decision={decision} day />
|
|
395
|
+
{props.project}
|
|
370
396
|
</p>
|
|
371
397
|
</div>
|
|
372
398
|
{answer !== null && (
|
|
@@ -412,6 +438,7 @@ export function DecisionLog(props: DecisionLogProps) {
|
|
|
412
438
|
taskHref={props.taskHref}
|
|
413
439
|
href={props.decisionHref?.(decision)}
|
|
414
440
|
docHref={props.docHref}
|
|
441
|
+
project={props.projectOf?.(decision)}
|
|
415
442
|
/>
|
|
416
443
|
))}
|
|
417
444
|
</ol>
|
|
@@ -440,12 +440,21 @@ export function DocumentFiles(props: DocumentFilesProps) {
|
|
|
440
440
|
}
|
|
441
441
|
}}
|
|
442
442
|
>
|
|
443
|
-
<
|
|
444
|
-
className="sc-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
443
|
+
<div className="sc-doc-attach-file">
|
|
444
|
+
<label className="sc-button sc-button-secondary sc-file-choice">
|
|
445
|
+
{words.chooseFile}
|
|
446
|
+
<input
|
|
447
|
+
key={chosen === undefined ? "none" : "some"}
|
|
448
|
+
className="sc-file-input"
|
|
449
|
+
type="file"
|
|
450
|
+
aria-label={words.fileToAttach}
|
|
451
|
+
onChange={(event) => setChosen(event.target.files?.[0] ?? undefined)}
|
|
452
|
+
/>
|
|
453
|
+
</label>
|
|
454
|
+
<span className={cx("sc-doc-attach-chosen", chosen === undefined && "sc-muted")}>
|
|
455
|
+
{chosen === undefined ? words.noFileChosen : chosen.name}
|
|
456
|
+
</span>
|
|
457
|
+
</div>
|
|
449
458
|
<input
|
|
450
459
|
className="sc-input"
|
|
451
460
|
placeholder={words.labelPlaceholder}
|
|
@@ -453,7 +462,11 @@ export function DocumentFiles(props: DocumentFilesProps) {
|
|
|
453
462
|
value={label}
|
|
454
463
|
onChange={(event) => setLabel(event.target.value)}
|
|
455
464
|
/>
|
|
456
|
-
<Button
|
|
465
|
+
<Button
|
|
466
|
+
type="submit"
|
|
467
|
+
variant="primary"
|
|
468
|
+
disabled={chosen === undefined || props.busy === true}
|
|
469
|
+
>
|
|
457
470
|
{words.attach}
|
|
458
471
|
</Button>
|
|
459
472
|
</form>
|
|
@@ -555,7 +568,7 @@ export function DocumentView(props: DocumentViewProps) {
|
|
|
555
568
|
</dd>
|
|
556
569
|
</div>
|
|
557
570
|
<div>
|
|
558
|
-
<dt>{docs.
|
|
571
|
+
<dt>{docs.author}</dt>
|
|
559
572
|
<dd>
|
|
560
573
|
<Writer by={shown.author} agent={shown.agent} /> <Time iso={shown.createdAt} />
|
|
561
574
|
</dd>
|
|
@@ -641,9 +654,12 @@ export function DocumentView(props: DocumentViewProps) {
|
|
|
641
654
|
>
|
|
642
655
|
{docs.version(entry.number)}
|
|
643
656
|
</a>
|
|
644
|
-
)}
|
|
645
|
-
|
|
646
|
-
<
|
|
657
|
+
)}
|
|
658
|
+
<span className="sc-version-title">{entry.title}</span>
|
|
659
|
+
<span className="sc-version-meta">
|
|
660
|
+
<Writer by={entry.author} agent={entry.agent} />{" "}
|
|
661
|
+
<Time iso={entry.createdAt} />
|
|
662
|
+
</span>
|
|
647
663
|
</li>
|
|
648
664
|
))}
|
|
649
665
|
</ol>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { agentLabel, type RestEvent } from "../api.js";
|
|
3
|
+
import { dayOf } from "../dates.js";
|
|
3
4
|
import { en, type Messages } from "../i18n/en.js";
|
|
4
|
-
import { useWords } from "../i18n/index.js";
|
|
5
|
+
import { useLanguage, useWords } from "../i18n/index.js";
|
|
5
6
|
import { ShowMore, usePaged } from "./paged.js";
|
|
6
|
-
import {
|
|
7
|
+
import { formatInstant, PersonChip } from "./ui.js";
|
|
7
8
|
|
|
8
9
|
// The feed: one line per event, newest first, as a sentence a person reads at a glance: who,
|
|
9
10
|
// as which agent when through one, and what. It takes its data as props and nothing from the
|
|
@@ -103,7 +104,9 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
|
103
104
|
? feed.event.runFinished(task)
|
|
104
105
|
: data.status === "failed"
|
|
105
106
|
? feed.event.runFailed(task)
|
|
106
|
-
:
|
|
107
|
+
: data.reason === "unheard"
|
|
108
|
+
? feed.event.runUnheard(task, data.agent ?? feed.someone)
|
|
109
|
+
: feed.event.runAbandoned(task);
|
|
107
110
|
case "document.written":
|
|
108
111
|
return feed.event.documentWritten(page, data.version);
|
|
109
112
|
case "document.archived":
|
|
@@ -123,8 +126,65 @@ export interface EventFeedProps {
|
|
|
123
126
|
readonly empty?: ReactNode;
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
/** The day an event fell on, by this machine's clock, as the calendar spells a day. */
|
|
130
|
+
function dayOfEvent(event: RestEvent): string {
|
|
131
|
+
const date = new Date(event.createdAt);
|
|
132
|
+
return Number.isNaN(date.getTime()) ? event.createdAt : dayOf(date);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A day as the feed heads it: the weekday and the date, with the year where it is not this one. */
|
|
136
|
+
function formatFeedDay(day: string, language: string, now: Date = new Date()): string {
|
|
137
|
+
const date = new Date(`${day}T00:00:00`);
|
|
138
|
+
if (Number.isNaN(date.getTime())) {
|
|
139
|
+
return day;
|
|
140
|
+
}
|
|
141
|
+
return date.toLocaleDateString(language, {
|
|
142
|
+
weekday: "short",
|
|
143
|
+
month: "long",
|
|
144
|
+
day: "numeric",
|
|
145
|
+
...(date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Whether two events were done by the same person as the same agent, so the name is said once. */
|
|
150
|
+
function sameHands(a: RestEvent, b: RestEvent): boolean {
|
|
151
|
+
return (a.actor?.id ?? null) === (b.actor?.id ?? null) && a.agent === b.agent;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The hour of an event, for a row whose day is in the heading above it. */
|
|
155
|
+
function formatClock(iso: string, language: string): string {
|
|
156
|
+
const date = new Date(iso);
|
|
157
|
+
return Number.isNaN(date.getTime())
|
|
158
|
+
? iso
|
|
159
|
+
: date.toLocaleTimeString(language, { hour: "2-digit", minute: "2-digit", hourCycle: "h23" });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type FeedRun = { readonly actor: RestEvent; readonly events: RestEvent[] };
|
|
163
|
+
type FeedDay = { readonly day: string; readonly runs: FeedRun[] };
|
|
164
|
+
|
|
165
|
+
/** The events by the day they fell on, and within a day by runs of the same hands; newest first. */
|
|
166
|
+
function groupFeed(events: readonly RestEvent[]): FeedDay[] {
|
|
167
|
+
const days: FeedDay[] = [];
|
|
168
|
+
for (const event of events) {
|
|
169
|
+
const day = dayOfEvent(event);
|
|
170
|
+
let last = days[days.length - 1];
|
|
171
|
+
if (last === undefined || last.day !== day) {
|
|
172
|
+
last = { day, runs: [] };
|
|
173
|
+
days.push(last);
|
|
174
|
+
}
|
|
175
|
+
const run = last.runs[last.runs.length - 1];
|
|
176
|
+
if (run !== undefined && sameHands(run.actor, event)) {
|
|
177
|
+
run.events.push(event);
|
|
178
|
+
} else {
|
|
179
|
+
last.runs.push({ actor: event, events: [event] });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return days;
|
|
183
|
+
}
|
|
184
|
+
|
|
126
185
|
export function EventFeed(props: EventFeedProps) {
|
|
127
186
|
const words = useWords();
|
|
187
|
+
const { tag } = useLanguage();
|
|
128
188
|
const newestFirst = [...props.events].reverse();
|
|
129
189
|
const { shown, rest, more } = usePaged(newestFirst);
|
|
130
190
|
if (props.events.length === 0) {
|
|
@@ -132,37 +192,60 @@ export function EventFeed(props: EventFeedProps) {
|
|
|
132
192
|
}
|
|
133
193
|
return (
|
|
134
194
|
<>
|
|
135
|
-
<
|
|
136
|
-
{shown.map((
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
195
|
+
<section className="sc-feed" aria-label={words.feed.list}>
|
|
196
|
+
{groupFeed(shown).map((group) => (
|
|
197
|
+
<section key={group.day} className="sc-feed-day">
|
|
198
|
+
<h3 className="sc-feed-day-title">
|
|
199
|
+
<time dateTime={group.day}>{formatFeedDay(group.day, tag)}</time>
|
|
200
|
+
</h3>
|
|
201
|
+
<ol className="sc-feed-day-list">
|
|
202
|
+
{group.runs.map((run) => (
|
|
203
|
+
<li key={run.actor.id} className="sc-feed-run">
|
|
204
|
+
<span className="sc-feed-actor">
|
|
205
|
+
{run.actor.actor === null ? (
|
|
206
|
+
<span className="sc-person-login">{words.feed.console}</span>
|
|
207
|
+
) : (
|
|
208
|
+
<PersonChip person={run.actor.actor} />
|
|
209
|
+
)}
|
|
210
|
+
{run.actor.agent !== null && (
|
|
211
|
+
<span
|
|
212
|
+
className="sc-feed-agent"
|
|
213
|
+
title={
|
|
214
|
+
agentLabel(run.actor.agent) === run.actor.agent
|
|
215
|
+
? words.feed.asTitle
|
|
216
|
+
: run.actor.agent
|
|
217
|
+
}
|
|
218
|
+
>
|
|
219
|
+
{words.feed.as(agentLabel(run.actor.agent))}
|
|
220
|
+
</span>
|
|
221
|
+
)}
|
|
155
222
|
</span>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
223
|
+
<ol className="sc-feed-run-list">
|
|
224
|
+
{run.events.map((event) => {
|
|
225
|
+
const href = props.href?.(event);
|
|
226
|
+
const said = describeEvent(event, words);
|
|
227
|
+
return (
|
|
228
|
+
<li key={event.id} className="sc-feed-item">
|
|
229
|
+
<time
|
|
230
|
+
className="sc-time sc-feed-clock"
|
|
231
|
+
dateTime={event.createdAt}
|
|
232
|
+
title={formatInstant(event.createdAt, tag)}
|
|
233
|
+
>
|
|
234
|
+
{formatClock(event.createdAt, tag)}
|
|
235
|
+
</time>
|
|
236
|
+
<span className="sc-feed-words">
|
|
237
|
+
{href === undefined ? said : <a href={href}>{said}</a>}
|
|
238
|
+
</span>
|
|
239
|
+
</li>
|
|
240
|
+
);
|
|
241
|
+
})}
|
|
242
|
+
</ol>
|
|
243
|
+
</li>
|
|
244
|
+
))}
|
|
245
|
+
</ol>
|
|
246
|
+
</section>
|
|
247
|
+
))}
|
|
248
|
+
</section>
|
|
166
249
|
<ShowMore rest={rest} onMore={more} />
|
|
167
250
|
</>
|
|
168
251
|
);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { RestAnswerInput, RestDecision, RestMyDecisions } from "../api.js";
|
|
3
|
+
import { useWords } from "../i18n/index.js";
|
|
4
|
+
import { DecisionCard, DecisionLog } from "./decision-list.js";
|
|
5
|
+
import { ProjectMark } from "./projects.js";
|
|
6
|
+
import { Button } from "./ui.js";
|
|
7
|
+
|
|
8
|
+
// A person's decisions (ADR-0042), across the projects they may see, laid out as a project's
|
|
9
|
+
// decisions page is: in the first column the decisions that wait for their answer, as cards
|
|
10
|
+
// answered here, and under them the decisions they asked of someone else, as cards that say
|
|
11
|
+
// whom they wait for; in the second the record, the answered ones of both kinds, newest first.
|
|
12
|
+
// Every card and row names the project, and who asked and who answers, as the cards do
|
|
13
|
+
// everywhere. Takes its data as props and nothing from the network; the page reads more when
|
|
14
|
+
// asked.
|
|
15
|
+
|
|
16
|
+
/** One kind of a person's decisions, a page at a time. */
|
|
17
|
+
export interface DecisionsPage {
|
|
18
|
+
readonly items: RestMyDecisions["items"];
|
|
19
|
+
/** Whether there is more after what is shown; `onMore` reads it. */
|
|
20
|
+
readonly more?: boolean | undefined;
|
|
21
|
+
readonly onMore?: (() => void) | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MyDecisionsProps {
|
|
25
|
+
/** The decisions addressed to the person: waiting and answered. */
|
|
26
|
+
readonly addressed: DecisionsPage;
|
|
27
|
+
/** The decisions the person asked of someone else: waiting and answered. */
|
|
28
|
+
readonly raised: DecisionsPage;
|
|
29
|
+
readonly decisionHref: (projectKey: string, decisionId: string) => string;
|
|
30
|
+
/** Called with the person's answer on a decision of theirs, in its project. Left out, a waiting decision is answered on its own page. */
|
|
31
|
+
readonly onAnswer?:
|
|
32
|
+
| ((projectKey: string, decision: RestDecision, input: RestAnswerInput) => void)
|
|
33
|
+
| undefined;
|
|
34
|
+
readonly busy?: boolean | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Item = RestMyDecisions["items"][number];
|
|
38
|
+
|
|
39
|
+
/** The project a decision is in, as the plate names one. */
|
|
40
|
+
function InProject(props: { readonly project: Item["project"] }) {
|
|
41
|
+
const words = useWords();
|
|
42
|
+
return (
|
|
43
|
+
<span className="sc-task-row-project">
|
|
44
|
+
<ProjectMark project={props.project} size="sm" /> {words.work.inProject(props.project.name)}
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function More(props: {
|
|
50
|
+
readonly page: DecisionsPage;
|
|
51
|
+
readonly label: string;
|
|
52
|
+
readonly busy?: boolean | undefined;
|
|
53
|
+
}) {
|
|
54
|
+
const { page } = props;
|
|
55
|
+
if (page.more !== true || page.onMore === undefined) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<div className="sc-paged-more">
|
|
60
|
+
<Button variant="secondary" size="sm" disabled={props.busy} onClick={page.onMore}>
|
|
61
|
+
{props.label}
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function MyDecisions(props: MyDecisionsProps) {
|
|
68
|
+
const words = useWords();
|
|
69
|
+
const own = words.myDecisions;
|
|
70
|
+
const { onAnswer } = props;
|
|
71
|
+
const waiting = props.addressed.items.filter(({ decision }) => decision.answer === null);
|
|
72
|
+
const asked = props.raised.items.filter(({ decision }) => decision.answer === null);
|
|
73
|
+
const record = [...props.addressed.items, ...props.raised.items]
|
|
74
|
+
.filter(({ decision }) => decision.answer !== null)
|
|
75
|
+
.sort((a, b) => (b.decision.answer?.at ?? "").localeCompare(a.decision.answer?.at ?? ""));
|
|
76
|
+
const projectOf = new Map(record.map((item) => [item.decision.id, item.project]));
|
|
77
|
+
const card = (item: Item, answerable: boolean): ReactNode => (
|
|
78
|
+
<DecisionCard
|
|
79
|
+
key={item.decision.id}
|
|
80
|
+
decision={item.decision}
|
|
81
|
+
href={props.decisionHref(item.project.key, item.decision.id)}
|
|
82
|
+
project={<InProject project={item.project} />}
|
|
83
|
+
onAnswer={
|
|
84
|
+
answerable && onAnswer !== undefined
|
|
85
|
+
? (decision, input) => onAnswer(item.project.key, decision, input)
|
|
86
|
+
: undefined
|
|
87
|
+
}
|
|
88
|
+
busy={props.busy}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
return (
|
|
92
|
+
<div className="sc-decisions">
|
|
93
|
+
<div className="sc-decisions-waiting">
|
|
94
|
+
<section aria-label={own.waiting}>
|
|
95
|
+
<h2 className="sc-section-title">
|
|
96
|
+
{own.waiting} <span className="sc-column-count">{waiting.length}</span>
|
|
97
|
+
</h2>
|
|
98
|
+
{waiting.length === 0 ? (
|
|
99
|
+
<p className="sc-muted">{own.noneWaiting}</p>
|
|
100
|
+
) : (
|
|
101
|
+
waiting.map((item) => card(item, true))
|
|
102
|
+
)}
|
|
103
|
+
<More page={props.addressed} label={own.more} busy={props.busy} />
|
|
104
|
+
</section>
|
|
105
|
+
<section aria-label={own.raised}>
|
|
106
|
+
<h2 className="sc-section-title">
|
|
107
|
+
{own.raised} <span className="sc-column-count">{asked.length}</span>
|
|
108
|
+
</h2>
|
|
109
|
+
{asked.length === 0 ? (
|
|
110
|
+
<p className="sc-muted">{own.noneRaised}</p>
|
|
111
|
+
) : (
|
|
112
|
+
asked.map((item) => card(item, false))
|
|
113
|
+
)}
|
|
114
|
+
<More page={props.raised} label={own.more} busy={props.busy} />
|
|
115
|
+
</section>
|
|
116
|
+
</div>
|
|
117
|
+
<section aria-label={words.decisions.record}>
|
|
118
|
+
<h2 className="sc-section-title">
|
|
119
|
+
{words.decisions.record} <span className="sc-column-count">{record.length}</span>
|
|
120
|
+
</h2>
|
|
121
|
+
{record.length === 0 ? (
|
|
122
|
+
<p className="sc-muted">{own.noneAnswered}</p>
|
|
123
|
+
) : (
|
|
124
|
+
<DecisionLog
|
|
125
|
+
decisions={record.map((item) => item.decision)}
|
|
126
|
+
decisionHref={(decision) =>
|
|
127
|
+
props.decisionHref(projectOf.get(decision.id)?.key ?? "", decision.id)
|
|
128
|
+
}
|
|
129
|
+
projectOf={(decision) => {
|
|
130
|
+
const project = projectOf.get(decision.id);
|
|
131
|
+
return project === undefined ? undefined : <InProject project={project} />;
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
)}
|
|
135
|
+
</section>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -75,9 +75,11 @@ export function PeopleList(props: PeopleListProps) {
|
|
|
75
75
|
{active.map((person) => (
|
|
76
76
|
<li key={person.id} className="sc-person-row">
|
|
77
77
|
<PersonChip person={person} size="md" />
|
|
78
|
+
{props.me?.id === person.id && (
|
|
79
|
+
<span className="sc-person-you">{words.common.you}</span>
|
|
80
|
+
)}
|
|
78
81
|
<span className="sc-person-name sc-muted">
|
|
79
82
|
{person.name === null ? "" : person.login}
|
|
80
|
-
{props.me?.id === person.id && <span> {words.common.you}</span>}
|
|
81
83
|
<span className="sc-person-note"> {people.source[person.source]}</span>
|
|
82
84
|
</span>
|
|
83
85
|
{props.onChangeRole === undefined ? (
|