@skillcdn/console 0.1.17 → 0.1.18
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 +3 -3
- package/dist/api.d.ts +2 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -0
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +10 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +145 -14
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +8 -3
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +7 -5
- 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 +15 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +8 -3
- 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/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/goals.d.ts +3 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +23 -11
- package/dist/components/goals.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/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 +204 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +8 -7
- package/dist/console.js.map +1 -1
- package/dist/data.js +1 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +24 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +24 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +24 -0
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +6 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +10 -1
- package/dist/router.js.map +1 -1
- package/dist/schemas.d.ts +42 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +18 -0
- package/dist/schemas.js.map +1 -1
- package/docs/specs/cli.md +9 -6
- package/docs/specs/rest.md +9 -5
- package/package.json +3 -2
- package/skills/working-the-board/SKILL.md +4 -3
- package/src/api.ts +23 -0
- package/src/cli/cli.ts +178 -15
- package/src/cli/format.ts +8 -3
- package/src/cli/help.ts +7 -5
- 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 +31 -6
- package/src/compat.ts +173 -0
- package/src/components/calendar.tsx +41 -1
- package/src/components/goals.tsx +86 -7
- package/src/components/roadmap.tsx +242 -0
- package/src/components/tokens.tsx +38 -1
- package/src/console.tsx +19 -0
- package/src/data.ts +1 -1
- package/src/i18n/en.ts +24 -0
- package/src/i18n/ko.ts +24 -0
- package/src/index.ts +8 -0
- package/src/router.ts +13 -2
- package/src/schemas.ts +20 -0
- package/src/styles/console.css +204 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { RestCategory, RestGoal } from "../api.js";
|
|
2
|
+
import { addMonths, daysBetween, formatMonth, monthOf, today as todayOf } from "../dates.js";
|
|
3
|
+
import { useLanguage, useWords } from "../i18n/index.js";
|
|
4
|
+
import { cx, GoalStateBadge } from "./ui.js";
|
|
5
|
+
|
|
6
|
+
// The roadmap as months (ADR-0026): the months as columns, and each goal planned in days a bar
|
|
7
|
+
// from its start to its target in the colour of its category, filled as far as its tasks are
|
|
8
|
+
// done, with today a line down the months. A goal with one day alone sits on it as a point; one
|
|
9
|
+
// with no day is named below the months, since a plan without days is on none of them. Takes
|
|
10
|
+
// its data as props and nothing from the network; the months shown are the address's (ADR-0010).
|
|
11
|
+
|
|
12
|
+
/** How many months the roadmap shows at once when the address names no other span. */
|
|
13
|
+
export const DEFAULT_TIMELINE_MONTHS = 6;
|
|
14
|
+
|
|
15
|
+
export interface TimelineBar {
|
|
16
|
+
readonly goal: RestGoal;
|
|
17
|
+
/** Where the bar begins and ends, as shares of the whole span, 0 to 1. */
|
|
18
|
+
readonly from: number;
|
|
19
|
+
readonly to: number;
|
|
20
|
+
/** Whether the goal begins within the span, and whether it ends within it. */
|
|
21
|
+
readonly begins: boolean;
|
|
22
|
+
readonly ends: boolean;
|
|
23
|
+
/** Whether the goal has one day only: a point on it, not a bar. */
|
|
24
|
+
readonly point: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The first day of a month, as a day. */
|
|
28
|
+
const firstDayOf = (month: string): string => `${month}-01`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The bars of the goals that touch the span of `months` from `from` on, cut to it: from the
|
|
32
|
+
* goal's start, or the span's first day when it began before, to its target day whole, or the
|
|
33
|
+
* span's end when it ends after. A goal with a start or a target alone sits on that day; a goal
|
|
34
|
+
* with neither is left out.
|
|
35
|
+
*/
|
|
36
|
+
export function timelineBars(
|
|
37
|
+
goals: readonly RestGoal[],
|
|
38
|
+
from: string,
|
|
39
|
+
months: number,
|
|
40
|
+
): TimelineBar[] {
|
|
41
|
+
const first = firstDayOf(from);
|
|
42
|
+
const end = firstDayOf(addMonths(from, months));
|
|
43
|
+
const total = daysBetween(first, end);
|
|
44
|
+
if (total <= 0) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return goals.flatMap((goal) => {
|
|
48
|
+
const startsOn = goal.startsOn ?? goal.dueOn;
|
|
49
|
+
const dueOn = goal.dueOn ?? goal.startsOn;
|
|
50
|
+
if (startsOn === null || dueOn === null || dueOn < first || startsOn >= end) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const begins = startsOn >= first;
|
|
54
|
+
const ends = dueOn < end;
|
|
55
|
+
const fromDay = begins ? daysBetween(first, startsOn) : 0;
|
|
56
|
+
const toDay = ends ? daysBetween(first, dueOn) + 1 : total;
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
goal,
|
|
60
|
+
from: fromDay / total,
|
|
61
|
+
to: toDay / total,
|
|
62
|
+
begins,
|
|
63
|
+
ends,
|
|
64
|
+
point: startsOn === dueOn,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GoalTimelineProps {
|
|
71
|
+
readonly goals: readonly RestGoal[];
|
|
72
|
+
/** The first month shown: `2026-10`. */
|
|
73
|
+
readonly from: string;
|
|
74
|
+
/** How many months are shown; `DEFAULT_TIMELINE_MONTHS` when left out. */
|
|
75
|
+
readonly months?: number | undefined;
|
|
76
|
+
/** The day to count from; the browser's today when left out. */
|
|
77
|
+
readonly today?: string | undefined;
|
|
78
|
+
/** The workspace's categories, for the legend to say what the colours mean. */
|
|
79
|
+
readonly categories?: readonly RestCategory[] | undefined;
|
|
80
|
+
readonly goalHref: (goal: RestGoal) => string;
|
|
81
|
+
/** Where the roadmap is from another month on. */
|
|
82
|
+
readonly fromHref: (month: string) => string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const percent = (share: number): string => `${(share * 100).toFixed(3)}%`;
|
|
86
|
+
|
|
87
|
+
export function GoalTimeline(props: GoalTimelineProps) {
|
|
88
|
+
const words = useWords().roadmap;
|
|
89
|
+
const { tag } = useLanguage();
|
|
90
|
+
const months = Math.max(1, props.months ?? DEFAULT_TIMELINE_MONTHS);
|
|
91
|
+
const now = props.today ?? todayOf();
|
|
92
|
+
const first = firstDayOf(props.from);
|
|
93
|
+
const end = firstDayOf(addMonths(props.from, months));
|
|
94
|
+
const total = daysBetween(first, end);
|
|
95
|
+
const columns = Array.from({ length: months }, (_, index) => addMonths(props.from, index));
|
|
96
|
+
const shares = columns.map(
|
|
97
|
+
(month) => daysBetween(firstDayOf(month), firstDayOf(addMonths(month, 1))) / total,
|
|
98
|
+
);
|
|
99
|
+
const bars = timelineBars(props.goals, props.from, months);
|
|
100
|
+
const todayAt = now >= first && now < end ? daysBetween(first, now) / total : undefined;
|
|
101
|
+
const undated = props.goals.filter((goal) => goal.startsOn === null && goal.dueOn === null);
|
|
102
|
+
const doneShare = (goal: RestGoal): number => {
|
|
103
|
+
const counts = goal.taskCounts;
|
|
104
|
+
const all = counts.idea + counts.ready + counts.in_progress + counts.in_review + counts.done;
|
|
105
|
+
return all === 0 ? 0 : counts.done / all;
|
|
106
|
+
};
|
|
107
|
+
return (
|
|
108
|
+
<div className="sc-timeline">
|
|
109
|
+
<div className="sc-timeline-head">
|
|
110
|
+
<h2 className="sc-timeline-title">{words.timeline.title}</h2>
|
|
111
|
+
<nav className="sc-calendar-nav" aria-label={words.timeline.title}>
|
|
112
|
+
<a
|
|
113
|
+
className="sc-button sc-button-ghost sc-button-sm"
|
|
114
|
+
href={props.fromHref(addMonths(props.from, -1))}
|
|
115
|
+
aria-label={words.timeline.earlier}
|
|
116
|
+
>
|
|
117
|
+
{"<"}
|
|
118
|
+
</a>
|
|
119
|
+
<a className="sc-button sc-button-ghost sc-button-sm" href={props.fromHref(monthOf(now))}>
|
|
120
|
+
{words.timeline.thisMonth}
|
|
121
|
+
</a>
|
|
122
|
+
<a
|
|
123
|
+
className="sc-button sc-button-ghost sc-button-sm"
|
|
124
|
+
href={props.fromHref(addMonths(props.from, 1))}
|
|
125
|
+
aria-label={words.timeline.later}
|
|
126
|
+
>
|
|
127
|
+
{">"}
|
|
128
|
+
</a>
|
|
129
|
+
</nav>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="sc-timeline-grid">
|
|
132
|
+
<div className="sc-timeline-row sc-timeline-months">
|
|
133
|
+
<div className="sc-timeline-label" aria-hidden="true" />
|
|
134
|
+
<div className="sc-timeline-track">
|
|
135
|
+
{columns.map((month, index) => (
|
|
136
|
+
<span
|
|
137
|
+
key={month}
|
|
138
|
+
className="sc-timeline-month"
|
|
139
|
+
style={{ width: percent(shares[index] ?? 0) }}
|
|
140
|
+
>
|
|
141
|
+
{formatMonth(month, tag)}
|
|
142
|
+
</span>
|
|
143
|
+
))}
|
|
144
|
+
{todayAt !== undefined && (
|
|
145
|
+
<span
|
|
146
|
+
className="sc-timeline-today sc-timeline-today-mark"
|
|
147
|
+
style={{ left: percent(todayAt) }}
|
|
148
|
+
title={words.timeline.today}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
{bars.map(({ goal, from, to, begins, ends, point }) => {
|
|
154
|
+
const counts = goal.taskCounts;
|
|
155
|
+
const all =
|
|
156
|
+
counts.idea + counts.ready + counts.in_progress + counts.in_review + counts.done;
|
|
157
|
+
return (
|
|
158
|
+
<div key={goal.id} className={cx("sc-timeline-row", `sc-goal-${goal.state}`)}>
|
|
159
|
+
<div className="sc-timeline-label">
|
|
160
|
+
<a className="sc-timeline-goal" href={props.goalHref(goal)}>
|
|
161
|
+
{goal.title}
|
|
162
|
+
</a>
|
|
163
|
+
<GoalStateBadge state={goal.state} />
|
|
164
|
+
</div>
|
|
165
|
+
<div className="sc-timeline-track">
|
|
166
|
+
{columns.map((month, index) => (
|
|
167
|
+
<span
|
|
168
|
+
key={month}
|
|
169
|
+
className="sc-timeline-column"
|
|
170
|
+
style={{ width: percent(shares[index] ?? 0) }}
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
/>
|
|
173
|
+
))}
|
|
174
|
+
{todayAt !== undefined && (
|
|
175
|
+
<span
|
|
176
|
+
className="sc-timeline-today"
|
|
177
|
+
style={{ left: percent(todayAt) }}
|
|
178
|
+
aria-hidden="true"
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
<a
|
|
182
|
+
className={cx(
|
|
183
|
+
point ? "sc-timeline-point" : "sc-timeline-bar",
|
|
184
|
+
`sc-category-${goal.category?.color ?? "gray"}`,
|
|
185
|
+
begins && "sc-timeline-bar-begins",
|
|
186
|
+
ends && "sc-timeline-bar-ends",
|
|
187
|
+
)}
|
|
188
|
+
style={
|
|
189
|
+
point
|
|
190
|
+
? { left: percent(from) }
|
|
191
|
+
: { left: percent(from), width: percent(to - from) }
|
|
192
|
+
}
|
|
193
|
+
href={props.goalHref(goal)}
|
|
194
|
+
aria-label={`${goal.title}: ${words.progress(counts.done, all)}`}
|
|
195
|
+
>
|
|
196
|
+
{!point && (
|
|
197
|
+
<>
|
|
198
|
+
<span
|
|
199
|
+
className="sc-timeline-fill"
|
|
200
|
+
style={{ width: percent(doneShare(goal)) }}
|
|
201
|
+
aria-hidden="true"
|
|
202
|
+
/>
|
|
203
|
+
<span className="sc-timeline-words">
|
|
204
|
+
{counts.done}/{all}
|
|
205
|
+
</span>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</a>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
})}
|
|
213
|
+
</div>
|
|
214
|
+
{bars.length === 0 && <p className="sc-muted">{words.timeline.nothing}</p>}
|
|
215
|
+
{(props.categories ?? []).length > 0 && (
|
|
216
|
+
<dl className="sc-calendar-legend" aria-label={words.timeline.legend}>
|
|
217
|
+
{(props.categories ?? []).map((category) => (
|
|
218
|
+
<div key={category.key}>
|
|
219
|
+
<dt>
|
|
220
|
+
<span className={cx("sc-calendar-dot", `sc-category-${category.color}`)} />
|
|
221
|
+
</dt>
|
|
222
|
+
<dd>{category.name}</dd>
|
|
223
|
+
</div>
|
|
224
|
+
))}
|
|
225
|
+
</dl>
|
|
226
|
+
)}
|
|
227
|
+
{undated.length > 0 && (
|
|
228
|
+
<p className="sc-timeline-undated">
|
|
229
|
+
<span className="sc-muted">{words.timeline.noDays}: </span>
|
|
230
|
+
{undated.map((goal, index) => (
|
|
231
|
+
<span key={goal.id}>
|
|
232
|
+
{index > 0 && ", "}
|
|
233
|
+
<a className="sc-timeline-goal" href={props.goalHref(goal)}>
|
|
234
|
+
{goal.title}
|
|
235
|
+
</a>
|
|
236
|
+
</span>
|
|
237
|
+
))}
|
|
238
|
+
</p>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -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
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type ProjectListProps,
|
|
54
54
|
ProjectMark,
|
|
55
55
|
} from "./components/projects.js";
|
|
56
|
+
import { GoalTimeline } from "./components/roadmap.js";
|
|
56
57
|
import { RulesForm, RulesView } from "./components/rules.js";
|
|
57
58
|
import {
|
|
58
59
|
type Brand,
|
|
@@ -106,6 +107,7 @@ import {
|
|
|
106
107
|
projectHref,
|
|
107
108
|
projectSettingsHref,
|
|
108
109
|
type Route,
|
|
110
|
+
roadmapHref,
|
|
109
111
|
settingsHref,
|
|
110
112
|
signInFailureOf,
|
|
111
113
|
taskHref,
|
|
@@ -702,6 +704,7 @@ function ProjectPage(props: {
|
|
|
702
704
|
categories={data.categories}
|
|
703
705
|
goals={data.goals}
|
|
704
706
|
taskHref={href}
|
|
707
|
+
goalHref={(goal) => goalHref(key, goal.id)}
|
|
705
708
|
monthHref={(wanted) => calendarHref(key, wanted, route.filter)}
|
|
706
709
|
/>
|
|
707
710
|
<div className="sc-calendar-below">
|
|
@@ -765,6 +768,7 @@ function ProjectPage(props: {
|
|
|
765
768
|
{making && (
|
|
766
769
|
<section className="sc-panel sc-panel-form" aria-label={roadmap.setGoalAria}>
|
|
767
770
|
<GoalForm
|
|
771
|
+
categories={data.categories}
|
|
768
772
|
busy={busy}
|
|
769
773
|
onSubmit={(input) =>
|
|
770
774
|
void act(async () => {
|
|
@@ -776,6 +780,18 @@ function ProjectPage(props: {
|
|
|
776
780
|
/>
|
|
777
781
|
</section>
|
|
778
782
|
)}
|
|
783
|
+
{data.goals.length > 0 && (
|
|
784
|
+
<GoalTimeline
|
|
785
|
+
goals={data.goals}
|
|
786
|
+
from={
|
|
787
|
+
route.name === "roadmap" && route.from !== undefined ? route.from : monthOf(today())
|
|
788
|
+
}
|
|
789
|
+
today={today()}
|
|
790
|
+
categories={data.categories}
|
|
791
|
+
goalHref={(goal) => goalHref(key, goal.id)}
|
|
792
|
+
fromHref={(month) => roadmapHref(key, month)}
|
|
793
|
+
/>
|
|
794
|
+
)}
|
|
779
795
|
<components.GoalList
|
|
780
796
|
goals={data.goals}
|
|
781
797
|
goalHref={(goal) => goalHref(key, goal.id)}
|
|
@@ -820,6 +836,7 @@ function ProjectPage(props: {
|
|
|
820
836
|
<section className="sc-panel sc-panel-form" aria-label={roadmap.editAria}>
|
|
821
837
|
<GoalForm
|
|
822
838
|
goal={goal}
|
|
839
|
+
categories={data.categories}
|
|
823
840
|
busy={busy}
|
|
824
841
|
onSubmit={(input) =>
|
|
825
842
|
void act(async () => {
|
|
@@ -1451,6 +1468,7 @@ function PersonAgentsPage(props: {
|
|
|
1451
1468
|
<components.TokenList
|
|
1452
1469
|
tokens={tokens}
|
|
1453
1470
|
busy={busy}
|
|
1471
|
+
consoleVersion={data.me?.console?.version}
|
|
1454
1472
|
onRevoke={(token) =>
|
|
1455
1473
|
void act(async () => {
|
|
1456
1474
|
await data.actions.revokePersonToken(id, token.id);
|
|
@@ -1763,6 +1781,7 @@ function WorkspacePage(props: {
|
|
|
1763
1781
|
<components.TokenList
|
|
1764
1782
|
tokens={data.tokens}
|
|
1765
1783
|
busy={busy}
|
|
1784
|
+
consoleVersion={data.me?.console?.version}
|
|
1766
1785
|
onRevoke={(token) => void act(() => data.actions.revokeToken(token.id))}
|
|
1767
1786
|
empty={<EmptyState title={agents.empty.title} body={agents.empty.body} />}
|
|
1768
1787
|
/>
|
package/src/data.ts
CHANGED
|
@@ -267,7 +267,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
267
267
|
scope.get(signal),
|
|
268
268
|
scope.tasks(undefined, signal),
|
|
269
269
|
scope.decisions(undefined, signal),
|
|
270
|
-
scope.goals(signal),
|
|
270
|
+
scope.goals({}, signal),
|
|
271
271
|
scope.runs(undefined, signal),
|
|
272
272
|
scope.members(signal),
|
|
273
273
|
]);
|
package/src/i18n/en.ts
CHANGED
|
@@ -385,16 +385,33 @@ export const en = {
|
|
|
385
385
|
inProgress: (n: number) => `${n} in progress`,
|
|
386
386
|
noTasks: "No task works towards it yet.",
|
|
387
387
|
tasks: "Tasks",
|
|
388
|
+
category: "Category",
|
|
389
|
+
noCategory: "No category",
|
|
390
|
+
starts: "Starts",
|
|
391
|
+
noStart: "No start day",
|
|
388
392
|
target: "Target",
|
|
389
393
|
noTarget: "No target day",
|
|
390
394
|
setBy: "Set by",
|
|
391
395
|
state: "State",
|
|
392
396
|
nothingMore: "Nothing more was written about it.",
|
|
397
|
+
timeline: {
|
|
398
|
+
title: "The months",
|
|
399
|
+
legend: "Category colours",
|
|
400
|
+
earlier: "A month earlier",
|
|
401
|
+
later: "A month later",
|
|
402
|
+
thisMonth: "This month",
|
|
403
|
+
today: "Today",
|
|
404
|
+
nothing: "No goal is planned in these months.",
|
|
405
|
+
noDays: "No day set",
|
|
406
|
+
},
|
|
393
407
|
form: {
|
|
394
408
|
title: "Title",
|
|
395
409
|
body: "What reaching it means",
|
|
396
410
|
bodyPlaceholder: "What is true once the goal is reached, in Markdown.",
|
|
397
411
|
state: "State",
|
|
412
|
+
category: "Category",
|
|
413
|
+
noCategory: "No category",
|
|
414
|
+
startsOn: "Start day",
|
|
398
415
|
dueOn: "Target day",
|
|
399
416
|
set: "Set the goal",
|
|
400
417
|
},
|
|
@@ -734,6 +751,13 @@ export const en = {
|
|
|
734
751
|
goodUntil: "Good until",
|
|
735
752
|
notActive: "Not active yet",
|
|
736
753
|
lastActive: "Last active",
|
|
754
|
+
command: (version: string) => `console ${version}`,
|
|
755
|
+
commandVerdict: {
|
|
756
|
+
behind: "an update is available",
|
|
757
|
+
ahead: "newer than this console",
|
|
758
|
+
outdated: "must be updated",
|
|
759
|
+
too_new: "too new for this console",
|
|
760
|
+
},
|
|
737
761
|
disconnect: "Disconnect",
|
|
738
762
|
form: {
|
|
739
763
|
name: "Name",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -354,16 +354,33 @@ export const ko: Messages = {
|
|
|
354
354
|
inProgress: (n) => `${n}개 진행 중`,
|
|
355
355
|
noTasks: "아직 이 목표를 향한 작업이 없습니다.",
|
|
356
356
|
tasks: "작업",
|
|
357
|
+
category: "카테고리",
|
|
358
|
+
noCategory: "카테고리 없음",
|
|
359
|
+
starts: "시작일",
|
|
360
|
+
noStart: "시작일 없음",
|
|
357
361
|
target: "목표일",
|
|
358
362
|
noTarget: "목표일 없음",
|
|
359
363
|
setBy: "세운 사람",
|
|
360
364
|
state: "상태",
|
|
361
365
|
nothingMore: "더 적힌 내용이 없습니다.",
|
|
366
|
+
timeline: {
|
|
367
|
+
title: "달별 로드맵",
|
|
368
|
+
legend: "카테고리 색",
|
|
369
|
+
earlier: "한 달 앞으로",
|
|
370
|
+
later: "한 달 뒤로",
|
|
371
|
+
thisMonth: "이번 달",
|
|
372
|
+
today: "오늘",
|
|
373
|
+
nothing: "이 기간에 잡힌 목표가 없습니다.",
|
|
374
|
+
noDays: "날짜 없음",
|
|
375
|
+
},
|
|
362
376
|
form: {
|
|
363
377
|
title: "제목",
|
|
364
378
|
body: "달성의 의미",
|
|
365
379
|
bodyPlaceholder: "목표를 이루면 무엇이 참이 되는지, Markdown으로.",
|
|
366
380
|
state: "상태",
|
|
381
|
+
category: "카테고리",
|
|
382
|
+
noCategory: "카테고리 없음",
|
|
383
|
+
startsOn: "시작일",
|
|
367
384
|
dueOn: "목표일",
|
|
368
385
|
set: "목표 세우기",
|
|
369
386
|
},
|
|
@@ -698,6 +715,13 @@ export const ko: Messages = {
|
|
|
698
715
|
goodUntil: "만료",
|
|
699
716
|
notActive: "아직 활동 없음",
|
|
700
717
|
lastActive: "마지막 활동",
|
|
718
|
+
command: (version) => `console ${version}`,
|
|
719
|
+
commandVerdict: {
|
|
720
|
+
behind: "업데이트할 수 있음",
|
|
721
|
+
ahead: "이 콘솔보다 새 버전",
|
|
722
|
+
outdated: "업데이트 필요",
|
|
723
|
+
too_new: "이 콘솔에는 너무 새 버전",
|
|
724
|
+
},
|
|
701
725
|
disconnect: "연결 끊기",
|
|
702
726
|
form: {
|
|
703
727
|
name: "이름",
|
package/src/index.ts
CHANGED
|
@@ -100,6 +100,13 @@ export {
|
|
|
100
100
|
ProjectRoleBadge,
|
|
101
101
|
VISIBILITY_LABELS,
|
|
102
102
|
} from "./components/projects.js";
|
|
103
|
+
export {
|
|
104
|
+
DEFAULT_TIMELINE_MONTHS,
|
|
105
|
+
GoalTimeline,
|
|
106
|
+
type GoalTimelineProps,
|
|
107
|
+
type TimelineBar,
|
|
108
|
+
timelineBars,
|
|
109
|
+
} from "./components/roadmap.js";
|
|
103
110
|
export {
|
|
104
111
|
RulesForm,
|
|
105
112
|
type RulesFormProps,
|
|
@@ -230,6 +237,7 @@ export {
|
|
|
230
237
|
projectHref,
|
|
231
238
|
projectSettingsHref,
|
|
232
239
|
type Route,
|
|
240
|
+
roadmapHref,
|
|
233
241
|
settingsHref,
|
|
234
242
|
signInFailureOf,
|
|
235
243
|
type TaskFormKind,
|
package/src/router.ts
CHANGED
|
@@ -59,7 +59,8 @@ export type Route =
|
|
|
59
59
|
readonly month: string | undefined;
|
|
60
60
|
readonly filter: BoardFilter;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
/** The roadmap of a project: the months shown from the one named, or from this one. */
|
|
63
|
+
| { readonly name: "roadmap"; readonly project: string; readonly from: string | undefined }
|
|
63
64
|
| { readonly name: "new-goal"; readonly project: string }
|
|
64
65
|
| {
|
|
65
66
|
readonly name: "goal";
|
|
@@ -345,7 +346,11 @@ export function matchRoute(pathname: string, search = ""): Route {
|
|
|
345
346
|
return { name: "calendar", project, month: month ?? undefined, filter: boardFilterOf(params) };
|
|
346
347
|
}
|
|
347
348
|
if (page === "roadmap") {
|
|
348
|
-
|
|
349
|
+
const from = params.get("from");
|
|
350
|
+
if (from !== null && !isMonth(from)) {
|
|
351
|
+
return NOT_FOUND;
|
|
352
|
+
}
|
|
353
|
+
return { name: "roadmap", project, from: from ?? undefined };
|
|
349
354
|
}
|
|
350
355
|
if (page === "settings") {
|
|
351
356
|
return {
|
|
@@ -455,6 +460,12 @@ export function customPageHref(page: string, project?: string): string {
|
|
|
455
460
|
}
|
|
456
461
|
|
|
457
462
|
/** Where a goal is set: the form. */
|
|
463
|
+
/** Where a project's roadmap is, from a month on, or from this one when none is named. */
|
|
464
|
+
export function roadmapHref(key: string, from?: string): string {
|
|
465
|
+
const base = projectHref(key, "roadmap");
|
|
466
|
+
return from === undefined ? base : `${base}?from=${from}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
458
469
|
export function newGoalHref(key: string): string {
|
|
459
470
|
return `${PATHS.project}/${key}/roadmap/new`;
|
|
460
471
|
}
|
package/src/schemas.ts
CHANGED
|
@@ -196,6 +196,13 @@ export const restMeSchema = z.object({
|
|
|
196
196
|
/** The language the person chose for the pages (ADR-0012), or `null` for the browser's. */
|
|
197
197
|
language: z.nullable(languageTag),
|
|
198
198
|
signIn: z.array(restProviderSchema),
|
|
199
|
+
/**
|
|
200
|
+
* The console's version and the range of command versions it takes, as npm spells one
|
|
201
|
+
* (ADR-0025): what a console of a person's own compares the package it was built on with.
|
|
202
|
+
* Every console answers it from 0.1.18; read as optional, so that a command or a console
|
|
203
|
+
* built on this package still reads an older console's answer, and says it is older.
|
|
204
|
+
*/
|
|
205
|
+
console: z.optional(z.object({ version: z.string(), clientRange: z.string() })),
|
|
199
206
|
});
|
|
200
207
|
export type RestMe = z.infer<typeof restMeSchema>;
|
|
201
208
|
|
|
@@ -485,6 +492,12 @@ export const restGoalSchema = z.object({
|
|
|
485
492
|
/** Markdown: what reaching the goal means. Shown as text or rendered to elements, never as HTML. */
|
|
486
493
|
body: z.string(),
|
|
487
494
|
state: z.enum(GOAL_STATES),
|
|
495
|
+
/** The category the goal is sorted into, as the workspace keeps it, or `null` (ADR-0026). */
|
|
496
|
+
category: z.nullable(
|
|
497
|
+
z.object({ key: z.string(), name: z.string(), color: z.enum(CATEGORY_COLORS) }),
|
|
498
|
+
),
|
|
499
|
+
/** The plan (ADR-0026): the day the work towards the goal is meant to begin, or `null`. */
|
|
500
|
+
startsOn: z.nullable(day),
|
|
488
501
|
/** The day the goal is meant to be reached by, or `null`. */
|
|
489
502
|
dueOn: z.nullable(day),
|
|
490
503
|
createdBy: restPersonSchema,
|
|
@@ -505,6 +518,9 @@ export const restGoalInputSchema = z.object({
|
|
|
505
518
|
body: z.optional(body),
|
|
506
519
|
/** Left out: `planned`. */
|
|
507
520
|
state: z.optional(z.enum(GOAL_STATES)),
|
|
521
|
+
/** The key of a category of the workspace, or `null` for none. */
|
|
522
|
+
category: z.optional(z.nullable(categoryKey)),
|
|
523
|
+
startsOn: z.optional(z.nullable(day)),
|
|
508
524
|
dueOn: z.optional(z.nullable(day)),
|
|
509
525
|
});
|
|
510
526
|
export type RestGoalInput = z.infer<typeof restGoalInputSchema>;
|
|
@@ -514,6 +530,8 @@ export const restGoalPatchSchema = z.object({
|
|
|
514
530
|
title: z.optional(line(MAX_TITLE_LENGTH)),
|
|
515
531
|
body: z.optional(body),
|
|
516
532
|
state: z.optional(z.enum(GOAL_STATES)),
|
|
533
|
+
category: z.optional(z.nullable(categoryKey)),
|
|
534
|
+
startsOn: z.optional(z.nullable(day)),
|
|
517
535
|
dueOn: z.optional(z.nullable(day)),
|
|
518
536
|
});
|
|
519
537
|
export type RestGoalPatch = z.infer<typeof restGoalPatchSchema>;
|
|
@@ -791,6 +809,8 @@ export const restTokenSchema = z.object({
|
|
|
791
809
|
expiresAt: z.nullable(instant),
|
|
792
810
|
/** When it was last presented, or `null` if never. */
|
|
793
811
|
lastUsedAt: z.nullable(instant),
|
|
812
|
+
/** The version of the command that last presented it (`0.1.18`), or `null` when the last to present it named none. */
|
|
813
|
+
client: z.nullable(z.string()),
|
|
794
814
|
});
|
|
795
815
|
export type RestToken = z.infer<typeof restTokenSchema>;
|
|
796
816
|
|