@skillcdn/console 0.1.10 → 0.1.12
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 +6 -5
- package/dist/api.d.ts +6 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +49 -9
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +2 -0
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +7 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +10 -9
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +14 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -1
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +1 -4
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +6 -13
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +21 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +69 -9
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +11 -9
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +11 -7
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/paged.d.ts +14 -0
- package/dist/components/paged.d.ts.map +1 -0
- package/dist/components/paged.js +28 -0
- package/dist/components/paged.js.map +1 -0
- package/dist/components/people.js +2 -2
- package/dist/components/people.js.map +1 -1
- package/dist/components/projects.d.ts +22 -2
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +37 -9
- package/dist/components/projects.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +3 -3
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts +2 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +3 -3
- package/dist/components/shell.js.map +1 -1
- package/dist/components/task-form.js +2 -2
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.d.ts +1 -1
- package/dist/components/task-list.d.ts.map +1 -1
- package/dist/components/task-list.js +6 -4
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.js +1 -1
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +7 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +9 -4
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +7 -3
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +149 -2
- package/dist/console.d.ts +38 -4
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +84 -23
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +5 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +15 -2
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +17 -11
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +18 -14
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +18 -14
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/marks.d.ts +12 -0
- package/dist/marks.d.ts.map +1 -0
- package/dist/marks.js +31 -0
- package/dist/marks.js.map +1 -0
- package/dist/router.d.ts +19 -3
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +28 -5
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +7 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +8 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +123 -3
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +42 -10
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +10 -4
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +8 -3
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +7 -5
- package/docs/specs/rest.md +18 -12
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +2 -2
- package/skills/working-the-board/SKILL.md +1 -1
- package/src/api.ts +12 -0
- package/src/cli/cli.ts +51 -8
- package/src/cli/format.ts +8 -6
- package/src/cli/help.ts +10 -10
- package/src/client.ts +29 -6
- package/src/components/board.tsx +6 -30
- package/src/components/calendar.tsx +168 -56
- package/src/components/decision-list.tsx +38 -33
- package/src/components/event-feed.tsx +34 -27
- package/src/components/paged.tsx +39 -0
- package/src/components/people.tsx +5 -5
- package/src/components/projects.tsx +132 -12
- package/src/components/runs.tsx +9 -3
- package/src/components/shell.tsx +8 -5
- package/src/components/task-form.tsx +2 -2
- package/src/components/task-list.tsx +35 -32
- package/src/components/task-view.tsx +1 -1
- package/src/components/ui.tsx +14 -5
- package/src/components/work.tsx +12 -2
- package/src/console.tsx +158 -25
- package/src/data.ts +20 -4
- package/src/i18n/en.ts +19 -14
- package/src/i18n/ko.ts +19 -14
- package/src/index.ts +9 -0
- package/src/limits.ts +6 -0
- package/src/marks.ts +40 -0
- package/src/router.ts +45 -7
- package/src/routes.ts +9 -0
- package/src/schemas.ts +48 -8
- package/src/styles/console.css +149 -2
- package/src/vocabulary.ts +14 -4
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import { type FormEvent, type ReactNode, useState } from "react";
|
|
1
|
+
import { type FormEvent, type ReactNode, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
COLOR_NAMES,
|
|
4
|
+
type ColorName,
|
|
3
5
|
MAX_PROJECT_DESCRIPTION_LENGTH,
|
|
4
6
|
MAX_PROJECT_KEY_LENGTH,
|
|
5
7
|
MAX_PROJECT_NAME_LENGTH,
|
|
8
|
+
MAX_PROJECT_SYMBOL_LENGTH,
|
|
6
9
|
MAX_SKILLS_ADDRESS_LENGTH,
|
|
10
|
+
PROJECT_ICON_TYPES,
|
|
7
11
|
PROJECT_ROLES,
|
|
8
12
|
PROJECT_VISIBILITIES,
|
|
9
13
|
type ProjectRole,
|
|
10
14
|
type ProjectVisibility,
|
|
15
|
+
projectColorOf,
|
|
16
|
+
projectIconPath,
|
|
17
|
+
projectSymbolOf,
|
|
11
18
|
type RestMember,
|
|
12
19
|
type RestMemberInput,
|
|
13
20
|
type RestPerson,
|
|
@@ -16,7 +23,44 @@ import {
|
|
|
16
23
|
type RestProjectPatch,
|
|
17
24
|
} from "../api.js";
|
|
18
25
|
import { useWords } from "../i18n/index.js";
|
|
19
|
-
import { Badge, Button, PersonChip, Time } from "./ui.js";
|
|
26
|
+
import { Badge, Button, cx, PersonChip, personName, Time } from "./ui.js";
|
|
27
|
+
|
|
28
|
+
/** A project's mark: its symbol on its colour, so that projects tell apart at a glance. */
|
|
29
|
+
export function ProjectMark(props: {
|
|
30
|
+
readonly project: {
|
|
31
|
+
readonly key: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly color: ColorName | null;
|
|
34
|
+
readonly symbol: string | null;
|
|
35
|
+
/** The project's own image, when it has one; the symbol on the colour otherwise. */
|
|
36
|
+
readonly icon?: { readonly sha256: string } | null | undefined;
|
|
37
|
+
};
|
|
38
|
+
readonly size?: "sm" | "md" | "lg";
|
|
39
|
+
/** Where the image is read from; the console's own path for it when left out. */
|
|
40
|
+
readonly iconSrc?: string | undefined;
|
|
41
|
+
}) {
|
|
42
|
+
const size = props.size ?? "md";
|
|
43
|
+
const icon = props.project.icon ?? null;
|
|
44
|
+
const src =
|
|
45
|
+
props.iconSrc ?? (icon === null ? undefined : projectIconPath(props.project.key, icon));
|
|
46
|
+
return (
|
|
47
|
+
<span
|
|
48
|
+
className={cx(
|
|
49
|
+
"sc-mark",
|
|
50
|
+
`sc-mark-${size}`,
|
|
51
|
+
`sc-category-${projectColorOf(props.project)}`,
|
|
52
|
+
src !== undefined && "sc-mark-pictured",
|
|
53
|
+
)}
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
>
|
|
56
|
+
{src === undefined ? (
|
|
57
|
+
projectSymbolOf(props.project)
|
|
58
|
+
) : (
|
|
59
|
+
<img className="sc-mark-image" src={src} alt="" />
|
|
60
|
+
)}
|
|
61
|
+
</span>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
20
64
|
|
|
21
65
|
// The projects of the workspace (ADR-0008): the list a person may see, the way to make one,
|
|
22
66
|
// a project's settings for its owners, and those listed in it. Each takes its data as props
|
|
@@ -60,6 +104,7 @@ export function ProjectList(props: ProjectListProps) {
|
|
|
60
104
|
{props.projects.map((project) => (
|
|
61
105
|
<li key={project.id} className="sc-project">
|
|
62
106
|
<div className="sc-project-head">
|
|
107
|
+
<ProjectMark project={project} size="md" />
|
|
63
108
|
<a
|
|
64
109
|
className="sc-project-name"
|
|
65
110
|
href={props.projectHref(project)}
|
|
@@ -79,14 +124,11 @@ export function ProjectList(props: ProjectListProps) {
|
|
|
79
124
|
{project.description.length > 0 && (
|
|
80
125
|
<p className="sc-project-description">{project.description}</p>
|
|
81
126
|
)}
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
{project.openDecisions > 0 && (
|
|
128
|
+
<p className="sc-project-meta">
|
|
84
129
|
<Badge tone="warning">{words.decisionsWaiting(project.openDecisions)}</Badge>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
<Badge tone="accent">{words.agentsAtWork(project.openRuns)}</Badge>
|
|
88
|
-
)}
|
|
89
|
-
</p>
|
|
130
|
+
</p>
|
|
131
|
+
)}
|
|
90
132
|
</li>
|
|
91
133
|
))}
|
|
92
134
|
</ul>
|
|
@@ -98,7 +140,11 @@ export interface ProjectFormProps {
|
|
|
98
140
|
readonly project?: RestProject | undefined;
|
|
99
141
|
readonly busy?: boolean | undefined;
|
|
100
142
|
readonly error?: string | undefined;
|
|
101
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Called with the settings as they are to be, and the image for the mark: a file chosen,
|
|
145
|
+
* `null` to take the image away, or nothing to keep what the project has.
|
|
146
|
+
*/
|
|
147
|
+
readonly onSubmit: (input: RestProjectInput | RestProjectPatch, icon?: File | null) => void;
|
|
102
148
|
readonly onCancel?: (() => void) | undefined;
|
|
103
149
|
}
|
|
104
150
|
|
|
@@ -121,6 +167,31 @@ export function ProjectForm(props: ProjectFormProps) {
|
|
|
121
167
|
const [description, setDescription] = useState(project?.description ?? "");
|
|
122
168
|
const [visibility, setVisibility] = useState<ProjectVisibility>(project?.visibility ?? "private");
|
|
123
169
|
const [skillsAddress, setSkillsAddress] = useState(project?.skillsAddress ?? "");
|
|
170
|
+
const [color, setColor] = useState<ColorName | "">(project?.color ?? "");
|
|
171
|
+
const [symbol, setSymbol] = useState(project?.symbol ?? "");
|
|
172
|
+
/** The image for the mark: a file chosen, `null` for none any more, nothing to keep the project's. */
|
|
173
|
+
const [icon, setIcon] = useState<File | null | undefined>(undefined);
|
|
174
|
+
const chosenSrc = useMemo(
|
|
175
|
+
() => (icon instanceof File ? URL.createObjectURL(icon) : undefined),
|
|
176
|
+
[icon],
|
|
177
|
+
);
|
|
178
|
+
useEffect(
|
|
179
|
+
() => () => {
|
|
180
|
+
if (chosenSrc !== undefined) {
|
|
181
|
+
URL.revokeObjectURL(chosenSrc);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
[chosenSrc],
|
|
185
|
+
);
|
|
186
|
+
const kept = icon === undefined ? (project?.icon ?? null) : null;
|
|
187
|
+
/** The mark as it will be, from what is typed and chosen so far. */
|
|
188
|
+
const preview = {
|
|
189
|
+
key: key.trim() === "" ? keyOf(name) : key.trim(),
|
|
190
|
+
name: name.trim() === "" ? "?" : name.trim(),
|
|
191
|
+
color: color === "" ? null : color,
|
|
192
|
+
symbol: symbol.trim() === "" ? null : symbol.trim(),
|
|
193
|
+
icon: icon instanceof File ? { sha256: "chosen" } : kept,
|
|
194
|
+
};
|
|
124
195
|
|
|
125
196
|
const submit = (event: FormEvent) => {
|
|
126
197
|
event.preventDefault();
|
|
@@ -129,9 +200,12 @@ export function ProjectForm(props: ProjectFormProps) {
|
|
|
129
200
|
description: description.trim(),
|
|
130
201
|
visibility,
|
|
131
202
|
skillsAddress: skillsAddress.trim() === "" ? null : skillsAddress.trim(),
|
|
203
|
+
color: preview.color,
|
|
204
|
+
symbol: preview.symbol,
|
|
132
205
|
};
|
|
133
206
|
props.onSubmit(
|
|
134
207
|
project === undefined ? { key: key.trim(), ...settings, name: name.trim() } : settings,
|
|
208
|
+
icon,
|
|
135
209
|
);
|
|
136
210
|
};
|
|
137
211
|
|
|
@@ -207,6 +281,50 @@ export function ProjectForm(props: ProjectFormProps) {
|
|
|
207
281
|
/>
|
|
208
282
|
</label>
|
|
209
283
|
</div>
|
|
284
|
+
<div className="sc-field-row sc-mark-row">
|
|
285
|
+
<ProjectMark project={preview} size="lg" iconSrc={chosenSrc} />
|
|
286
|
+
<label className="sc-field">
|
|
287
|
+
<span className="sc-field-label">{form.symbol}</span>
|
|
288
|
+
<input
|
|
289
|
+
className="sc-input"
|
|
290
|
+
value={symbol}
|
|
291
|
+
maxLength={MAX_PROJECT_SYMBOL_LENGTH}
|
|
292
|
+
pattern="\S*"
|
|
293
|
+
placeholder={projectSymbolOf({ name: preview.name, symbol: null })}
|
|
294
|
+
onChange={(event) => setSymbol(event.target.value)}
|
|
295
|
+
/>
|
|
296
|
+
</label>
|
|
297
|
+
<label className="sc-field">
|
|
298
|
+
<span className="sc-field-label">{form.color}</span>
|
|
299
|
+
<select
|
|
300
|
+
className="sc-input"
|
|
301
|
+
value={color}
|
|
302
|
+
onChange={(event) => setColor(event.target.value as ColorName | "")}
|
|
303
|
+
>
|
|
304
|
+
<option value="">{form.colorAuto}</option>
|
|
305
|
+
{COLOR_NAMES.map((candidate) => (
|
|
306
|
+
<option key={candidate} value={candidate}>
|
|
307
|
+
{words.vocabulary.categoryColor[candidate]}
|
|
308
|
+
</option>
|
|
309
|
+
))}
|
|
310
|
+
</select>
|
|
311
|
+
</label>
|
|
312
|
+
<label className="sc-field">
|
|
313
|
+
<span className="sc-field-label">{form.image}</span>
|
|
314
|
+
<input
|
|
315
|
+
key={icon === null ? "none" : "some"}
|
|
316
|
+
className="sc-input sc-input-file"
|
|
317
|
+
type="file"
|
|
318
|
+
accept={PROJECT_ICON_TYPES.join(",")}
|
|
319
|
+
onChange={(event) => setIcon(event.target.files?.[0] ?? undefined)}
|
|
320
|
+
/>
|
|
321
|
+
</label>
|
|
322
|
+
{(icon instanceof File || kept !== null) && (
|
|
323
|
+
<Button variant="ghost" size="sm" onClick={() => setIcon(null)}>
|
|
324
|
+
{form.imageRemove}
|
|
325
|
+
</Button>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
210
328
|
{props.error !== undefined && (
|
|
211
329
|
<p className="sc-form-error" role="alert">
|
|
212
330
|
{props.error}
|
|
@@ -274,7 +392,9 @@ export function MemberList(props: MemberListProps) {
|
|
|
274
392
|
<ProjectRoleBadge role={member.role} />
|
|
275
393
|
) : (
|
|
276
394
|
<label className="sc-field sc-field-inline">
|
|
277
|
-
<span className="sc-visually-hidden">
|
|
395
|
+
<span className="sc-visually-hidden">
|
|
396
|
+
{members.roleOf(personName(member.person))}
|
|
397
|
+
</span>
|
|
278
398
|
<select
|
|
279
399
|
className="sc-input"
|
|
280
400
|
value={member.role}
|
|
@@ -328,7 +448,7 @@ export function MemberList(props: MemberListProps) {
|
|
|
328
448
|
<option value="">{members.chooseSomeone}</option>
|
|
329
449
|
{candidates.map((person) => (
|
|
330
450
|
<option key={person.id} value={person.id}>
|
|
331
|
-
{person
|
|
451
|
+
{personName(person)}
|
|
332
452
|
</option>
|
|
333
453
|
))}
|
|
334
454
|
</select>
|
package/src/components/runs.tsx
CHANGED
|
@@ -2,14 +2,14 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import type { RestArtifact, RestRun, RunStatus } from "../api.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
4
|
import { Markdown } from "./markdown.js";
|
|
5
|
-
import { Badge, Button, formatBytes, PersonChip, Time } from "./ui.js";
|
|
5
|
+
import { Badge, Button, formatBytes, PersonChip, personName, Time } from "./ui.js";
|
|
6
6
|
|
|
7
7
|
// A run: one agent at work on one task for one person, with what it reported, what it handed
|
|
8
8
|
// in, and what it waits for. Takes its data as props and nothing from the network.
|
|
9
9
|
|
|
10
10
|
/** The words of a run's status in English: the default pack's. */
|
|
11
11
|
export const RUN_STATUS_LABELS: Readonly<Record<RunStatus, string>> = {
|
|
12
|
-
running: "
|
|
12
|
+
running: "Open",
|
|
13
13
|
waiting: "Waiting for a decision",
|
|
14
14
|
finished: "Finished",
|
|
15
15
|
failed: "Failed",
|
|
@@ -49,7 +49,7 @@ export function RunCard(props: RunCardProps) {
|
|
|
49
49
|
const { run } = props;
|
|
50
50
|
const open = run.status === "running" || run.status === "waiting";
|
|
51
51
|
return (
|
|
52
|
-
<article className="sc-run" aria-label={words.label(run.agent, run.person
|
|
52
|
+
<article className="sc-run" aria-label={words.label(run.agent, personName(run.person))}>
|
|
53
53
|
<header className="sc-run-header">
|
|
54
54
|
<span className="sc-run-agent">{run.agent}</span>
|
|
55
55
|
<RunStatusBadge status={run.status} />
|
|
@@ -61,6 +61,12 @@ export function RunCard(props: RunCardProps) {
|
|
|
61
61
|
{words.until} <Time iso={run.endedAt} />
|
|
62
62
|
</>
|
|
63
63
|
)}
|
|
64
|
+
{open && (
|
|
65
|
+
<>
|
|
66
|
+
{" "}
|
|
67
|
+
· {words.lastHeard} <Time iso={run.lastHeardAt} />
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
64
70
|
</span>
|
|
65
71
|
{open && props.onAbandon !== undefined && (
|
|
66
72
|
<Button
|
package/src/components/shell.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ReactNode, useEffect, useRef } from "react";
|
|
2
2
|
import type { RestPerson } from "../api.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
|
-
import { Avatar, cx } from "./ui.js";
|
|
4
|
+
import { Avatar, cx, personName } from "./ui.js";
|
|
5
5
|
|
|
6
6
|
// The frame every page sits in (ADR-0010): a header that names the workspace and the project the
|
|
7
7
|
// page is on, with the person's menu at its end; a row of tabs below it with the pages of the
|
|
@@ -34,8 +34,10 @@ export interface ShellProps {
|
|
|
34
34
|
readonly title: string;
|
|
35
35
|
/** The marks to show beside the name; none shows the name alone. */
|
|
36
36
|
readonly brand?: Brand | undefined;
|
|
37
|
-
/** The project the page is on, named after the workspace, or nothing on the workspace's own pages. */
|
|
38
|
-
readonly project?:
|
|
37
|
+
/** The project the page is on, named after the workspace, with its mark when the page has it, or nothing on the workspace's own pages. */
|
|
38
|
+
readonly project?:
|
|
39
|
+
| { readonly name: string; readonly href: string; readonly mark?: ReactNode }
|
|
40
|
+
| undefined;
|
|
39
41
|
/** The pages of the level the person is on, as tabs: the project's, or the workspace's. */
|
|
40
42
|
readonly nav: readonly NavItem[];
|
|
41
43
|
/** The workspace's pages, reached from anywhere through the person's menu. */
|
|
@@ -107,9 +109,9 @@ function PersonMenu(props: {
|
|
|
107
109
|
const { person } = props;
|
|
108
110
|
return (
|
|
109
111
|
<details className="sc-menu" ref={details}>
|
|
110
|
-
<summary className="sc-menu-summary" aria-label={words.shell.menu(person
|
|
112
|
+
<summary className="sc-menu-summary" aria-label={words.shell.menu(personName(person))}>
|
|
111
113
|
<Avatar person={person} size="sm" />
|
|
112
|
-
<span className="sc-person-login">{person
|
|
114
|
+
<span className="sc-person-login">{personName(person)}</span>
|
|
113
115
|
<svg className="sc-menu-caret" viewBox="0 0 16 16" aria-hidden="true">
|
|
114
116
|
<path
|
|
115
117
|
d="M4 6l4 4 4-4"
|
|
@@ -228,6 +230,7 @@ export function Shell(props: ShellProps) {
|
|
|
228
230
|
href={props.project.href}
|
|
229
231
|
onClick={follow(props.project.href)}
|
|
230
232
|
>
|
|
233
|
+
{props.project.mark}
|
|
231
234
|
{props.project.name}
|
|
232
235
|
</a>
|
|
233
236
|
</span>
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
TASK_STATES,
|
|
15
15
|
} from "../api.js";
|
|
16
16
|
import { useWords } from "../i18n/index.js";
|
|
17
|
-
import { Button } from "./ui.js";
|
|
17
|
+
import { Button, personName } from "./ui.js";
|
|
18
18
|
|
|
19
19
|
// Writing a task, or changing one. The bounds are the package's, said before a request is made.
|
|
20
20
|
// The plan is two days (ADR-0019); what happened to the task is the board's to keep, not a
|
|
@@ -151,7 +151,7 @@ export function TaskForm(props: TaskFormProps) {
|
|
|
151
151
|
<option value="">{words.common.nobody}</option>
|
|
152
152
|
{props.people.map((person) => (
|
|
153
153
|
<option key={person.id} value={person.id}>
|
|
154
|
-
{person
|
|
154
|
+
{personName(person)}
|
|
155
155
|
</option>
|
|
156
156
|
))}
|
|
157
157
|
</select>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import type { RestTask } from "../api.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
|
+
import { ShowMore, usePaged } from "./paged.js";
|
|
4
5
|
import { Avatar, Badge, CategoryBadge, DueBadge, StateBadge } from "./ui.js";
|
|
5
6
|
|
|
6
7
|
// A list of tasks as rows, where the board's columns are not the point: the calendar's late
|
|
7
8
|
// and unscheduled tasks, a goal's tasks, a person's own work. Each row says what a card says,
|
|
8
|
-
// in one line. Takes its data as props and nothing from
|
|
9
|
+
// in one line; a long list shows a page at a time. Takes its data as props and nothing from
|
|
10
|
+
// the network.
|
|
9
11
|
|
|
10
12
|
export interface TaskListProps {
|
|
11
13
|
readonly tasks: readonly RestTask[];
|
|
@@ -13,7 +15,7 @@ export interface TaskListProps {
|
|
|
13
15
|
/** Whether each row says the task's state: where the rows are not grouped by it. */
|
|
14
16
|
readonly showState?: boolean | undefined;
|
|
15
17
|
/** What the row says the task belongs to, where tasks of several projects are listed together. */
|
|
16
|
-
readonly projectOf?: ((task: RestTask) =>
|
|
18
|
+
readonly projectOf?: ((task: RestTask) => ReactNode) | undefined;
|
|
17
19
|
/** The day to count due days from; the browser's today when left out. */
|
|
18
20
|
readonly today?: string | undefined;
|
|
19
21
|
readonly label?: string | undefined;
|
|
@@ -22,40 +24,41 @@ export interface TaskListProps {
|
|
|
22
24
|
|
|
23
25
|
export function TaskList(props: TaskListProps) {
|
|
24
26
|
const words = useWords();
|
|
27
|
+
const { shown, rest, more } = usePaged(props.tasks);
|
|
25
28
|
if (props.tasks.length === 0) {
|
|
26
29
|
return <>{props.empty ?? null}</>;
|
|
27
30
|
}
|
|
28
31
|
return (
|
|
29
|
-
|
|
30
|
-
{props.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{props.showState === true && <StateBadge state={task.state} />}
|
|
41
|
-
{task.openDecisions > 0 && (
|
|
42
|
-
<Badge tone="warning" title={words.board.decisionsTitle}>
|
|
43
|
-
{words.board.decisions(task.openDecisions)}
|
|
44
|
-
</Badge>
|
|
32
|
+
<>
|
|
33
|
+
<ul className="sc-task-rows" aria-label={props.label}>
|
|
34
|
+
{shown.map((task) => {
|
|
35
|
+
const project = props.projectOf?.(task);
|
|
36
|
+
return (
|
|
37
|
+
<li key={task.id} className="sc-task-row">
|
|
38
|
+
<a className="sc-task-row-title" href={props.taskHref(task)}>
|
|
39
|
+
<span className="sc-card-number">#{task.number}</span> {task.title}
|
|
40
|
+
</a>
|
|
41
|
+
{project !== undefined && project !== null && (
|
|
42
|
+
<span className="sc-task-row-project">{project}</span>
|
|
45
43
|
)}
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
<span className="sc-task-row-meta">
|
|
45
|
+
{task.category !== null && <CategoryBadge category={task.category} />}
|
|
46
|
+
{props.showState === true && <StateBadge state={task.state} />}
|
|
47
|
+
{task.openDecisions > 0 && (
|
|
48
|
+
<Badge tone="warning" title={words.board.decisionsTitle}>
|
|
49
|
+
{words.board.decisions(task.openDecisions)}
|
|
50
|
+
</Badge>
|
|
51
|
+
)}
|
|
52
|
+
{task.dueOn !== null && (
|
|
53
|
+
<DueBadge dueOn={task.dueOn} state={task.state} today={props.today} />
|
|
54
|
+
)}
|
|
55
|
+
{task.assignee !== null && <Avatar person={task.assignee} size="sm" />}
|
|
56
|
+
</span>
|
|
57
|
+
</li>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</ul>
|
|
61
|
+
<ShowMore rest={rest} onMore={more} />
|
|
62
|
+
</>
|
|
60
63
|
);
|
|
61
64
|
}
|
|
@@ -177,7 +177,7 @@ export function TaskView(props: TaskViewProps) {
|
|
|
177
177
|
)}
|
|
178
178
|
{runs.length > 0 && (
|
|
179
179
|
<section className="sc-task-section" aria-label={words.task.runs}>
|
|
180
|
-
<h2 className="sc-section-title">{words.task.
|
|
180
|
+
<h2 className="sc-section-title">{words.task.runs}</h2>
|
|
181
181
|
<RunList
|
|
182
182
|
runs={runs}
|
|
183
183
|
decisionHref={props.decisionHref}
|
package/src/components/ui.tsx
CHANGED
|
@@ -74,13 +74,22 @@ export function Callout(props: {
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** What a person is called on the pages: the name the host gives, or the login where there is none. */
|
|
78
|
+
export function personName(person: {
|
|
79
|
+
readonly login: string;
|
|
80
|
+
readonly name: string | null;
|
|
81
|
+
}): string {
|
|
82
|
+
const name = person.name?.trim() ?? "";
|
|
83
|
+
return name === "" ? person.login : name;
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
/**
|
|
78
87
|
* A person's picture as the git host serves it: loaded lazily, without a referrer, and gone as
|
|
79
|
-
* a whole when there is none or it cannot be loaded, leaving the initial of the
|
|
88
|
+
* a whole when there is none or it cannot be loaded, leaving the initial of the name instead.
|
|
80
89
|
*/
|
|
81
90
|
export function Avatar(props: { readonly person: RestPerson; readonly size?: "sm" | "md" | "lg" }) {
|
|
82
91
|
const size = props.size ?? "md";
|
|
83
|
-
const initial = props.person.
|
|
92
|
+
const initial = personName(props.person).slice(0, 1).toUpperCase();
|
|
84
93
|
if (props.person.avatar === null) {
|
|
85
94
|
return (
|
|
86
95
|
<span
|
|
@@ -103,12 +112,12 @@ export function Avatar(props: { readonly person: RestPerson; readonly size?: "sm
|
|
|
103
112
|
);
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
/** A person, as a picture and
|
|
115
|
+
/** A person, as a picture and their name, wherever one is named; the login is there on hover. */
|
|
107
116
|
export function PersonChip(props: { readonly person: RestPerson; readonly size?: "sm" | "md" }) {
|
|
108
117
|
return (
|
|
109
|
-
<span className="sc-person" title={props.person.
|
|
118
|
+
<span className="sc-person" title={props.person.login}>
|
|
110
119
|
<Avatar person={props.person} size={props.size ?? "sm"} />
|
|
111
|
-
<span className="sc-person-login">{props.person
|
|
120
|
+
<span className="sc-person-login">{personName(props.person)}</span>
|
|
112
121
|
</span>
|
|
113
122
|
);
|
|
114
123
|
}
|
package/src/components/work.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RestTask, RestWork } from "../api.js";
|
|
2
2
|
import { addDays, today as todayOf } from "../dates.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
|
+
import { ProjectMark } from "./projects.js";
|
|
4
5
|
import { TaskList } from "./task-list.js";
|
|
5
6
|
import { PersonChip, Time } from "./ui.js";
|
|
6
7
|
|
|
@@ -24,7 +25,14 @@ export function WorkList(props: WorkListProps) {
|
|
|
24
25
|
const { tasks, decisions } = props.work;
|
|
25
26
|
const projectOf = new Map(tasks.map((item) => [item.task.id, item.project]));
|
|
26
27
|
const hrefOf = (task: RestTask) => props.taskHref(projectOf.get(task.id)?.key ?? "", task);
|
|
27
|
-
const nameOf = (task: RestTask) =>
|
|
28
|
+
const nameOf = (task: RestTask) => {
|
|
29
|
+
const project = projectOf.get(task.id);
|
|
30
|
+
return project === undefined ? undefined : (
|
|
31
|
+
<>
|
|
32
|
+
<ProjectMark project={project} size="sm" /> {project.name}
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
28
36
|
const groups: { readonly title: string; readonly tasks: RestTask[] }[] = [
|
|
29
37
|
{ title: work.overdue, tasks: [] },
|
|
30
38
|
{ title: work.thisWeek, tasks: [] },
|
|
@@ -69,7 +77,9 @@ export function WorkList(props: WorkListProps) {
|
|
|
69
77
|
>
|
|
70
78
|
{decision.question}
|
|
71
79
|
</a>
|
|
72
|
-
<span className="sc-task-row-project">
|
|
80
|
+
<span className="sc-task-row-project">
|
|
81
|
+
<ProjectMark project={project} size="sm" /> {work.inProject(project.name)}
|
|
82
|
+
</span>
|
|
73
83
|
<span className="sc-decision-row-meta">
|
|
74
84
|
<PersonChip person={decision.raisedBy} /> <Time iso={decision.createdAt} />
|
|
75
85
|
</span>
|