@skillcdn/console 0.1.24 → 0.3.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 +6 -5
- package/dist/api.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -1
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +64 -65
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +4 -28
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +7 -261
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +6 -6
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/pack.d.ts +46 -0
- package/dist/cli/pack.d.ts.map +1 -0
- package/dist/cli/pack.js +250 -0
- package/dist/cli/pack.js.map +1 -0
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/decision-list.d.ts +13 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +74 -29
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/paged.d.ts +24 -0
- package/dist/components/paged.d.ts.map +1 -1
- package/dist/components/paged.js +40 -4
- package/dist/components/paged.js.map +1 -1
- package/dist/components/skills.d.ts.map +1 -1
- package/dist/components/skills.js +8 -4
- package/dist/components/skills.js.map +1 -1
- package/dist/components/ui.d.ts +2 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +7 -2
- package/dist/components/ui.js.map +1 -1
- package/dist/console.css +112 -5
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +2 -2
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -2
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +13 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +15 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +15 -0
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- 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 +13 -7
- package/dist/router.js.map +1 -1
- package/dist/schemas.d.ts +321 -159
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +61 -154
- package/dist/schemas.js.map +1 -1
- package/docs/specs/cli.md +3 -3
- package/docs/specs/rest.md +5 -13
- package/package.json +2 -1
- package/skills/deploying-a-console/SKILL.md +2 -2
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +2 -7
- package/src/cli/cli.ts +92 -76
- package/src/cli/format.ts +10 -328
- package/src/cli/help.ts +7 -6
- package/src/cli/pack.ts +356 -0
- package/src/compat.ts +1 -1
- package/src/components/decision-list.tsx +227 -94
- package/src/components/paged.tsx +72 -3
- package/src/components/skills.tsx +51 -33
- package/src/components/ui.tsx +16 -3
- package/src/console.tsx +3 -0
- package/src/data.ts +32 -2
- package/src/i18n/en.ts +18 -0
- package/src/i18n/ko.ts +17 -0
- package/src/index.ts +9 -1
- package/src/router.ts +20 -9
- package/src/schemas.ts +76 -180
- package/src/styles/console.css +112 -5
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
agentLabel,
|
|
4
4
|
MAX_BODY_LENGTH,
|
|
5
5
|
MAX_NOTE_LENGTH,
|
|
6
|
+
MAX_OPTION_LABEL_LENGTH,
|
|
6
7
|
type RestAnswerInput,
|
|
7
8
|
type RestDecision,
|
|
8
9
|
type RestDecisionPatch,
|
|
@@ -10,8 +11,8 @@ import {
|
|
|
10
11
|
} from "../api.js";
|
|
11
12
|
import { useWords } from "../i18n/index.js";
|
|
12
13
|
import { Markdown } from "./markdown.js";
|
|
13
|
-
import {
|
|
14
|
-
import { Button, cx, PersonChip, Time } from "./ui.js";
|
|
14
|
+
import { Pager, pageOf, RECORD_PAGE_SIZE } from "./paged.js";
|
|
15
|
+
import { Badge, Button, cx, PersonChip, Time } from "./ui.js";
|
|
15
16
|
|
|
16
17
|
// The decisions: the ones that wait, each a card with the way to answer it, since they are what
|
|
17
18
|
// a person came for; and the ones answered, the record, a row each, since they are read back
|
|
@@ -35,6 +36,131 @@ export interface DecisionCardProps {
|
|
|
35
36
|
readonly busy?: boolean | undefined;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/** Who asked, and when: the person, through their agent when a run asked. */
|
|
40
|
+
function Asked(props: { readonly decision: RestDecision; readonly day?: boolean }) {
|
|
41
|
+
const words = useWords();
|
|
42
|
+
const { decision } = props;
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<PersonChip person={decision.raisedBy} />{" "}
|
|
46
|
+
{decision.run === null
|
|
47
|
+
? words.decisions.asked
|
|
48
|
+
: words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
|
|
49
|
+
<Time iso={decision.createdAt} precision={props.day === true ? "day" : "minute"} />
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What was chosen: the option's label, marked when it is the person's own words. */
|
|
55
|
+
function Chosen(props: { readonly decision: RestDecision }) {
|
|
56
|
+
const words = useWords();
|
|
57
|
+
const { decision } = props;
|
|
58
|
+
const chosen = decision.options.find((option) => option.id === decision.answer?.option);
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<span>{chosen?.label ?? decision.answer?.option ?? ""}</span>
|
|
62
|
+
{chosen?.own === true && <Badge>{words.decisions.ownAnswer}</Badge>}
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The way to answer: one of the options, or a line of one's own, which is chosen by writing
|
|
69
|
+
* on it; and a word about either.
|
|
70
|
+
*/
|
|
71
|
+
function AnswerForm(props: {
|
|
72
|
+
readonly decision: RestDecision;
|
|
73
|
+
readonly onAnswer: (decision: RestDecision, input: RestAnswerInput) => void;
|
|
74
|
+
readonly busy: boolean | undefined;
|
|
75
|
+
}) {
|
|
76
|
+
const words = useWords();
|
|
77
|
+
const { decision } = props;
|
|
78
|
+
const [chosen, setChosen] = useState<string | undefined>(undefined);
|
|
79
|
+
const [own, setOwn] = useState(false);
|
|
80
|
+
const [ownText, setOwnText] = useState("");
|
|
81
|
+
const [note, setNote] = useState("");
|
|
82
|
+
const chooseOwn = () => {
|
|
83
|
+
setOwn(true);
|
|
84
|
+
setChosen(undefined);
|
|
85
|
+
};
|
|
86
|
+
const answer: RestAnswerInput | undefined = own
|
|
87
|
+
? ownText.trim().length > 0
|
|
88
|
+
? { own: ownText.trim() }
|
|
89
|
+
: undefined
|
|
90
|
+
: chosen === undefined
|
|
91
|
+
? undefined
|
|
92
|
+
: { option: chosen };
|
|
93
|
+
const name = `decision-${decision.id}`;
|
|
94
|
+
return (
|
|
95
|
+
<form
|
|
96
|
+
className="sc-answer-form"
|
|
97
|
+
onSubmit={(event) => {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
if (answer !== undefined) {
|
|
100
|
+
props.onAnswer(decision, {
|
|
101
|
+
...answer,
|
|
102
|
+
...(note.trim() === "" ? {} : { note: note.trim() }),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<div className="sc-options-list" role="radiogroup" aria-label={words.decisions.options}>
|
|
108
|
+
{decision.options.map((option) => (
|
|
109
|
+
<label
|
|
110
|
+
key={option.id}
|
|
111
|
+
className={cx("sc-option", chosen === option.id && "sc-option-chosen")}
|
|
112
|
+
>
|
|
113
|
+
<input
|
|
114
|
+
type="radio"
|
|
115
|
+
name={name}
|
|
116
|
+
value={option.id}
|
|
117
|
+
checked={chosen === option.id}
|
|
118
|
+
onChange={() => {
|
|
119
|
+
setChosen(option.id);
|
|
120
|
+
setOwn(false);
|
|
121
|
+
}}
|
|
122
|
+
/>
|
|
123
|
+
<span>{option.label}</span>
|
|
124
|
+
</label>
|
|
125
|
+
))}
|
|
126
|
+
<label className={cx("sc-option", "sc-option-own", own && "sc-option-chosen")}>
|
|
127
|
+
<input type="radio" name={name} value="own" checked={own} onChange={chooseOwn} />
|
|
128
|
+
<span>{words.decisions.own}</span>
|
|
129
|
+
<input
|
|
130
|
+
className="sc-input"
|
|
131
|
+
aria-label={words.decisions.own}
|
|
132
|
+
placeholder={words.decisions.ownPlaceholder}
|
|
133
|
+
maxLength={MAX_OPTION_LABEL_LENGTH}
|
|
134
|
+
value={ownText}
|
|
135
|
+
onFocus={chooseOwn}
|
|
136
|
+
onChange={(event) => {
|
|
137
|
+
setOwnText(event.target.value);
|
|
138
|
+
chooseOwn();
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
</label>
|
|
142
|
+
</div>
|
|
143
|
+
<input
|
|
144
|
+
className="sc-input"
|
|
145
|
+
placeholder={words.decisions.notePlaceholder}
|
|
146
|
+
maxLength={MAX_NOTE_LENGTH}
|
|
147
|
+
value={note}
|
|
148
|
+
onChange={(event) => setNote(event.target.value)}
|
|
149
|
+
/>
|
|
150
|
+
<div className="sc-form-actions">
|
|
151
|
+
<Button
|
|
152
|
+
type="submit"
|
|
153
|
+
variant="primary"
|
|
154
|
+
size="sm"
|
|
155
|
+
disabled={answer === undefined || props.busy === true}
|
|
156
|
+
>
|
|
157
|
+
{words.decisions.answer}
|
|
158
|
+
</Button>
|
|
159
|
+
</div>
|
|
160
|
+
</form>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
38
164
|
/** What followed the decision: shown, and written or changed by whoever may. */
|
|
39
165
|
function Outcome(props: {
|
|
40
166
|
readonly decision: RestDecision;
|
|
@@ -109,11 +235,7 @@ function Outcome(props: {
|
|
|
109
235
|
export function DecisionCard(props: DecisionCardProps) {
|
|
110
236
|
const words = useWords();
|
|
111
237
|
const { decision, task } = props;
|
|
112
|
-
const [chosen, setChosen] = useState<string | undefined>(undefined);
|
|
113
|
-
const [note, setNote] = useState("");
|
|
114
238
|
const answered = decision.answer !== null;
|
|
115
|
-
const chosenLabel = (id: string) =>
|
|
116
|
-
decision.options.find((option) => option.id === id)?.label ?? id;
|
|
117
239
|
|
|
118
240
|
return (
|
|
119
241
|
<article id={decision.id} className={cx("sc-decision", answered && "sc-decision-answered")}>
|
|
@@ -126,11 +248,7 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
126
248
|
)}
|
|
127
249
|
</h3>
|
|
128
250
|
<p className="sc-decision-meta">
|
|
129
|
-
<
|
|
130
|
-
{decision.run === null
|
|
131
|
-
? words.decisions.asked
|
|
132
|
-
: words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
|
|
133
|
-
<Time iso={decision.createdAt} />
|
|
251
|
+
<Asked decision={decision} />
|
|
134
252
|
{task !== undefined && (
|
|
135
253
|
<>
|
|
136
254
|
{" "}
|
|
@@ -156,7 +274,9 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
156
274
|
)}
|
|
157
275
|
{decision.answer !== null ? (
|
|
158
276
|
<div className="sc-answer">
|
|
159
|
-
<p className="sc-answer-option">
|
|
277
|
+
<p className="sc-answer-option">
|
|
278
|
+
<Chosen decision={decision} />
|
|
279
|
+
</p>
|
|
160
280
|
{decision.answer.note !== null && (
|
|
161
281
|
<div className="sc-answer-note">
|
|
162
282
|
<Markdown source={decision.answer.note} docHref={props.docHref} />
|
|
@@ -170,53 +290,7 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
170
290
|
) : props.onAnswer === undefined ? (
|
|
171
291
|
<p className="sc-decision-waiting">{words.decisions.waitingForPerson}</p>
|
|
172
292
|
) : (
|
|
173
|
-
<
|
|
174
|
-
className="sc-answer-form"
|
|
175
|
-
onSubmit={(event) => {
|
|
176
|
-
event.preventDefault();
|
|
177
|
-
if (chosen !== undefined) {
|
|
178
|
-
props.onAnswer?.(decision, {
|
|
179
|
-
option: chosen,
|
|
180
|
-
...(note.trim() === "" ? {} : { note: note.trim() }),
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}}
|
|
184
|
-
>
|
|
185
|
-
<div className="sc-options-list" role="radiogroup" aria-label={words.decisions.options}>
|
|
186
|
-
{decision.options.map((option) => (
|
|
187
|
-
<label
|
|
188
|
-
key={option.id}
|
|
189
|
-
className={cx("sc-option", chosen === option.id && "sc-option-chosen")}
|
|
190
|
-
>
|
|
191
|
-
<input
|
|
192
|
-
type="radio"
|
|
193
|
-
name={`decision-${decision.id}`}
|
|
194
|
-
value={option.id}
|
|
195
|
-
checked={chosen === option.id}
|
|
196
|
-
onChange={() => setChosen(option.id)}
|
|
197
|
-
/>
|
|
198
|
-
<span>{option.label}</span>
|
|
199
|
-
</label>
|
|
200
|
-
))}
|
|
201
|
-
</div>
|
|
202
|
-
<input
|
|
203
|
-
className="sc-input"
|
|
204
|
-
placeholder={words.decisions.notePlaceholder}
|
|
205
|
-
maxLength={MAX_NOTE_LENGTH}
|
|
206
|
-
value={note}
|
|
207
|
-
onChange={(event) => setNote(event.target.value)}
|
|
208
|
-
/>
|
|
209
|
-
<div className="sc-form-actions">
|
|
210
|
-
<Button
|
|
211
|
-
type="submit"
|
|
212
|
-
variant="primary"
|
|
213
|
-
size="sm"
|
|
214
|
-
disabled={chosen === undefined || props.busy === true}
|
|
215
|
-
>
|
|
216
|
-
{words.decisions.answer}
|
|
217
|
-
</Button>
|
|
218
|
-
</div>
|
|
219
|
-
</form>
|
|
293
|
+
<AnswerForm decision={decision} onAnswer={props.onAnswer} busy={props.busy} />
|
|
220
294
|
)}
|
|
221
295
|
{answered && (
|
|
222
296
|
<Outcome
|
|
@@ -238,6 +312,9 @@ export interface DecisionListProps {
|
|
|
238
312
|
readonly docHref?: ((path: string) => string) | undefined;
|
|
239
313
|
/** Where each decision's own page is. */
|
|
240
314
|
readonly decisionHref?: ((decision: RestDecision) => string) | undefined;
|
|
315
|
+
/** The page of the record shown, from 1, and where each page is (ADR-0010); left out, the record turns its pages in place. */
|
|
316
|
+
readonly page?: number | undefined;
|
|
317
|
+
readonly pageHref?: ((page: number) => string) | undefined;
|
|
241
318
|
readonly onAnswer?: ((decision: RestDecision, input: RestAnswerInput) => void) | undefined;
|
|
242
319
|
readonly onUpdate?: ((decision: RestDecision, patch: RestDecisionPatch) => void) | undefined;
|
|
243
320
|
readonly busy?: boolean | undefined;
|
|
@@ -251,48 +328,101 @@ export interface DecisionLogProps {
|
|
|
251
328
|
readonly taskHref?: ((task: RestTask) => string) | undefined;
|
|
252
329
|
/** Where each decision's own page is, with its context, its rationale and what followed. */
|
|
253
330
|
readonly decisionHref?: ((decision: RestDecision) => string) | undefined;
|
|
331
|
+
/** Where a document the word given with an answer links to is read, by its path. */
|
|
332
|
+
readonly docHref?: ((path: string) => string) | undefined;
|
|
333
|
+
/** The page shown, from 1; the first when left out, the last for one past the end. Read with `pageHref`. */
|
|
334
|
+
readonly page?: number | undefined;
|
|
335
|
+
/** Where each page of the record is, so that a page has an address; left out, the pages turn in place. */
|
|
336
|
+
readonly pageHref?: ((page: number) => string) | undefined;
|
|
254
337
|
}
|
|
255
338
|
|
|
256
|
-
/**
|
|
339
|
+
/** One row of the record: what was asked, by whom and when; what was answered, with the word given, by whom and when. */
|
|
340
|
+
function RecordRow(props: {
|
|
341
|
+
readonly decision: RestDecision;
|
|
342
|
+
readonly task: RestTask | undefined;
|
|
343
|
+
readonly taskHref: ((task: RestTask) => string) | undefined;
|
|
344
|
+
readonly href: string | undefined;
|
|
345
|
+
readonly docHref: ((path: string) => string) | undefined;
|
|
346
|
+
}) {
|
|
347
|
+
const words = useWords();
|
|
348
|
+
const { decision, task } = props;
|
|
349
|
+
const { answer } = decision;
|
|
350
|
+
return (
|
|
351
|
+
<li className="sc-decision-record-row">
|
|
352
|
+
<div className="sc-decision-record-asked">
|
|
353
|
+
<p className="sc-decision-record-question">
|
|
354
|
+
{props.href === undefined ? (
|
|
355
|
+
decision.question
|
|
356
|
+
) : (
|
|
357
|
+
<a href={props.href}>{decision.question}</a>
|
|
358
|
+
)}
|
|
359
|
+
{task !== undefined &&
|
|
360
|
+
(props.taskHref === undefined ? (
|
|
361
|
+
<span className="sc-card-number">#{task.number}</span>
|
|
362
|
+
) : (
|
|
363
|
+
<a className="sc-card-number" href={props.taskHref(task)}>
|
|
364
|
+
#{task.number}
|
|
365
|
+
</a>
|
|
366
|
+
))}
|
|
367
|
+
</p>
|
|
368
|
+
<p className="sc-decision-record-meta">
|
|
369
|
+
<Asked decision={decision} day />
|
|
370
|
+
</p>
|
|
371
|
+
</div>
|
|
372
|
+
{answer !== null && (
|
|
373
|
+
<div className="sc-decision-record-answer">
|
|
374
|
+
<p className="sc-decision-record-chosen">
|
|
375
|
+
<Chosen decision={decision} />
|
|
376
|
+
</p>
|
|
377
|
+
{answer.note !== null && (
|
|
378
|
+
<div className="sc-decision-record-note">
|
|
379
|
+
<Markdown source={answer.note} docHref={props.docHref} />
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
<p className="sc-decision-record-meta">
|
|
383
|
+
<PersonChip person={answer.by} /> {words.decisions.answeredBy}{" "}
|
|
384
|
+
<Time iso={answer.at} precision="day" />
|
|
385
|
+
</p>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</li>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The record: one row per decision answered, what was asked and by whom, what was chosen with
|
|
394
|
+
* the word given and by whom; twenty to a page, the page named by the address or turned here.
|
|
395
|
+
*/
|
|
257
396
|
export function DecisionLog(props: DecisionLogProps) {
|
|
258
397
|
const words = useWords();
|
|
259
|
-
const
|
|
398
|
+
const [turned, setTurned] = useState(1);
|
|
399
|
+
const { shown, page, pages } = pageOf(
|
|
400
|
+
props.decisions,
|
|
401
|
+
props.pageHref === undefined ? turned : props.page,
|
|
402
|
+
RECORD_PAGE_SIZE,
|
|
403
|
+
);
|
|
260
404
|
return (
|
|
261
405
|
<>
|
|
262
|
-
<ol className="sc-decision-
|
|
263
|
-
{shown.map((decision) =>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
decision.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
{href === undefined ? decision.question : <a href={href}>{decision.question}</a>}
|
|
274
|
-
</span>
|
|
275
|
-
<span className="sc-decision-row-answer">{chosen}</span>
|
|
276
|
-
<span className="sc-decision-row-meta">
|
|
277
|
-
{decision.answer !== null && (
|
|
278
|
-
<>
|
|
279
|
-
<PersonChip person={decision.answer.by} /> <Time iso={decision.answer.at} />
|
|
280
|
-
</>
|
|
281
|
-
)}
|
|
282
|
-
{task !== undefined &&
|
|
283
|
-
(props.taskHref === undefined ? (
|
|
284
|
-
<span className="sc-card-number">#{task.number}</span>
|
|
285
|
-
) : (
|
|
286
|
-
<a className="sc-card-number" href={props.taskHref(task)}>
|
|
287
|
-
#{task.number}
|
|
288
|
-
</a>
|
|
289
|
-
))}
|
|
290
|
-
</span>
|
|
291
|
-
</li>
|
|
292
|
-
);
|
|
293
|
-
})}
|
|
406
|
+
<ol className="sc-decision-record" aria-label={words.decisions.record}>
|
|
407
|
+
{shown.map((decision) => (
|
|
408
|
+
<RecordRow
|
|
409
|
+
key={decision.id}
|
|
410
|
+
decision={decision}
|
|
411
|
+
task={decision.taskId === null ? undefined : props.tasks?.get(decision.taskId)}
|
|
412
|
+
taskHref={props.taskHref}
|
|
413
|
+
href={props.decisionHref?.(decision)}
|
|
414
|
+
docHref={props.docHref}
|
|
415
|
+
/>
|
|
416
|
+
))}
|
|
294
417
|
</ol>
|
|
295
|
-
<
|
|
418
|
+
<Pager
|
|
419
|
+
page={page}
|
|
420
|
+
pages={pages}
|
|
421
|
+
total={props.decisions.length}
|
|
422
|
+
pageSize={RECORD_PAGE_SIZE}
|
|
423
|
+
pageHref={props.pageHref}
|
|
424
|
+
onPage={setTurned}
|
|
425
|
+
/>
|
|
296
426
|
</>
|
|
297
427
|
);
|
|
298
428
|
}
|
|
@@ -332,6 +462,9 @@ export function DecisionList(props: DecisionListProps) {
|
|
|
332
462
|
tasks={props.tasks}
|
|
333
463
|
taskHref={props.taskHref}
|
|
334
464
|
decisionHref={props.decisionHref}
|
|
465
|
+
docHref={props.docHref}
|
|
466
|
+
page={props.page}
|
|
467
|
+
pageHref={props.pageHref}
|
|
335
468
|
/>
|
|
336
469
|
</section>
|
|
337
470
|
)}
|
package/src/components/paged.tsx
CHANGED
|
@@ -2,13 +2,17 @@ import { useState } from "react";
|
|
|
2
2
|
import { useWords } from "../i18n/index.js";
|
|
3
3
|
import { Button } from "./ui.js";
|
|
4
4
|
|
|
5
|
-
// A long list shown a page at a time, from what the page already holds: the
|
|
6
|
-
//
|
|
7
|
-
// asked of the server; the API's own pages (`limit`, `offset`)
|
|
5
|
+
// A long list shown a page at a time, from what the page already holds: the feed and the tasks
|
|
6
|
+
// with no day grow by a page on a click; the record of the decisions turns its pages, each with
|
|
7
|
+
// an address. Nothing is asked of the server either way; the API's own pages (`limit`, `offset`)
|
|
8
|
+
// are for the command and scripts.
|
|
8
9
|
|
|
9
10
|
/** How many items a list shows before it asks to be told to show more. */
|
|
10
11
|
export const PAGE_SIZE = 50;
|
|
11
12
|
|
|
13
|
+
/** How many rows the record of the decisions shows on a page: each says what was asked and what was answered. */
|
|
14
|
+
export const RECORD_PAGE_SIZE = 20;
|
|
15
|
+
|
|
12
16
|
/** The first `shown` items of a list, and the way to show the next page of it. */
|
|
13
17
|
export function usePaged<T>(
|
|
14
18
|
items: readonly T[],
|
|
@@ -37,3 +41,68 @@ export function ShowMore(props: { readonly rest: number; readonly onMore: () =>
|
|
|
37
41
|
</div>
|
|
38
42
|
);
|
|
39
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The page `page` of a list, counted from one and `pageSize` long, and which it is of how many:
|
|
47
|
+
* a page past the end, or no page at all, is the last or the first, and an empty list has one.
|
|
48
|
+
*/
|
|
49
|
+
export function pageOf<T>(
|
|
50
|
+
items: readonly T[],
|
|
51
|
+
page: number | undefined,
|
|
52
|
+
pageSize = RECORD_PAGE_SIZE,
|
|
53
|
+
): { readonly shown: readonly T[]; readonly page: number; readonly pages: number } {
|
|
54
|
+
const pages = Math.max(1, Math.ceil(items.length / pageSize));
|
|
55
|
+
const current =
|
|
56
|
+
page === undefined || !Number.isInteger(page) ? 1 : Math.min(Math.max(page, 1), pages);
|
|
57
|
+
return {
|
|
58
|
+
shown: items.slice((current - 1) * pageSize, current * pageSize),
|
|
59
|
+
page: current,
|
|
60
|
+
pages,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The way between the pages of a list: the page before, which items of how many are shown, the
|
|
66
|
+
* page after; nothing for a list of one page. Given `pageHref`, each page has an address and the
|
|
67
|
+
* steps lead there; else `onPage` turns the page in place.
|
|
68
|
+
*/
|
|
69
|
+
export function Pager(props: {
|
|
70
|
+
readonly page: number;
|
|
71
|
+
readonly pages: number;
|
|
72
|
+
readonly total: number;
|
|
73
|
+
readonly pageSize: number;
|
|
74
|
+
readonly pageHref?: ((page: number) => string) | undefined;
|
|
75
|
+
readonly onPage?: ((page: number) => void) | undefined;
|
|
76
|
+
}) {
|
|
77
|
+
const words = useWords().common.pager;
|
|
78
|
+
if (props.pages <= 1) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const from = (props.page - 1) * props.pageSize + 1;
|
|
82
|
+
const to = Math.min(props.page * props.pageSize, props.total);
|
|
83
|
+
const step = (label: string, target: number, there: boolean) => {
|
|
84
|
+
if (!there) {
|
|
85
|
+
return (
|
|
86
|
+
<span className="sc-button sc-button-secondary sc-button-sm" aria-disabled="true">
|
|
87
|
+
{label}
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return props.pageHref === undefined ? (
|
|
92
|
+
<Button variant="secondary" size="sm" onClick={() => props.onPage?.(target)}>
|
|
93
|
+
{label}
|
|
94
|
+
</Button>
|
|
95
|
+
) : (
|
|
96
|
+
<a className="sc-button sc-button-secondary sc-button-sm" href={props.pageHref(target)}>
|
|
97
|
+
{label}
|
|
98
|
+
</a>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
return (
|
|
102
|
+
<nav className="sc-pager" aria-label={words.label}>
|
|
103
|
+
{step(words.previous, props.page - 1, props.page > 1)}
|
|
104
|
+
<span className="sc-pager-range">{words.range(from, to, props.total)}</span>
|
|
105
|
+
{step(words.next, props.page + 1, props.page < props.pages)}
|
|
106
|
+
</nav>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -2,9 +2,10 @@ import type { RestSkills } from "../api.js";
|
|
|
2
2
|
import { useWords } from "../i18n/index.js";
|
|
3
3
|
import { Callout, EmptyState, Spinner } from "./ui.js";
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// A project's skills, as the pack lists them: at a SkillCDN deployment, where a person reads
|
|
6
|
+
// each and what an agent loads it by; or from a directory on the console's machine (ADR-0031),
|
|
7
|
+
// with nothing to link to. Takes its data as props and nothing from the network; what the
|
|
8
|
+
// console answered is shown as text, never as HTML.
|
|
8
9
|
|
|
9
10
|
export interface SkillListProps {
|
|
10
11
|
/** What the console answered, or nothing while it has not yet. */
|
|
@@ -21,35 +22,48 @@ export function SkillList(props: SkillListProps) {
|
|
|
21
22
|
if (skills.status === "unavailable" && skills.address === null) {
|
|
22
23
|
return <Callout tone="warning">{words.status.unavailable}</Callout>;
|
|
23
24
|
}
|
|
24
|
-
if (skills.status === "none" || skills.address === null
|
|
25
|
+
if (skills.status === "none" || skills.address === null) {
|
|
25
26
|
return <EmptyState title={words.noAddress.title} body={words.noAddress.body} />;
|
|
26
27
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
<code>{skills.address}</code>
|
|
37
|
-
</
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
28
|
+
const deployment =
|
|
29
|
+
skills.source !== null && skills.page !== null
|
|
30
|
+
? { source: skills.source, page: skills.page }
|
|
31
|
+
: undefined;
|
|
32
|
+
const local = deployment === undefined;
|
|
33
|
+
const where =
|
|
34
|
+
deployment === undefined ? (
|
|
35
|
+
<>
|
|
36
|
+
<p className="sc-lead">
|
|
37
|
+
{words.fromMachine} <code>{skills.address}</code>
|
|
38
|
+
</p>
|
|
39
|
+
<p className="sc-muted">{words.connectLocal}</p>
|
|
40
|
+
</>
|
|
41
|
+
) : (
|
|
42
|
+
<>
|
|
43
|
+
<p className="sc-lead">
|
|
44
|
+
{words.servedBy}{" "}
|
|
45
|
+
<a href={deployment.source} target="_blank" rel="noopener noreferrer">
|
|
46
|
+
SkillCDN
|
|
47
|
+
</a>{" "}
|
|
48
|
+
{words.at}{" "}
|
|
49
|
+
<a href={deployment.page} target="_blank" rel="noopener noreferrer">
|
|
50
|
+
<code>{skills.address}</code>
|
|
51
|
+
</a>
|
|
52
|
+
</p>
|
|
53
|
+
<p className="sc-muted">
|
|
54
|
+
{words.connect}{" "}
|
|
55
|
+
<a href={deployment.page} target="_blank" rel="noopener noreferrer">
|
|
56
|
+
{words.connectLink}
|
|
57
|
+
</a>
|
|
58
|
+
</p>
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
47
61
|
if (skills.status !== "ready") {
|
|
48
62
|
return (
|
|
49
63
|
<>
|
|
50
64
|
{where}
|
|
51
65
|
<Callout tone={skills.status === "indexing" ? "info" : "warning"}>
|
|
52
|
-
{words.status[skills.status]}
|
|
66
|
+
{(local ? words.localStatus[skills.status] : undefined) ?? words.status[skills.status]}
|
|
53
67
|
</Callout>
|
|
54
68
|
</>
|
|
55
69
|
);
|
|
@@ -69,14 +83,18 @@ export function SkillList(props: SkillListProps) {
|
|
|
69
83
|
{skills.items.map((skill) => (
|
|
70
84
|
<li key={skill.path} className="sc-skill">
|
|
71
85
|
<p className="sc-skill-head">
|
|
72
|
-
|
|
73
|
-
className="sc-skill-name"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
{skill.page === null ? (
|
|
87
|
+
<span className="sc-skill-name">{skill.name}</span>
|
|
88
|
+
) : (
|
|
89
|
+
<a
|
|
90
|
+
className="sc-skill-name"
|
|
91
|
+
href={skill.page}
|
|
92
|
+
target="_blank"
|
|
93
|
+
rel="noopener noreferrer nofollow"
|
|
94
|
+
>
|
|
95
|
+
{skill.name}
|
|
96
|
+
</a>
|
|
97
|
+
)}
|
|
80
98
|
<span className="sc-muted">{skill.directory === "" ? "/" : skill.directory}</span>
|
|
81
99
|
</p>
|
|
82
100
|
<p className="sc-skill-description">{skill.description}</p>
|
package/src/components/ui.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type TaskPriority,
|
|
10
10
|
type TaskState,
|
|
11
11
|
} from "../api.js";
|
|
12
|
-
import { daysBetween, formatDay, today } from "../dates.js";
|
|
12
|
+
import { dayOf, daysBetween, formatDay, today } from "../dates.js";
|
|
13
13
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
14
14
|
|
|
15
15
|
// Small building blocks. Each one is a class in styles/console.css and nothing more. Classes
|
|
@@ -356,11 +356,24 @@ export function formatBytes(size: number): string {
|
|
|
356
356
|
return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
export function Time(props: {
|
|
359
|
+
export function Time(props: {
|
|
360
|
+
readonly iso: string;
|
|
361
|
+
/** `day` shows the day alone, with the moment as the title, where a row has no room for the hour. */
|
|
362
|
+
readonly precision?: "minute" | "day";
|
|
363
|
+
}) {
|
|
360
364
|
const { tag } = useLanguage();
|
|
365
|
+
const full = formatInstant(props.iso, tag);
|
|
366
|
+
if (props.precision === "day") {
|
|
367
|
+
const date = new Date(props.iso);
|
|
368
|
+
return (
|
|
369
|
+
<time className="sc-time" dateTime={props.iso} title={full}>
|
|
370
|
+
{Number.isNaN(date.getTime()) ? props.iso : formatDay(dayOf(date), tag)}
|
|
371
|
+
</time>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
361
374
|
return (
|
|
362
375
|
<time className="sc-time" dateTime={props.iso}>
|
|
363
|
-
{
|
|
376
|
+
{full}
|
|
364
377
|
</time>
|
|
365
378
|
);
|
|
366
379
|
}
|
package/src/console.tsx
CHANGED
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
calendarHref,
|
|
99
99
|
customPageHref,
|
|
100
100
|
decisionHref,
|
|
101
|
+
decisionsHref,
|
|
101
102
|
docHref,
|
|
102
103
|
type GoalTaskView,
|
|
103
104
|
goalHref,
|
|
@@ -1112,6 +1113,8 @@ function ProjectPage(props: {
|
|
|
1112
1113
|
taskHref={href}
|
|
1113
1114
|
docHref={toDoc}
|
|
1114
1115
|
decisionHref={(decision) => decisionHref(key, decision.id)}
|
|
1116
|
+
page={route.page}
|
|
1117
|
+
pageHref={(page) => decisionsHref(key, page)}
|
|
1115
1118
|
onAnswer={onAnswer}
|
|
1116
1119
|
onUpdate={onUpdate}
|
|
1117
1120
|
busy={busy}
|