@skillcdn/console 0.1.18 → 0.1.20
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 +2 -2
- package/SKILLCDN.md +1 -1
- 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 +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +353 -39
- 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 +10 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +106 -2
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +44 -22
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +21 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -1
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +23 -5
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +239 -9
- package/dist/components/board.js.map +1 -1
- package/dist/components/comments.d.ts +0 -2
- package/dist/components/comments.d.ts.map +1 -1
- package/dist/components/comments.js +14 -41
- package/dist/components/comments.js.map +1 -1
- package/dist/components/documents.d.ts +2 -0
- package/dist/components/documents.d.ts.map +1 -1
- package/dist/components/documents.js +51 -5
- package/dist/components/documents.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/files.d.ts +11 -0
- package/dist/components/files.d.ts.map +1 -0
- package/dist/components/files.js +44 -0
- package/dist/components/files.js.map +1 -0
- package/dist/components/goals.d.ts +17 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +34 -9
- 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/runs.d.ts.map +1 -1
- package/dist/components/runs.js +34 -7
- package/dist/components/runs.js.map +1 -1
- 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/console.css +338 -37
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +124 -10
- 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 +47 -2
- package/dist/data.js.map +1 -1
- package/dist/docs-page.d.ts.map +1 -1
- package/dist/docs-page.js +14 -1
- package/dist/docs-page.js.map +1 -1
- package/dist/i18n/en.d.ts +56 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +54 -3
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +54 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +25 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +38 -7
- 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 +158 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +78 -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 +19 -11
- package/docs/specs/rest.md +24 -6
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +12 -6
- package/src/api.ts +13 -0
- package/src/cli/cli.ts +441 -42
- package/src/cli/credentials.ts +276 -14
- package/src/cli/directory.ts +43 -6
- package/src/cli/format.ts +138 -2
- package/src/cli/help.ts +46 -22
- package/src/client.ts +60 -0
- package/src/components/board.tsx +401 -28
- package/src/components/comments.tsx +30 -49
- package/src/components/documents.tsx +87 -4
- package/src/components/event-feed.tsx +4 -0
- package/src/components/files.ts +49 -0
- package/src/components/goals.tsx +173 -106
- package/src/components/markdown.tsx +19 -0
- package/src/components/pictures.tsx +389 -0
- package/src/components/projects.tsx +68 -0
- package/src/components/runs.tsx +73 -15
- package/src/components/task-view.tsx +43 -2
- package/src/console.tsx +268 -7
- package/src/data.ts +72 -1
- package/src/docs-page.tsx +18 -1
- package/src/i18n/en.ts +58 -3
- package/src/i18n/ko.ts +58 -3
- package/src/index.ts +28 -1
- package/src/router.ts +57 -9
- package/src/rules.ts +5 -5
- package/src/schemas.ts +97 -0
- package/src/styles/console.css +338 -37
- package/src/vocabulary.ts +3 -0
package/src/console.tsx
CHANGED
|
@@ -43,11 +43,13 @@ 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,
|
|
@@ -75,6 +77,8 @@ import {
|
|
|
75
77
|
EmptyState,
|
|
76
78
|
personName,
|
|
77
79
|
Spinner,
|
|
80
|
+
StateBadge,
|
|
81
|
+
Time,
|
|
78
82
|
} from "./components/ui.js";
|
|
79
83
|
import { WorkList, type WorkListProps } from "./components/work.js";
|
|
80
84
|
import { ConnectPage } from "./connect-page.js";
|
|
@@ -95,6 +99,7 @@ import {
|
|
|
95
99
|
customPageHref,
|
|
96
100
|
decisionHref,
|
|
97
101
|
docHref,
|
|
102
|
+
type GoalTaskView,
|
|
98
103
|
goalHref,
|
|
99
104
|
matchRoute,
|
|
100
105
|
type Navigation,
|
|
@@ -442,6 +447,101 @@ function ProjectsPage(props: {
|
|
|
442
447
|
);
|
|
443
448
|
}
|
|
444
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
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
494
|
+
{!loaded ? (
|
|
495
|
+
<Spinner label={archive.loading} />
|
|
496
|
+
) : items.length === 0 ? (
|
|
497
|
+
<EmptyState title={archive.empty.title} body={archive.empty.body} />
|
|
498
|
+
) : (
|
|
499
|
+
<>
|
|
500
|
+
<ul className="sc-task-rows" aria-label={archive.title}>
|
|
501
|
+
{items.map((task) => (
|
|
502
|
+
<li key={task.id} className="sc-task-row">
|
|
503
|
+
<a className="sc-task-row-title" href={taskHref(key, task.number)}>
|
|
504
|
+
<span className="sc-card-number">#{task.number}</span> {task.title}
|
|
505
|
+
</a>
|
|
506
|
+
<span className="sc-task-row-meta">
|
|
507
|
+
<StateBadge state={task.state} />
|
|
508
|
+
{task.archivedAt !== null && (
|
|
509
|
+
<span className="sc-muted">
|
|
510
|
+
{archive.since} <Time iso={task.archivedAt} />
|
|
511
|
+
</span>
|
|
512
|
+
)}
|
|
513
|
+
<Button
|
|
514
|
+
size="sm"
|
|
515
|
+
variant="ghost"
|
|
516
|
+
disabled={busy}
|
|
517
|
+
onClick={() =>
|
|
518
|
+
void act(async () => {
|
|
519
|
+
await data.actions.restoreTask(task.id);
|
|
520
|
+
setItems((current) =>
|
|
521
|
+
current.filter((candidate) => candidate.id !== task.id),
|
|
522
|
+
);
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
>
|
|
526
|
+
{archive.restore}
|
|
527
|
+
</Button>
|
|
528
|
+
</span>
|
|
529
|
+
</li>
|
|
530
|
+
))}
|
|
531
|
+
</ul>
|
|
532
|
+
{more && (
|
|
533
|
+
<div className="sc-show-more">
|
|
534
|
+
<Button variant="secondary" size="sm" onClick={onMore}>
|
|
535
|
+
{archive.more}
|
|
536
|
+
</Button>
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
539
|
+
</>
|
|
540
|
+
)}
|
|
541
|
+
</>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
445
545
|
function TaskPage(props: {
|
|
446
546
|
readonly route: Extract<Route, { name: "task" }>;
|
|
447
547
|
readonly project: RestProject;
|
|
@@ -455,10 +555,43 @@ function TaskPage(props: {
|
|
|
455
555
|
const words = useWords();
|
|
456
556
|
const { route, project, data, components, navigation, busy, error, act } = props;
|
|
457
557
|
const key = project.key;
|
|
458
|
-
const
|
|
558
|
+
const current =
|
|
459
559
|
route.number === undefined
|
|
460
560
|
? data.tasks.find((candidate) => candidate.id === route.id)
|
|
461
561
|
: data.tasks.find((candidate) => candidate.number === route.number);
|
|
562
|
+
// A task put away is not among the board's (ADR-0029): it is asked for by itself.
|
|
563
|
+
const [fetched, setFetched] = useState<RestTask | undefined>(undefined);
|
|
564
|
+
const [missing, setMissing] = useState<string | undefined>(undefined);
|
|
565
|
+
const { fetchTask } = data.actions;
|
|
566
|
+
const wanted =
|
|
567
|
+
current === undefined && data.projectLoaded
|
|
568
|
+
? route.number === undefined
|
|
569
|
+
? route.id
|
|
570
|
+
: String(route.number)
|
|
571
|
+
: undefined;
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
if (wanted === undefined) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const controller = new AbortController();
|
|
577
|
+
fetchTask(wanted, controller.signal)
|
|
578
|
+
.then((found) => {
|
|
579
|
+
if (!controller.signal.aborted) {
|
|
580
|
+
setFetched(found);
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
.catch(() => {
|
|
584
|
+
if (!controller.signal.aborted) {
|
|
585
|
+
setMissing(wanted);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
return () => controller.abort();
|
|
589
|
+
}, [wanted, fetchTask]);
|
|
590
|
+
const task =
|
|
591
|
+
current ??
|
|
592
|
+
(fetched !== undefined && (fetched.id === route.id || fetched.number === route.number)
|
|
593
|
+
? fetched
|
|
594
|
+
: undefined);
|
|
462
595
|
const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
|
|
463
596
|
const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
|
|
464
597
|
const { taskHistory, taskComments } = data.actions;
|
|
@@ -495,6 +628,9 @@ function TaskPage(props: {
|
|
|
495
628
|
return () => controller.abort();
|
|
496
629
|
}, [taskId, taskHistory, taskComments, feedLength]);
|
|
497
630
|
if (task === undefined) {
|
|
631
|
+
if (wanted !== undefined && missing !== wanted) {
|
|
632
|
+
return <Spinner label={words.common.loading} />;
|
|
633
|
+
}
|
|
498
634
|
return (
|
|
499
635
|
<EmptyState
|
|
500
636
|
title={words.task.noSuch.title}
|
|
@@ -545,6 +681,17 @@ function TaskPage(props: {
|
|
|
545
681
|
void act(() => data.actions.updateDecision(decision.id, patch))
|
|
546
682
|
}
|
|
547
683
|
onAbandonRun={(run) => void act(() => data.actions.abandonRun(run.id))}
|
|
684
|
+
onArchive={(target) =>
|
|
685
|
+
void act(async () => {
|
|
686
|
+
await data.actions.archiveTask(target.id);
|
|
687
|
+
navigation.go(projectHref(key, "archive"));
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
onRestore={(target) =>
|
|
691
|
+
void act(async () => {
|
|
692
|
+
setFetched(await data.actions.restoreTask(target.id));
|
|
693
|
+
})
|
|
694
|
+
}
|
|
548
695
|
onComment={(draft) =>
|
|
549
696
|
void act(async () => {
|
|
550
697
|
await data.actions.comment(task.id, {
|
|
@@ -592,9 +739,12 @@ function ProjectPage(props: {
|
|
|
592
739
|
<>
|
|
593
740
|
<div className="sc-page-head">
|
|
594
741
|
<h1 className="sc-page-title">{project.name}</h1>
|
|
595
|
-
<
|
|
596
|
-
{words.board.
|
|
597
|
-
|
|
742
|
+
<div className="sc-page-actions">
|
|
743
|
+
<LinkButton href={projectHref(key, "archive")}>{words.board.archive}</LinkButton>
|
|
744
|
+
<LinkButton href={newTaskHref(key)} variant="primary">
|
|
745
|
+
{words.board.writeTask}
|
|
746
|
+
</LinkButton>
|
|
747
|
+
</div>
|
|
598
748
|
</div>
|
|
599
749
|
{project.description.length > 0 && <p className="sc-lead">{project.description}</p>}
|
|
600
750
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
@@ -615,6 +765,9 @@ function ProjectPage(props: {
|
|
|
615
765
|
taskHref={href}
|
|
616
766
|
onOpen={(task) => navigation.go(href(task))}
|
|
617
767
|
onMove={onMove}
|
|
768
|
+
editHref={(task) => taskHref(key, task.number, "edit")}
|
|
769
|
+
onEdit={(task) => navigation.go(taskHref(key, task.number, "edit"))}
|
|
770
|
+
onArchive={(task) => void act(() => data.actions.archiveTask(task.id))}
|
|
618
771
|
empty={
|
|
619
772
|
data.tasks.length === 0 ? (
|
|
620
773
|
<EmptyState
|
|
@@ -850,6 +1003,10 @@ function ProjectPage(props: {
|
|
|
850
1003
|
</>
|
|
851
1004
|
);
|
|
852
1005
|
}
|
|
1006
|
+
const now = today();
|
|
1007
|
+
const towards = data.tasks.filter((task) => task.goalId === goal.id);
|
|
1008
|
+
const month = route.month ?? goalMonth(goal, now);
|
|
1009
|
+
const viewHref = (view: GoalTaskView) => goalHref(key, goal.id, { view });
|
|
853
1010
|
return (
|
|
854
1011
|
<>
|
|
855
1012
|
<div className="sc-page-head">
|
|
@@ -862,13 +1019,62 @@ function ProjectPage(props: {
|
|
|
862
1019
|
tasks={data.tasks}
|
|
863
1020
|
taskHref={href}
|
|
864
1021
|
docHref={toDoc}
|
|
865
|
-
|
|
1022
|
+
today={now}
|
|
1023
|
+
editHref={owner ? goalHref(key, goal.id, { form: "edit" }) : undefined}
|
|
866
1024
|
onMove={
|
|
867
1025
|
owner
|
|
868
|
-
? (target, state) =>
|
|
1026
|
+
? (target, state) =>
|
|
1027
|
+
void act(async () => {
|
|
1028
|
+
await data.actions.updateGoal(target.id, { state });
|
|
1029
|
+
// A goal closed takes its done and dropped tasks to the archive, if the person says so (ADR-0029).
|
|
1030
|
+
const closed = towards.filter(
|
|
1031
|
+
(task) => !OPEN_TASK_STATES.includes(task.state),
|
|
1032
|
+
).length;
|
|
1033
|
+
if (
|
|
1034
|
+
(state === "done" || state === "dropped") &&
|
|
1035
|
+
closed > 0 &&
|
|
1036
|
+
window.confirm(roadmap.archiveAsk(closed))
|
|
1037
|
+
) {
|
|
1038
|
+
await data.actions.archiveGoalTasks(target.id);
|
|
1039
|
+
}
|
|
1040
|
+
})
|
|
1041
|
+
: undefined
|
|
1042
|
+
}
|
|
1043
|
+
onArchiveTasks={
|
|
1044
|
+
owner && towards.some((task) => !OPEN_TASK_STATES.includes(task.state))
|
|
1045
|
+
? () => void act(() => data.actions.archiveGoalTasks(goal.id))
|
|
869
1046
|
: undefined
|
|
870
1047
|
}
|
|
871
1048
|
busy={busy}
|
|
1049
|
+
view={route.view}
|
|
1050
|
+
viewHref={viewHref}
|
|
1051
|
+
board={
|
|
1052
|
+
<components.Board
|
|
1053
|
+
tasks={towards}
|
|
1054
|
+
taskHref={href}
|
|
1055
|
+
onOpen={(task) => navigation.go(href(task))}
|
|
1056
|
+
onMove={onMove}
|
|
1057
|
+
today={now}
|
|
1058
|
+
editHref={(task) => taskHref(key, task.number, "edit")}
|
|
1059
|
+
onEdit={(task) => navigation.go(taskHref(key, task.number, "edit"))}
|
|
1060
|
+
onArchive={(task) => void act(() => data.actions.archiveTask(task.id))}
|
|
1061
|
+
empty={
|
|
1062
|
+
towards.length === 0 ? <p className="sc-muted">{roadmap.noTasks}</p> : undefined
|
|
1063
|
+
}
|
|
1064
|
+
/>
|
|
1065
|
+
}
|
|
1066
|
+
calendar={
|
|
1067
|
+
<components.Calendar
|
|
1068
|
+
tasks={towards}
|
|
1069
|
+
month={month}
|
|
1070
|
+
today={now}
|
|
1071
|
+
categories={data.categories}
|
|
1072
|
+
goals={[goal]}
|
|
1073
|
+
taskHref={href}
|
|
1074
|
+
goalHref={(target) => goalHref(key, target.id)}
|
|
1075
|
+
monthHref={(wanted) => goalHref(key, goal.id, { view: "calendar", month: wanted })}
|
|
1076
|
+
/>
|
|
1077
|
+
}
|
|
872
1078
|
/>
|
|
873
1079
|
</>
|
|
874
1080
|
);
|
|
@@ -958,6 +1164,9 @@ function ProjectPage(props: {
|
|
|
958
1164
|
</>
|
|
959
1165
|
);
|
|
960
1166
|
}
|
|
1167
|
+
if (route.name === "archive") {
|
|
1168
|
+
return <ArchivePage project={project} data={data} />;
|
|
1169
|
+
}
|
|
961
1170
|
if (route.name === "feed") {
|
|
962
1171
|
return (
|
|
963
1172
|
<>
|
|
@@ -1156,6 +1365,39 @@ function ProjectSettingsPage(props: {
|
|
|
1156
1365
|
</>
|
|
1157
1366
|
);
|
|
1158
1367
|
}
|
|
1368
|
+
if (route.form === "pack-token") {
|
|
1369
|
+
if (!owner) {
|
|
1370
|
+
return <NothingHere />;
|
|
1371
|
+
}
|
|
1372
|
+
return (
|
|
1373
|
+
<>
|
|
1374
|
+
{head}
|
|
1375
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1376
|
+
<section className="sc-panel sc-panel-form" aria-label={words.projects.packToken.aria}>
|
|
1377
|
+
<PackTokenForm
|
|
1378
|
+
set={project.packToken !== null}
|
|
1379
|
+
busy={busy}
|
|
1380
|
+
onSubmit={(token) =>
|
|
1381
|
+
void act(async () => {
|
|
1382
|
+
await data.actions.setPackToken(token);
|
|
1383
|
+
navigation.replace(here);
|
|
1384
|
+
})
|
|
1385
|
+
}
|
|
1386
|
+
onRemove={
|
|
1387
|
+
project.packToken === null
|
|
1388
|
+
? undefined
|
|
1389
|
+
: () =>
|
|
1390
|
+
void act(async () => {
|
|
1391
|
+
await data.actions.removePackToken();
|
|
1392
|
+
navigation.replace(here);
|
|
1393
|
+
})
|
|
1394
|
+
}
|
|
1395
|
+
onCancel={() => navigation.back(here)}
|
|
1396
|
+
/>
|
|
1397
|
+
</section>
|
|
1398
|
+
</>
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1159
1401
|
return (
|
|
1160
1402
|
<>
|
|
1161
1403
|
{head}
|
|
@@ -1191,6 +1433,24 @@ function ProjectSettingsPage(props: {
|
|
|
1191
1433
|
)}
|
|
1192
1434
|
</dd>
|
|
1193
1435
|
</div>
|
|
1436
|
+
<div>
|
|
1437
|
+
<dt>{settings.packToken}</dt>
|
|
1438
|
+
<dd>
|
|
1439
|
+
{project.packToken === null ? (
|
|
1440
|
+
<span className="sc-muted">{settings.packTokenNone}</span>
|
|
1441
|
+
) : (
|
|
1442
|
+
<>
|
|
1443
|
+
{settings.packTokenSet} <Time iso={project.packToken.setAt} />
|
|
1444
|
+
</>
|
|
1445
|
+
)}
|
|
1446
|
+
{owner && (
|
|
1447
|
+
<>
|
|
1448
|
+
{" "}
|
|
1449
|
+
<a href={projectSettingsHref(key, "pack-token")}>{settings.packTokenEdit}</a>
|
|
1450
|
+
</>
|
|
1451
|
+
)}
|
|
1452
|
+
</dd>
|
|
1453
|
+
</div>
|
|
1194
1454
|
<div>
|
|
1195
1455
|
<dt>{form.symbol}</dt>
|
|
1196
1456
|
<dd>
|
|
@@ -2073,6 +2333,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
2073
2333
|
wide={
|
|
2074
2334
|
route.name === "board" ||
|
|
2075
2335
|
route.name === "calendar" ||
|
|
2336
|
+
(route.name === "goal" && route.form === undefined) ||
|
|
2076
2337
|
(route.name === "custom" &&
|
|
2077
2338
|
pages.some((candidate) => candidate.key === route.page && candidate.wide === true))
|
|
2078
2339
|
}
|
package/src/data.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
type DocumentFilter,
|
|
7
7
|
EVENT_KINDS,
|
|
8
8
|
type FileUpload,
|
|
9
|
+
LIST_LIMIT,
|
|
10
|
+
type ProjectClient,
|
|
9
11
|
type RestAnswerInput,
|
|
10
12
|
type RestCategory,
|
|
11
13
|
type RestCategoryInput,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
type RestTask,
|
|
45
47
|
type RestTaskInput,
|
|
46
48
|
type RestTaskPatch,
|
|
49
|
+
type RestTasks,
|
|
47
50
|
type RestToken,
|
|
48
51
|
type RestTokenCreated,
|
|
49
52
|
type RestTokenInput,
|
|
@@ -74,11 +77,26 @@ export interface ConsoleActions {
|
|
|
74
77
|
/** Sets the image for the mark of the project the page is on, or takes it away; for an owner. */
|
|
75
78
|
setProjectIcon(input: FileUpload): Promise<RestProject>;
|
|
76
79
|
removeProjectIcon(): Promise<RestProject>;
|
|
80
|
+
/** Sets the pack token of the project the page is on, or takes it away; for an owner (ADR-0028). */
|
|
81
|
+
setPackToken(token: string): Promise<RestProject>;
|
|
82
|
+
removePackToken(): Promise<RestProject>;
|
|
77
83
|
addMember(input: RestMemberInput): Promise<RestMember>;
|
|
78
84
|
updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
|
|
79
85
|
removeMember(personId: string): Promise<void>;
|
|
80
86
|
createTask(input: RestTaskInput): Promise<RestTask>;
|
|
81
87
|
updateTask(id: string, patch: RestTaskPatch): Promise<RestTask>;
|
|
88
|
+
/** One task by its id or its number, asked for by itself: a task put away is not among the board's. */
|
|
89
|
+
fetchTask(ref: string, signal?: AbortSignal): Promise<RestTask>;
|
|
90
|
+
/** Puts a done or dropped task away, or brings one back (ADR-0029). */
|
|
91
|
+
archiveTask(id: string): Promise<RestTask>;
|
|
92
|
+
restoreTask(id: string): Promise<RestTask>;
|
|
93
|
+
/** Puts every done or dropped task of a goal away; for an owner. Answers how many went. */
|
|
94
|
+
archiveGoalTasks(goalId: string): Promise<number>;
|
|
95
|
+
/** A page of the tasks put away, newest first. */
|
|
96
|
+
archivedTasks(
|
|
97
|
+
page: { readonly limit: number; readonly offset: number },
|
|
98
|
+
signal?: AbortSignal,
|
|
99
|
+
): Promise<RestTasks>;
|
|
82
100
|
raiseDecision(input: RestDecisionInput): Promise<RestDecision>;
|
|
83
101
|
answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
|
|
84
102
|
/** Grows a decision's record: its context, or what followed. */
|
|
@@ -201,6 +219,32 @@ async function readFeed(
|
|
|
201
219
|
return events.slice(-FEED_KEEP);
|
|
202
220
|
}
|
|
203
221
|
|
|
222
|
+
/** How many pages of the current tasks are read before the board is shown as it stands. */
|
|
223
|
+
const TASKS_MAX_PAGES = 20;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The current tasks of a project, whole: page after page while the server says there is more,
|
|
227
|
+
* so that a board past one page loses nothing without a word; the tasks put away are not among
|
|
228
|
+
* them (ADR-0029).
|
|
229
|
+
*/
|
|
230
|
+
async function readTasks(
|
|
231
|
+
scope: ProjectClient,
|
|
232
|
+
signal: AbortSignal | undefined,
|
|
233
|
+
): Promise<{ readonly items: RestTask[] }> {
|
|
234
|
+
const items: RestTask[] = [];
|
|
235
|
+
for (let page = 0; page < TASKS_MAX_PAGES; page += 1) {
|
|
236
|
+
const result = await scope.tasks(
|
|
237
|
+
{ archived: false, limit: LIST_LIMIT, offset: page * LIST_LIMIT },
|
|
238
|
+
signal,
|
|
239
|
+
);
|
|
240
|
+
items.push(...result.items);
|
|
241
|
+
if (!result.more) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { items };
|
|
246
|
+
}
|
|
247
|
+
|
|
204
248
|
const wordsOf = (failure: unknown, fallback: string): string =>
|
|
205
249
|
failure instanceof ApiError ? failure.message : fallback;
|
|
206
250
|
|
|
@@ -265,7 +309,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
265
309
|
const [nextProject, nextTasks, nextDecisions, nextGoals, nextRuns, nextMembers] =
|
|
266
310
|
await Promise.all([
|
|
267
311
|
scope.get(signal),
|
|
268
|
-
scope
|
|
312
|
+
readTasks(scope, signal),
|
|
269
313
|
scope.decisions(undefined, signal),
|
|
270
314
|
scope.goals({}, signal),
|
|
271
315
|
scope.runs(undefined, signal),
|
|
@@ -454,6 +498,16 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
454
498
|
await refreshWorkspace();
|
|
455
499
|
return changed;
|
|
456
500
|
},
|
|
501
|
+
async setPackToken(token) {
|
|
502
|
+
const changed = await scope().setPackToken({ token });
|
|
503
|
+
setProject(changed);
|
|
504
|
+
return changed;
|
|
505
|
+
},
|
|
506
|
+
async removePackToken() {
|
|
507
|
+
const changed = await scope().removePackToken();
|
|
508
|
+
setProject(changed);
|
|
509
|
+
return changed;
|
|
510
|
+
},
|
|
457
511
|
async addMember(input) {
|
|
458
512
|
const member = await scope().addMember(input);
|
|
459
513
|
await refreshProject();
|
|
@@ -478,6 +532,23 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
478
532
|
setTasks((current) => current.map((candidate) => (candidate.id === id ? task : candidate)));
|
|
479
533
|
return task;
|
|
480
534
|
},
|
|
535
|
+
fetchTask: (ref, signal) => scope().task(ref, signal),
|
|
536
|
+
async archiveTask(id) {
|
|
537
|
+
const task = await scope().archiveTask(id);
|
|
538
|
+
setTasks((current) => current.filter((candidate) => candidate.id !== id));
|
|
539
|
+
return task;
|
|
540
|
+
},
|
|
541
|
+
async restoreTask(id) {
|
|
542
|
+
const task = await scope().restoreTask(id);
|
|
543
|
+
setTasks((current) => [task, ...current.filter((candidate) => candidate.id !== id)]);
|
|
544
|
+
return task;
|
|
545
|
+
},
|
|
546
|
+
async archiveGoalTasks(goalId) {
|
|
547
|
+
const { archived } = await scope().archiveTasksOfGoal(goalId);
|
|
548
|
+
await refreshProject();
|
|
549
|
+
return archived;
|
|
550
|
+
},
|
|
551
|
+
archivedTasks: (page, signal) => scope().tasks({ archived: true, ...page }, signal),
|
|
481
552
|
async raiseDecision(input) {
|
|
482
553
|
const decision = await scope().raiseDecision(input);
|
|
483
554
|
await refreshProject();
|
package/src/docs-page.tsx
CHANGED
|
@@ -159,6 +159,22 @@ export function DocsPage(props: DocsPageProps) {
|
|
|
159
159
|
const toDecision = (decisionId: string) => decisionHref(key, decisionId);
|
|
160
160
|
/** The plain address of what the page is on. */
|
|
161
161
|
const here = toDoc(path);
|
|
162
|
+
// The pictures put into the body are attached once the page is written, one request each.
|
|
163
|
+
const attachAll = async (
|
|
164
|
+
at: string,
|
|
165
|
+
files: readonly File[],
|
|
166
|
+
written: RestDocument,
|
|
167
|
+
): Promise<RestDocument> => {
|
|
168
|
+
let page = written;
|
|
169
|
+
for (const file of files) {
|
|
170
|
+
page = await actions.attachDocumentFile(at, {
|
|
171
|
+
name: file.name,
|
|
172
|
+
bytes: file,
|
|
173
|
+
...(file.type === "" ? {} : { contentType: file.type }),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return page;
|
|
177
|
+
};
|
|
162
178
|
const page = document !== null && document !== undefined ? document : undefined;
|
|
163
179
|
/** The folder a new page goes in: this one, or the one the page shown is in. */
|
|
164
180
|
const folder = page === undefined ? path : folderOf(path);
|
|
@@ -218,6 +234,7 @@ export function DocsPage(props: DocsPageProps) {
|
|
|
218
234
|
title: input.title,
|
|
219
235
|
body: input.body,
|
|
220
236
|
});
|
|
237
|
+
await attachAll(written.path, input.files, written);
|
|
221
238
|
navigation.replace(toDoc(written.path));
|
|
222
239
|
})
|
|
223
240
|
}
|
|
@@ -255,7 +272,7 @@ export function DocsPage(props: DocsPageProps) {
|
|
|
255
272
|
body: input.body,
|
|
256
273
|
...(input.baseVersion === undefined ? {} : { baseVersion: input.baseVersion }),
|
|
257
274
|
});
|
|
258
|
-
setDocument(written);
|
|
275
|
+
setDocument(await attachAll(page.path, input.files, written));
|
|
259
276
|
navigation.replace(here);
|
|
260
277
|
})
|
|
261
278
|
}
|
package/src/i18n/en.ts
CHANGED
|
@@ -194,12 +194,24 @@ export const en = {
|
|
|
194
194
|
private: "The administrators own it until an owner is listed.",
|
|
195
195
|
},
|
|
196
196
|
},
|
|
197
|
+
packToken: {
|
|
198
|
+
aria: "The pack token",
|
|
199
|
+
label: "Token",
|
|
200
|
+
hint: "A token a person made at SkillCDN for the pack's repository, on their account page there. The console keeps it sealed, reads the pack with it for whoever may see the project, and shows it to nobody.",
|
|
201
|
+
placeholder: "scdn_repo_…",
|
|
202
|
+
set: "Set the token",
|
|
203
|
+
remove: "Remove the token",
|
|
204
|
+
},
|
|
197
205
|
settings: {
|
|
198
206
|
title: "Settings",
|
|
199
207
|
aria: "Project settings",
|
|
200
208
|
edit: "Edit the project",
|
|
201
209
|
organizationsSkills: (address: string) => `The organization's: ${address}`,
|
|
202
210
|
noSkills: "None: the console names no address for the organization's.",
|
|
211
|
+
packToken: "Pack token",
|
|
212
|
+
packTokenNone: "None: a public pack needs none, and a private one is not read.",
|
|
213
|
+
packTokenSet: "Set",
|
|
214
|
+
packTokenEdit: "Change it",
|
|
203
215
|
},
|
|
204
216
|
},
|
|
205
217
|
|
|
@@ -209,7 +221,8 @@ export const en = {
|
|
|
209
221
|
title: "Nothing on the board yet",
|
|
210
222
|
body: "Write the first task: what is to be done, and for whom.",
|
|
211
223
|
},
|
|
212
|
-
|
|
224
|
+
archive: "Archive",
|
|
225
|
+
menu: "Task menu",
|
|
213
226
|
decisions: (n: number) => count(n, "decision", "decisions"),
|
|
214
227
|
decisionsTitle: "Decisions waiting for a person",
|
|
215
228
|
filters: {
|
|
@@ -265,6 +278,10 @@ export const en = {
|
|
|
265
278
|
: "",
|
|
266
279
|
started: "Started",
|
|
267
280
|
done: "Done",
|
|
281
|
+
archived: "Put away",
|
|
282
|
+
archivedAt: "Put away",
|
|
283
|
+
archive: "Put away",
|
|
284
|
+
restore: "Bring back",
|
|
268
285
|
form: {
|
|
269
286
|
title: "Title",
|
|
270
287
|
titlePlaceholder: "What is to be done, in plain words that stand on their own.",
|
|
@@ -394,6 +411,12 @@ export const en = {
|
|
|
394
411
|
setBy: "Set by",
|
|
395
412
|
state: "State",
|
|
396
413
|
nothingMore: "Nothing more was written about it.",
|
|
414
|
+
about: "What reaching it means",
|
|
415
|
+
archiveTasks: "Put the done tasks away",
|
|
416
|
+
archiveAsk: (n: number) =>
|
|
417
|
+
`Put the ${count(n, "task", "tasks")} done or dropped under this goal away as well?`,
|
|
418
|
+
view: "View",
|
|
419
|
+
views: { board: "Board", list: "List", calendar: "Calendar" },
|
|
397
420
|
timeline: {
|
|
398
421
|
title: "The months",
|
|
399
422
|
legend: "Category colours",
|
|
@@ -555,6 +578,8 @@ export const en = {
|
|
|
555
578
|
taskUpdated: (fields: string, task: string) => `changed ${fields} of ${task}`,
|
|
556
579
|
taskCommented: (task: string, excerpt: string | undefined) =>
|
|
557
580
|
`commented on ${task}${excerpt === undefined || excerpt === "" ? "" : `: ${excerpt}`}`,
|
|
581
|
+
taskArchived: (task: string) => `put ${task} away`,
|
|
582
|
+
taskRestored: (task: string) => `brought ${task} back`,
|
|
558
583
|
decisionRaised: (question: string) => `asked: ${question}`,
|
|
559
584
|
decisionAnswered: (question: string, option: string) => `answered "${question}": ${option}`,
|
|
560
585
|
decisionUpdated: (fields: string, question: string) =>
|
|
@@ -575,6 +600,18 @@ export const en = {
|
|
|
575
600
|
},
|
|
576
601
|
},
|
|
577
602
|
|
|
603
|
+
archive: {
|
|
604
|
+
title: "Archive",
|
|
605
|
+
loading: "Loading the archive",
|
|
606
|
+
empty: {
|
|
607
|
+
title: "Nothing is put away",
|
|
608
|
+
body: "A task done or dropped is put away from its page, with its goal, or by the console after a while.",
|
|
609
|
+
},
|
|
610
|
+
since: "put away",
|
|
611
|
+
restore: "Bring back",
|
|
612
|
+
more: "Show more",
|
|
613
|
+
},
|
|
614
|
+
|
|
578
615
|
runs: {
|
|
579
616
|
for: "for",
|
|
580
617
|
since: "since",
|
|
@@ -585,6 +622,7 @@ export const en = {
|
|
|
585
622
|
answerIt: "answer it",
|
|
586
623
|
reports: "Reports",
|
|
587
624
|
handedIn: "Handed in",
|
|
625
|
+
picture: (name: string) => `The picture ${name}`,
|
|
588
626
|
label: (agent: string, name: string) => `${agent} for ${name}`,
|
|
589
627
|
},
|
|
590
628
|
|
|
@@ -601,6 +639,16 @@ export const en = {
|
|
|
601
639
|
picture: (name: string) => `The picture ${name}`,
|
|
602
640
|
label: (name: string) => `A comment by ${name}`,
|
|
603
641
|
},
|
|
642
|
+
pictures: {
|
|
643
|
+
open: (name: string) => `Open the picture ${name}`,
|
|
644
|
+
label: (name: string) => `The picture ${name}, large`,
|
|
645
|
+
close: "Close",
|
|
646
|
+
zoomIn: "Zoom in",
|
|
647
|
+
zoomOut: "Zoom out",
|
|
648
|
+
fit: "Fit to the screen",
|
|
649
|
+
file: "Open the file itself",
|
|
650
|
+
hint: "Zoom with the wheel, the buttons or two fingers; drag to move; Escape closes.",
|
|
651
|
+
},
|
|
604
652
|
|
|
605
653
|
skills: {
|
|
606
654
|
loading: "Loading the skills",
|
|
@@ -612,13 +660,15 @@ export const en = {
|
|
|
612
660
|
servedBy: "Served by",
|
|
613
661
|
at: "at",
|
|
614
662
|
connect:
|
|
615
|
-
"An agent reads these through its own MCP connection to this address, which a person makes once:",
|
|
663
|
+
"An agent reads these with the console command (console skill <path>), or through its own MCP connection to this address, which a person makes once:",
|
|
616
664
|
connectLink: "how, on the address's page at SkillCDN",
|
|
617
665
|
status: {
|
|
618
666
|
indexing: "SkillCDN is still indexing the repository. Look again in a moment.",
|
|
619
667
|
failed: "SkillCDN could not index the repository. What it found is on the address's page.",
|
|
620
668
|
not_found:
|
|
621
|
-
"SkillCDN does not serve this address, or
|
|
669
|
+
"SkillCDN does not serve this address to the console: there is nothing there, or the pack is private and the project holds no pack token for it (Settings).",
|
|
670
|
+
token_invalid:
|
|
671
|
+
"The pack token the project holds no longer works at SkillCDN: it expired, was removed, or its maker lost the repository. An owner sets a new one on the Settings page.",
|
|
622
672
|
unavailable: "SkillCDN could not be reached. The skills are as they were; look again later.",
|
|
623
673
|
},
|
|
624
674
|
none: {
|
|
@@ -683,6 +733,11 @@ export const en = {
|
|
|
683
733
|
body: "Body",
|
|
684
734
|
bodyPlaceholder:
|
|
685
735
|
"Markdown. A link to another page's path, such as guides/onboarding, refers to that page.",
|
|
736
|
+
pictures: "Pictures",
|
|
737
|
+
addPictures: "Add pictures",
|
|
738
|
+
pictureHint:
|
|
739
|
+
"A picture pasted or chosen here is attached to the page once it is written;  places it in the text.",
|
|
740
|
+
removeFile: (name: string) => `Take ${name} out`,
|
|
686
741
|
write: "Write the page",
|
|
687
742
|
writeVersion: "Write a new version",
|
|
688
743
|
},
|