@skillcdn/console 0.1.17 → 0.1.19
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/SKILLCDN.md +1 -1
- package/dist/api.d.ts +3 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +2 -1
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +11 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +496 -51
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/credentials.d.ts +102 -3
- package/dist/cli/credentials.d.ts.map +1 -1
- package/dist/cli/credentials.js +177 -8
- package/dist/cli/credentials.js.map +1 -1
- package/dist/cli/directory.d.ts +6 -0
- package/dist/cli/directory.d.ts.map +1 -1
- package/dist/cli/directory.js +37 -6
- package/dist/cli/directory.js.map +1 -1
- package/dist/cli/format.d.ts +11 -2
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +114 -5
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +49 -25
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/install.d.ts +21 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/install.js +87 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/main.js +22 -3
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/notices.d.ts +13 -0
- package/dist/cli/notices.d.ts.map +1 -0
- package/dist/cli/notices.js +31 -0
- package/dist/cli/notices.js.map +1 -0
- package/dist/client.d.ts +36 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +22 -4
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +66 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +115 -0
- package/dist/compat.js.map +1 -0
- package/dist/components/board.d.ts +9 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +82 -5
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +3 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +21 -2
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/comments.d.ts.map +1 -1
- package/dist/components/comments.js +12 -4
- package/dist/components/comments.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +4 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +20 -2
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +50 -13
- package/dist/components/goals.js.map +1 -1
- package/dist/components/markdown.d.ts +6 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +7 -0
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/pictures.d.ts +68 -0
- package/dist/components/pictures.d.ts.map +1 -0
- package/dist/components/pictures.js +213 -0
- package/dist/components/pictures.js.map +1 -0
- package/dist/components/projects.d.ts +12 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +15 -0
- package/dist/components/projects.js.map +1 -1
- package/dist/components/roadmap.d.ts +37 -0
- package/dist/components/roadmap.d.ts.map +1 -0
- package/dist/components/roadmap.js +76 -0
- package/dist/components/roadmap.js.map +1 -0
- package/dist/components/task-view.d.ts +3 -0
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/tokens.d.ts +2 -0
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +13 -3
- package/dist/components/tokens.js.map +1 -1
- package/dist/console.css +486 -3
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +132 -17
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +16 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +48 -3
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +76 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +74 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +74 -2
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +31 -7
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +48 -8
- package/dist/router.js.map +1 -1
- package/dist/rules.js +5 -5
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +200 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +96 -0
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +2 -2
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +3 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +26 -15
- package/docs/specs/rest.md +31 -9
- package/package.json +3 -2
- package/skills/working-the-board/SKILL.md +11 -4
- package/src/api.ts +36 -0
- package/src/cli/cli.ts +616 -54
- package/src/cli/credentials.ts +276 -14
- package/src/cli/directory.ts +43 -6
- package/src/cli/format.ts +146 -5
- package/src/cli/help.ts +51 -25
- package/src/cli/install.ts +111 -0
- package/src/cli/main.ts +23 -3
- package/src/cli/notices.ts +48 -0
- package/src/client.ts +91 -6
- package/src/compat.ts +173 -0
- package/src/components/board.tsx +154 -8
- package/src/components/calendar.tsx +41 -1
- package/src/components/comments.tsx +20 -10
- package/src/components/event-feed.tsx +4 -0
- package/src/components/goals.tsx +234 -88
- package/src/components/markdown.tsx +19 -0
- package/src/components/pictures.tsx +389 -0
- package/src/components/projects.tsx +68 -0
- package/src/components/roadmap.tsx +242 -0
- package/src/components/task-view.tsx +43 -2
- package/src/components/tokens.tsx +38 -1
- package/src/console.tsx +278 -4
- package/src/data.ts +73 -2
- package/src/i18n/en.ts +77 -2
- package/src/i18n/ko.ts +77 -2
- package/src/index.ts +35 -0
- package/src/router.ts +70 -11
- package/src/rules.ts +5 -5
- package/src/schemas.ts +117 -0
- package/src/styles/console.css +486 -3
- package/src/vocabulary.ts +3 -0
|
@@ -16,13 +16,22 @@ import type {
|
|
|
16
16
|
import { formatDay } from "../dates.js";
|
|
17
17
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
18
18
|
import type { TaskFormKind } from "../router.js";
|
|
19
|
-
import { TASK_STATES } from "../vocabulary.js";
|
|
19
|
+
import { OPEN_TASK_STATES, TASK_STATES } from "../vocabulary.js";
|
|
20
20
|
import { type CommentDraft, TaskThread } from "./comments.js";
|
|
21
21
|
import { DecisionForm } from "./decision-form.js";
|
|
22
22
|
import { EventFeed } from "./event-feed.js";
|
|
23
23
|
import { Markdown } from "./markdown.js";
|
|
24
24
|
import { TaskForm } from "./task-form.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
Badge,
|
|
27
|
+
Button,
|
|
28
|
+
CategoryBadge,
|
|
29
|
+
DueBadge,
|
|
30
|
+
PersonChip,
|
|
31
|
+
PriorityBadge,
|
|
32
|
+
StateBadge,
|
|
33
|
+
Time,
|
|
34
|
+
} from "./ui.js";
|
|
26
35
|
|
|
27
36
|
// One task: what it is, who it is on, what it links to, the discussion on it, which is what was
|
|
28
37
|
// said about it with the agents at work on it and the decisions about it in the order they
|
|
@@ -76,6 +85,9 @@ export interface TaskViewProps {
|
|
|
76
85
|
| ((decision: RestDecision, patch: RestDecisionPatch) => void)
|
|
77
86
|
| undefined;
|
|
78
87
|
readonly onAbandonRun?: ((run: RestRun) => void) | undefined;
|
|
88
|
+
/** Called to put a done or dropped task away, and to bring one back (ADR-0029); left out, neither is offered. */
|
|
89
|
+
readonly onArchive?: ((task: RestTask) => void) | undefined;
|
|
90
|
+
readonly onRestore?: ((task: RestTask) => void) | undefined;
|
|
79
91
|
/** Called with a word for the discussion, and the files put in it. Left out, nothing can be said here. */
|
|
80
92
|
readonly onComment?: ((draft: CommentDraft) => void) | undefined;
|
|
81
93
|
}
|
|
@@ -135,6 +147,7 @@ export function TaskView(props: TaskViewProps) {
|
|
|
135
147
|
</h1>
|
|
136
148
|
<div className="sc-task-badges">
|
|
137
149
|
<StateBadge state={task.state} />
|
|
150
|
+
{task.archivedAt !== null && <Badge tone="neutral">{words.task.archived}</Badge>}
|
|
138
151
|
{task.category !== null && <CategoryBadge category={task.category} />}
|
|
139
152
|
<PriorityBadge priority={task.priority} />
|
|
140
153
|
{task.dueOn !== null && (
|
|
@@ -312,6 +325,14 @@ export function TaskView(props: TaskViewProps) {
|
|
|
312
325
|
</dd>
|
|
313
326
|
</div>
|
|
314
327
|
)}
|
|
328
|
+
{task.archivedAt !== null && (
|
|
329
|
+
<div>
|
|
330
|
+
<dt>{words.task.archivedAt}</dt>
|
|
331
|
+
<dd>
|
|
332
|
+
<Time iso={task.archivedAt} />
|
|
333
|
+
</dd>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
315
336
|
<div>
|
|
316
337
|
<dt>{words.task.written}</dt>
|
|
317
338
|
<dd>
|
|
@@ -333,6 +354,26 @@ export function TaskView(props: TaskViewProps) {
|
|
|
333
354
|
<a className="sc-button sc-button-secondary" href={props.formHref("ask")}>
|
|
334
355
|
{words.task.raiseDecision}
|
|
335
356
|
</a>
|
|
357
|
+
{props.onArchive !== undefined &&
|
|
358
|
+
task.archivedAt === null &&
|
|
359
|
+
!OPEN_TASK_STATES.includes(task.state) && (
|
|
360
|
+
<Button
|
|
361
|
+
variant="secondary"
|
|
362
|
+
disabled={props.busy === true}
|
|
363
|
+
onClick={() => props.onArchive?.(task)}
|
|
364
|
+
>
|
|
365
|
+
{words.task.archive}
|
|
366
|
+
</Button>
|
|
367
|
+
)}
|
|
368
|
+
{props.onRestore !== undefined && task.archivedAt !== null && (
|
|
369
|
+
<Button
|
|
370
|
+
variant="secondary"
|
|
371
|
+
disabled={props.busy === true}
|
|
372
|
+
onClick={() => props.onRestore?.(task)}
|
|
373
|
+
>
|
|
374
|
+
{words.task.restore}
|
|
375
|
+
</Button>
|
|
376
|
+
)}
|
|
336
377
|
</div>
|
|
337
378
|
)}
|
|
338
379
|
</aside>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
type ClientVerdict,
|
|
3
4
|
DEFAULT_TOKEN_DAYS,
|
|
5
|
+
judgeClient,
|
|
4
6
|
MAX_TOKEN_NAME_LENGTH,
|
|
7
|
+
parseVersion,
|
|
5
8
|
type RestToken,
|
|
6
9
|
type RestTokenInput,
|
|
7
10
|
} from "../api.js";
|
|
8
11
|
import { useWords } from "../i18n/index.js";
|
|
9
|
-
import { Button, Callout, CopyButton, Time } from "./ui.js";
|
|
12
|
+
import { Badge, Button, Callout, CopyButton, Time } from "./ui.js";
|
|
10
13
|
|
|
11
14
|
// The tokens a person holds for their agents, scripts and consoles of their own, shown as the
|
|
12
15
|
// agents that hold them: the list, the way to make one by hand, and the one just made, whose
|
|
@@ -36,6 +39,22 @@ export interface TokenListProps {
|
|
|
36
39
|
readonly onRevoke?: ((token: RestToken) => void) | undefined;
|
|
37
40
|
readonly busy?: boolean | undefined;
|
|
38
41
|
readonly empty?: ReactNode;
|
|
42
|
+
/** The console's version (ADR-0025), so that the version of the command an agent runs is said to be behind or ahead; left out, the version alone is shown. */
|
|
43
|
+
readonly consoleVersion?: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What a command's version is to the console's: nothing to say when they go together, else which way they differ. */
|
|
47
|
+
function verdictOf(
|
|
48
|
+
client: string,
|
|
49
|
+
consoleVersion: string | undefined,
|
|
50
|
+
): Exclude<ClientVerdict, "current"> | undefined {
|
|
51
|
+
const command = parseVersion(client);
|
|
52
|
+
const server = parseVersion(consoleVersion);
|
|
53
|
+
if (command === undefined || server === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const { verdict } = judgeClient(command, server);
|
|
57
|
+
return verdict === "current" ? undefined : verdict;
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
export function TokenList(props: TokenListProps) {
|
|
@@ -53,6 +72,24 @@ export function TokenList(props: TokenListProps) {
|
|
|
53
72
|
<span>
|
|
54
73
|
{words.connected} <Time iso={token.createdAt} />
|
|
55
74
|
</span>
|
|
75
|
+
{token.client !== null && (
|
|
76
|
+
<span>
|
|
77
|
+
{words.command(token.client)}
|
|
78
|
+
{((verdict) =>
|
|
79
|
+
verdict === undefined ? null : (
|
|
80
|
+
<>
|
|
81
|
+
{" "}
|
|
82
|
+
<Badge
|
|
83
|
+
tone={
|
|
84
|
+
verdict === "outdated" || verdict === "too_new" ? "danger" : "warning"
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
{words.commandVerdict[verdict]}
|
|
88
|
+
</Badge>
|
|
89
|
+
</>
|
|
90
|
+
))(verdictOf(token.client, props.consoleVersion))}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
56
93
|
<span>
|
|
57
94
|
{token.expiresAt === null ? (
|
|
58
95
|
words.doesNotExpire
|
package/src/console.tsx
CHANGED
|
@@ -43,16 +43,19 @@ import { ConnectWords } from "./components/connect.js";
|
|
|
43
43
|
import { DecisionForm } from "./components/decision-form.js";
|
|
44
44
|
import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
|
|
45
45
|
import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
|
|
46
|
-
import { GoalForm, GoalList, type GoalListProps, GoalView } from "./components/goals.js";
|
|
46
|
+
import { GoalForm, GoalList, type GoalListProps, GoalView, goalMonth } from "./components/goals.js";
|
|
47
|
+
import { PAGE_SIZE } from "./components/paged.js";
|
|
47
48
|
import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
|
|
48
49
|
import {
|
|
49
50
|
MemberList,
|
|
50
51
|
type MemberListProps,
|
|
52
|
+
PackTokenForm,
|
|
51
53
|
ProjectForm,
|
|
52
54
|
ProjectList,
|
|
53
55
|
type ProjectListProps,
|
|
54
56
|
ProjectMark,
|
|
55
57
|
} from "./components/projects.js";
|
|
58
|
+
import { GoalTimeline } from "./components/roadmap.js";
|
|
56
59
|
import { RulesForm, RulesView } from "./components/rules.js";
|
|
57
60
|
import {
|
|
58
61
|
type Brand,
|
|
@@ -74,6 +77,8 @@ import {
|
|
|
74
77
|
EmptyState,
|
|
75
78
|
personName,
|
|
76
79
|
Spinner,
|
|
80
|
+
StateBadge,
|
|
81
|
+
Time,
|
|
77
82
|
} from "./components/ui.js";
|
|
78
83
|
import { WorkList, type WorkListProps } from "./components/work.js";
|
|
79
84
|
import { ConnectPage } from "./connect-page.js";
|
|
@@ -94,6 +99,7 @@ import {
|
|
|
94
99
|
customPageHref,
|
|
95
100
|
decisionHref,
|
|
96
101
|
docHref,
|
|
102
|
+
type GoalTaskView,
|
|
97
103
|
goalHref,
|
|
98
104
|
matchRoute,
|
|
99
105
|
type Navigation,
|
|
@@ -106,6 +112,7 @@ import {
|
|
|
106
112
|
projectHref,
|
|
107
113
|
projectSettingsHref,
|
|
108
114
|
type Route,
|
|
115
|
+
roadmapHref,
|
|
109
116
|
settingsHref,
|
|
110
117
|
signInFailureOf,
|
|
111
118
|
taskHref,
|
|
@@ -440,6 +447,102 @@ function ProjectsPage(props: {
|
|
|
440
447
|
);
|
|
441
448
|
}
|
|
442
449
|
|
|
450
|
+
/**
|
|
451
|
+
* The archive of the board (ADR-0029): the tasks put away, newest first, a page at a time from
|
|
452
|
+
* the server, each brought back from here by whoever may work in the project.
|
|
453
|
+
*/
|
|
454
|
+
function ArchivePage(props: { readonly project: RestProject; readonly data: ConsoleData }) {
|
|
455
|
+
const words = useWords();
|
|
456
|
+
const { project, data } = props;
|
|
457
|
+
const key = project.key;
|
|
458
|
+
const { busy, error, act } = useAction();
|
|
459
|
+
const [items, setItems] = useState<readonly RestTask[]>([]);
|
|
460
|
+
const [more, setMore] = useState(false);
|
|
461
|
+
const [loaded, setLoaded] = useState(false);
|
|
462
|
+
const { archivedTasks } = data.actions;
|
|
463
|
+
const feedLength = data.events.length;
|
|
464
|
+
// The first page, read again whenever the project's feed grows: something was put away or brought back.
|
|
465
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: the feed's length is what triggers the read
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
const controller = new AbortController();
|
|
468
|
+
archivedTasks({ limit: PAGE_SIZE, offset: 0 }, controller.signal)
|
|
469
|
+
.then((page) => {
|
|
470
|
+
if (!controller.signal.aborted) {
|
|
471
|
+
setItems(page.items);
|
|
472
|
+
setMore(page.more);
|
|
473
|
+
setLoaded(true);
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
.catch(() => undefined);
|
|
477
|
+
return () => controller.abort();
|
|
478
|
+
}, [archivedTasks, feedLength]);
|
|
479
|
+
const onMore = () =>
|
|
480
|
+
void archivedTasks({ limit: PAGE_SIZE, offset: items.length })
|
|
481
|
+
.then((page) => {
|
|
482
|
+
setItems((current) => [...current, ...page.items]);
|
|
483
|
+
setMore(page.more);
|
|
484
|
+
})
|
|
485
|
+
.catch(() => undefined);
|
|
486
|
+
const archive = words.archive;
|
|
487
|
+
return (
|
|
488
|
+
<>
|
|
489
|
+
<div className="sc-page-head">
|
|
490
|
+
<h1 className="sc-page-title">{archive.title}</h1>
|
|
491
|
+
<LinkButton href={projectHref(key)}>{words.nav.board}</LinkButton>
|
|
492
|
+
</div>
|
|
493
|
+
<p className="sc-lead">{archive.lead}</p>
|
|
494
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
495
|
+
{!loaded ? (
|
|
496
|
+
<Spinner label={archive.loading} />
|
|
497
|
+
) : items.length === 0 ? (
|
|
498
|
+
<EmptyState title={archive.empty.title} body={archive.empty.body} />
|
|
499
|
+
) : (
|
|
500
|
+
<>
|
|
501
|
+
<ul className="sc-task-rows" aria-label={archive.title}>
|
|
502
|
+
{items.map((task) => (
|
|
503
|
+
<li key={task.id} className="sc-task-row">
|
|
504
|
+
<a className="sc-task-row-title" href={taskHref(key, task.number)}>
|
|
505
|
+
<span className="sc-card-number">#{task.number}</span> {task.title}
|
|
506
|
+
</a>
|
|
507
|
+
<span className="sc-task-row-meta">
|
|
508
|
+
<StateBadge state={task.state} />
|
|
509
|
+
{task.archivedAt !== null && (
|
|
510
|
+
<span className="sc-muted">
|
|
511
|
+
{archive.since} <Time iso={task.archivedAt} />
|
|
512
|
+
</span>
|
|
513
|
+
)}
|
|
514
|
+
<Button
|
|
515
|
+
size="sm"
|
|
516
|
+
variant="ghost"
|
|
517
|
+
disabled={busy}
|
|
518
|
+
onClick={() =>
|
|
519
|
+
void act(async () => {
|
|
520
|
+
await data.actions.restoreTask(task.id);
|
|
521
|
+
setItems((current) =>
|
|
522
|
+
current.filter((candidate) => candidate.id !== task.id),
|
|
523
|
+
);
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
>
|
|
527
|
+
{archive.restore}
|
|
528
|
+
</Button>
|
|
529
|
+
</span>
|
|
530
|
+
</li>
|
|
531
|
+
))}
|
|
532
|
+
</ul>
|
|
533
|
+
{more && (
|
|
534
|
+
<div className="sc-show-more">
|
|
535
|
+
<Button variant="secondary" size="sm" onClick={onMore}>
|
|
536
|
+
{archive.more}
|
|
537
|
+
</Button>
|
|
538
|
+
</div>
|
|
539
|
+
)}
|
|
540
|
+
</>
|
|
541
|
+
)}
|
|
542
|
+
</>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
443
546
|
function TaskPage(props: {
|
|
444
547
|
readonly route: Extract<Route, { name: "task" }>;
|
|
445
548
|
readonly project: RestProject;
|
|
@@ -453,10 +556,43 @@ function TaskPage(props: {
|
|
|
453
556
|
const words = useWords();
|
|
454
557
|
const { route, project, data, components, navigation, busy, error, act } = props;
|
|
455
558
|
const key = project.key;
|
|
456
|
-
const
|
|
559
|
+
const current =
|
|
457
560
|
route.number === undefined
|
|
458
561
|
? data.tasks.find((candidate) => candidate.id === route.id)
|
|
459
562
|
: data.tasks.find((candidate) => candidate.number === route.number);
|
|
563
|
+
// A task put away is not among the board's (ADR-0029): it is asked for by itself.
|
|
564
|
+
const [fetched, setFetched] = useState<RestTask | undefined>(undefined);
|
|
565
|
+
const [missing, setMissing] = useState<string | undefined>(undefined);
|
|
566
|
+
const { fetchTask } = data.actions;
|
|
567
|
+
const wanted =
|
|
568
|
+
current === undefined && data.projectLoaded
|
|
569
|
+
? route.number === undefined
|
|
570
|
+
? route.id
|
|
571
|
+
: String(route.number)
|
|
572
|
+
: undefined;
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
if (wanted === undefined) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const controller = new AbortController();
|
|
578
|
+
fetchTask(wanted, controller.signal)
|
|
579
|
+
.then((found) => {
|
|
580
|
+
if (!controller.signal.aborted) {
|
|
581
|
+
setFetched(found);
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
.catch(() => {
|
|
585
|
+
if (!controller.signal.aborted) {
|
|
586
|
+
setMissing(wanted);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
return () => controller.abort();
|
|
590
|
+
}, [wanted, fetchTask]);
|
|
591
|
+
const task =
|
|
592
|
+
current ??
|
|
593
|
+
(fetched !== undefined && (fetched.id === route.id || fetched.number === route.number)
|
|
594
|
+
? fetched
|
|
595
|
+
: undefined);
|
|
460
596
|
const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
|
|
461
597
|
const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
|
|
462
598
|
const { taskHistory, taskComments } = data.actions;
|
|
@@ -493,6 +629,9 @@ function TaskPage(props: {
|
|
|
493
629
|
return () => controller.abort();
|
|
494
630
|
}, [taskId, taskHistory, taskComments, feedLength]);
|
|
495
631
|
if (task === undefined) {
|
|
632
|
+
if (wanted !== undefined && missing !== wanted) {
|
|
633
|
+
return <Spinner label={words.common.loading} />;
|
|
634
|
+
}
|
|
496
635
|
return (
|
|
497
636
|
<EmptyState
|
|
498
637
|
title={words.task.noSuch.title}
|
|
@@ -543,6 +682,17 @@ function TaskPage(props: {
|
|
|
543
682
|
void act(() => data.actions.updateDecision(decision.id, patch))
|
|
544
683
|
}
|
|
545
684
|
onAbandonRun={(run) => void act(() => data.actions.abandonRun(run.id))}
|
|
685
|
+
onArchive={(target) =>
|
|
686
|
+
void act(async () => {
|
|
687
|
+
await data.actions.archiveTask(target.id);
|
|
688
|
+
navigation.go(projectHref(key, "archive"));
|
|
689
|
+
})
|
|
690
|
+
}
|
|
691
|
+
onRestore={(target) =>
|
|
692
|
+
void act(async () => {
|
|
693
|
+
setFetched(await data.actions.restoreTask(target.id));
|
|
694
|
+
})
|
|
695
|
+
}
|
|
546
696
|
onComment={(draft) =>
|
|
547
697
|
void act(async () => {
|
|
548
698
|
await data.actions.comment(task.id, {
|
|
@@ -613,6 +763,7 @@ function ProjectPage(props: {
|
|
|
613
763
|
taskHref={href}
|
|
614
764
|
onOpen={(task) => navigation.go(href(task))}
|
|
615
765
|
onMove={onMove}
|
|
766
|
+
archiveHref={projectHref(key, "archive")}
|
|
616
767
|
empty={
|
|
617
768
|
data.tasks.length === 0 ? (
|
|
618
769
|
<EmptyState
|
|
@@ -702,6 +853,7 @@ function ProjectPage(props: {
|
|
|
702
853
|
categories={data.categories}
|
|
703
854
|
goals={data.goals}
|
|
704
855
|
taskHref={href}
|
|
856
|
+
goalHref={(goal) => goalHref(key, goal.id)}
|
|
705
857
|
monthHref={(wanted) => calendarHref(key, wanted, route.filter)}
|
|
706
858
|
/>
|
|
707
859
|
<div className="sc-calendar-below">
|
|
@@ -765,6 +917,7 @@ function ProjectPage(props: {
|
|
|
765
917
|
{making && (
|
|
766
918
|
<section className="sc-panel sc-panel-form" aria-label={roadmap.setGoalAria}>
|
|
767
919
|
<GoalForm
|
|
920
|
+
categories={data.categories}
|
|
768
921
|
busy={busy}
|
|
769
922
|
onSubmit={(input) =>
|
|
770
923
|
void act(async () => {
|
|
@@ -776,6 +929,18 @@ function ProjectPage(props: {
|
|
|
776
929
|
/>
|
|
777
930
|
</section>
|
|
778
931
|
)}
|
|
932
|
+
{data.goals.length > 0 && (
|
|
933
|
+
<GoalTimeline
|
|
934
|
+
goals={data.goals}
|
|
935
|
+
from={
|
|
936
|
+
route.name === "roadmap" && route.from !== undefined ? route.from : monthOf(today())
|
|
937
|
+
}
|
|
938
|
+
today={today()}
|
|
939
|
+
categories={data.categories}
|
|
940
|
+
goalHref={(goal) => goalHref(key, goal.id)}
|
|
941
|
+
fromHref={(month) => roadmapHref(key, month)}
|
|
942
|
+
/>
|
|
943
|
+
)}
|
|
779
944
|
<components.GoalList
|
|
780
945
|
goals={data.goals}
|
|
781
946
|
goalHref={(goal) => goalHref(key, goal.id)}
|
|
@@ -820,6 +985,7 @@ function ProjectPage(props: {
|
|
|
820
985
|
<section className="sc-panel sc-panel-form" aria-label={roadmap.editAria}>
|
|
821
986
|
<GoalForm
|
|
822
987
|
goal={goal}
|
|
988
|
+
categories={data.categories}
|
|
823
989
|
busy={busy}
|
|
824
990
|
onSubmit={(input) =>
|
|
825
991
|
void act(async () => {
|
|
@@ -833,6 +999,10 @@ function ProjectPage(props: {
|
|
|
833
999
|
</>
|
|
834
1000
|
);
|
|
835
1001
|
}
|
|
1002
|
+
const now = today();
|
|
1003
|
+
const towards = data.tasks.filter((task) => task.goalId === goal.id);
|
|
1004
|
+
const month = route.month ?? goalMonth(goal, now);
|
|
1005
|
+
const viewHref = (view: GoalTaskView) => goalHref(key, goal.id, { view });
|
|
836
1006
|
return (
|
|
837
1007
|
<>
|
|
838
1008
|
<div className="sc-page-head">
|
|
@@ -845,13 +1015,60 @@ function ProjectPage(props: {
|
|
|
845
1015
|
tasks={data.tasks}
|
|
846
1016
|
taskHref={href}
|
|
847
1017
|
docHref={toDoc}
|
|
848
|
-
|
|
1018
|
+
today={now}
|
|
1019
|
+
editHref={owner ? goalHref(key, goal.id, { form: "edit" }) : undefined}
|
|
849
1020
|
onMove={
|
|
850
1021
|
owner
|
|
851
|
-
? (target, state) =>
|
|
1022
|
+
? (target, state) =>
|
|
1023
|
+
void act(async () => {
|
|
1024
|
+
await data.actions.updateGoal(target.id, { state });
|
|
1025
|
+
// A goal closed takes its done and dropped tasks to the archive, if the person says so (ADR-0029).
|
|
1026
|
+
const closed = towards.filter(
|
|
1027
|
+
(task) => !OPEN_TASK_STATES.includes(task.state),
|
|
1028
|
+
).length;
|
|
1029
|
+
if (
|
|
1030
|
+
(state === "done" || state === "dropped") &&
|
|
1031
|
+
closed > 0 &&
|
|
1032
|
+
window.confirm(roadmap.archiveAsk(closed))
|
|
1033
|
+
) {
|
|
1034
|
+
await data.actions.archiveGoalTasks(target.id);
|
|
1035
|
+
}
|
|
1036
|
+
})
|
|
1037
|
+
: undefined
|
|
1038
|
+
}
|
|
1039
|
+
onArchiveTasks={
|
|
1040
|
+
owner && towards.some((task) => !OPEN_TASK_STATES.includes(task.state))
|
|
1041
|
+
? () => void act(() => data.actions.archiveGoalTasks(goal.id))
|
|
852
1042
|
: undefined
|
|
853
1043
|
}
|
|
854
1044
|
busy={busy}
|
|
1045
|
+
view={route.view}
|
|
1046
|
+
viewHref={viewHref}
|
|
1047
|
+
board={
|
|
1048
|
+
<components.Board
|
|
1049
|
+
tasks={towards}
|
|
1050
|
+
taskHref={href}
|
|
1051
|
+
onOpen={(task) => navigation.go(href(task))}
|
|
1052
|
+
onMove={onMove}
|
|
1053
|
+
today={now}
|
|
1054
|
+
archiveHref={projectHref(key, "archive")}
|
|
1055
|
+
empty={
|
|
1056
|
+
towards.length === 0 ? <p className="sc-muted">{roadmap.noTasks}</p> : undefined
|
|
1057
|
+
}
|
|
1058
|
+
/>
|
|
1059
|
+
}
|
|
1060
|
+
calendar={
|
|
1061
|
+
<components.Calendar
|
|
1062
|
+
tasks={towards}
|
|
1063
|
+
month={month}
|
|
1064
|
+
today={now}
|
|
1065
|
+
categories={data.categories}
|
|
1066
|
+
goals={[goal]}
|
|
1067
|
+
taskHref={href}
|
|
1068
|
+
goalHref={(target) => goalHref(key, target.id)}
|
|
1069
|
+
monthHref={(wanted) => goalHref(key, goal.id, { view: "calendar", month: wanted })}
|
|
1070
|
+
/>
|
|
1071
|
+
}
|
|
855
1072
|
/>
|
|
856
1073
|
</>
|
|
857
1074
|
);
|
|
@@ -941,6 +1158,9 @@ function ProjectPage(props: {
|
|
|
941
1158
|
</>
|
|
942
1159
|
);
|
|
943
1160
|
}
|
|
1161
|
+
if (route.name === "archive") {
|
|
1162
|
+
return <ArchivePage project={project} data={data} />;
|
|
1163
|
+
}
|
|
944
1164
|
if (route.name === "feed") {
|
|
945
1165
|
return (
|
|
946
1166
|
<>
|
|
@@ -1139,6 +1359,39 @@ function ProjectSettingsPage(props: {
|
|
|
1139
1359
|
</>
|
|
1140
1360
|
);
|
|
1141
1361
|
}
|
|
1362
|
+
if (route.form === "pack-token") {
|
|
1363
|
+
if (!owner) {
|
|
1364
|
+
return <NothingHere />;
|
|
1365
|
+
}
|
|
1366
|
+
return (
|
|
1367
|
+
<>
|
|
1368
|
+
{head}
|
|
1369
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1370
|
+
<section className="sc-panel sc-panel-form" aria-label={words.projects.packToken.aria}>
|
|
1371
|
+
<PackTokenForm
|
|
1372
|
+
set={project.packToken !== null}
|
|
1373
|
+
busy={busy}
|
|
1374
|
+
onSubmit={(token) =>
|
|
1375
|
+
void act(async () => {
|
|
1376
|
+
await data.actions.setPackToken(token);
|
|
1377
|
+
navigation.replace(here);
|
|
1378
|
+
})
|
|
1379
|
+
}
|
|
1380
|
+
onRemove={
|
|
1381
|
+
project.packToken === null
|
|
1382
|
+
? undefined
|
|
1383
|
+
: () =>
|
|
1384
|
+
void act(async () => {
|
|
1385
|
+
await data.actions.removePackToken();
|
|
1386
|
+
navigation.replace(here);
|
|
1387
|
+
})
|
|
1388
|
+
}
|
|
1389
|
+
onCancel={() => navigation.back(here)}
|
|
1390
|
+
/>
|
|
1391
|
+
</section>
|
|
1392
|
+
</>
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1142
1395
|
return (
|
|
1143
1396
|
<>
|
|
1144
1397
|
{head}
|
|
@@ -1174,6 +1427,24 @@ function ProjectSettingsPage(props: {
|
|
|
1174
1427
|
)}
|
|
1175
1428
|
</dd>
|
|
1176
1429
|
</div>
|
|
1430
|
+
<div>
|
|
1431
|
+
<dt>{settings.packToken}</dt>
|
|
1432
|
+
<dd>
|
|
1433
|
+
{project.packToken === null ? (
|
|
1434
|
+
<span className="sc-muted">{settings.packTokenNone}</span>
|
|
1435
|
+
) : (
|
|
1436
|
+
<>
|
|
1437
|
+
{settings.packTokenSet} <Time iso={project.packToken.setAt} />
|
|
1438
|
+
</>
|
|
1439
|
+
)}
|
|
1440
|
+
{owner && (
|
|
1441
|
+
<>
|
|
1442
|
+
{" "}
|
|
1443
|
+
<a href={projectSettingsHref(key, "pack-token")}>{settings.packTokenEdit}</a>
|
|
1444
|
+
</>
|
|
1445
|
+
)}
|
|
1446
|
+
</dd>
|
|
1447
|
+
</div>
|
|
1177
1448
|
<div>
|
|
1178
1449
|
<dt>{form.symbol}</dt>
|
|
1179
1450
|
<dd>
|
|
@@ -1451,6 +1722,7 @@ function PersonAgentsPage(props: {
|
|
|
1451
1722
|
<components.TokenList
|
|
1452
1723
|
tokens={tokens}
|
|
1453
1724
|
busy={busy}
|
|
1725
|
+
consoleVersion={data.me?.console?.version}
|
|
1454
1726
|
onRevoke={(token) =>
|
|
1455
1727
|
void act(async () => {
|
|
1456
1728
|
await data.actions.revokePersonToken(id, token.id);
|
|
@@ -1763,6 +2035,7 @@ function WorkspacePage(props: {
|
|
|
1763
2035
|
<components.TokenList
|
|
1764
2036
|
tokens={data.tokens}
|
|
1765
2037
|
busy={busy}
|
|
2038
|
+
consoleVersion={data.me?.console?.version}
|
|
1766
2039
|
onRevoke={(token) => void act(() => data.actions.revokeToken(token.id))}
|
|
1767
2040
|
empty={<EmptyState title={agents.empty.title} body={agents.empty.body} />}
|
|
1768
2041
|
/>
|
|
@@ -2054,6 +2327,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
2054
2327
|
wide={
|
|
2055
2328
|
route.name === "board" ||
|
|
2056
2329
|
route.name === "calendar" ||
|
|
2330
|
+
(route.name === "goal" && route.form === undefined) ||
|
|
2057
2331
|
(route.name === "custom" &&
|
|
2058
2332
|
pages.some((candidate) => candidate.key === route.page && candidate.wide === true))
|
|
2059
2333
|
}
|