@skillcdn/console 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- 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 +42 -20
- 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 +9 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +82 -5
- package/dist/components/board.js.map +1 -1
- package/dist/components/comments.d.ts.map +1 -1
- package/dist/components/comments.js +12 -4
- package/dist/components/comments.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +4 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +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/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 +282 -3
- 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/i18n/en.d.ts +52 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +50 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +50 -2
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- 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 +17 -9
- package/docs/specs/rest.md +22 -4
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +10 -4
- 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 +44 -20
- package/src/client.ts +60 -0
- package/src/components/board.tsx +154 -8
- package/src/components/comments.tsx +20 -10
- package/src/components/event-feed.tsx +4 -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/task-view.tsx +43 -2
- package/src/console.tsx +259 -4
- package/src/data.ts +72 -1
- package/src/i18n/en.ts +53 -2
- package/src/i18n/ko.ts +53 -2
- package/src/index.ts +27 -0
- package/src/router.ts +57 -9
- package/src/rules.ts +5 -5
- package/src/schemas.ts +97 -0
- package/src/styles/console.css +282 -3
- 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,102 @@ 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
|
+
<p className="sc-lead">{archive.lead}</p>
|
|
494
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
495
|
+
{!loaded ? (
|
|
496
|
+
<Spinner label={archive.loading} />
|
|
497
|
+
) : items.length === 0 ? (
|
|
498
|
+
<EmptyState title={archive.empty.title} body={archive.empty.body} />
|
|
499
|
+
) : (
|
|
500
|
+
<>
|
|
501
|
+
<ul className="sc-task-rows" aria-label={archive.title}>
|
|
502
|
+
{items.map((task) => (
|
|
503
|
+
<li key={task.id} className="sc-task-row">
|
|
504
|
+
<a className="sc-task-row-title" href={taskHref(key, task.number)}>
|
|
505
|
+
<span className="sc-card-number">#{task.number}</span> {task.title}
|
|
506
|
+
</a>
|
|
507
|
+
<span className="sc-task-row-meta">
|
|
508
|
+
<StateBadge state={task.state} />
|
|
509
|
+
{task.archivedAt !== null && (
|
|
510
|
+
<span className="sc-muted">
|
|
511
|
+
{archive.since} <Time iso={task.archivedAt} />
|
|
512
|
+
</span>
|
|
513
|
+
)}
|
|
514
|
+
<Button
|
|
515
|
+
size="sm"
|
|
516
|
+
variant="ghost"
|
|
517
|
+
disabled={busy}
|
|
518
|
+
onClick={() =>
|
|
519
|
+
void act(async () => {
|
|
520
|
+
await data.actions.restoreTask(task.id);
|
|
521
|
+
setItems((current) =>
|
|
522
|
+
current.filter((candidate) => candidate.id !== task.id),
|
|
523
|
+
);
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
>
|
|
527
|
+
{archive.restore}
|
|
528
|
+
</Button>
|
|
529
|
+
</span>
|
|
530
|
+
</li>
|
|
531
|
+
))}
|
|
532
|
+
</ul>
|
|
533
|
+
{more && (
|
|
534
|
+
<div className="sc-show-more">
|
|
535
|
+
<Button variant="secondary" size="sm" onClick={onMore}>
|
|
536
|
+
{archive.more}
|
|
537
|
+
</Button>
|
|
538
|
+
</div>
|
|
539
|
+
)}
|
|
540
|
+
</>
|
|
541
|
+
)}
|
|
542
|
+
</>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
445
546
|
function TaskPage(props: {
|
|
446
547
|
readonly route: Extract<Route, { name: "task" }>;
|
|
447
548
|
readonly project: RestProject;
|
|
@@ -455,10 +556,43 @@ function TaskPage(props: {
|
|
|
455
556
|
const words = useWords();
|
|
456
557
|
const { route, project, data, components, navigation, busy, error, act } = props;
|
|
457
558
|
const key = project.key;
|
|
458
|
-
const
|
|
559
|
+
const current =
|
|
459
560
|
route.number === undefined
|
|
460
561
|
? data.tasks.find((candidate) => candidate.id === route.id)
|
|
461
562
|
: data.tasks.find((candidate) => candidate.number === route.number);
|
|
563
|
+
// A task put away is not among the board's (ADR-0029): it is asked for by itself.
|
|
564
|
+
const [fetched, setFetched] = useState<RestTask | undefined>(undefined);
|
|
565
|
+
const [missing, setMissing] = useState<string | undefined>(undefined);
|
|
566
|
+
const { fetchTask } = data.actions;
|
|
567
|
+
const wanted =
|
|
568
|
+
current === undefined && data.projectLoaded
|
|
569
|
+
? route.number === undefined
|
|
570
|
+
? route.id
|
|
571
|
+
: String(route.number)
|
|
572
|
+
: undefined;
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
if (wanted === undefined) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const controller = new AbortController();
|
|
578
|
+
fetchTask(wanted, controller.signal)
|
|
579
|
+
.then((found) => {
|
|
580
|
+
if (!controller.signal.aborted) {
|
|
581
|
+
setFetched(found);
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
.catch(() => {
|
|
585
|
+
if (!controller.signal.aborted) {
|
|
586
|
+
setMissing(wanted);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
return () => controller.abort();
|
|
590
|
+
}, [wanted, fetchTask]);
|
|
591
|
+
const task =
|
|
592
|
+
current ??
|
|
593
|
+
(fetched !== undefined && (fetched.id === route.id || fetched.number === route.number)
|
|
594
|
+
? fetched
|
|
595
|
+
: undefined);
|
|
462
596
|
const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
|
|
463
597
|
const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
|
|
464
598
|
const { taskHistory, taskComments } = data.actions;
|
|
@@ -495,6 +629,9 @@ function TaskPage(props: {
|
|
|
495
629
|
return () => controller.abort();
|
|
496
630
|
}, [taskId, taskHistory, taskComments, feedLength]);
|
|
497
631
|
if (task === undefined) {
|
|
632
|
+
if (wanted !== undefined && missing !== wanted) {
|
|
633
|
+
return <Spinner label={words.common.loading} />;
|
|
634
|
+
}
|
|
498
635
|
return (
|
|
499
636
|
<EmptyState
|
|
500
637
|
title={words.task.noSuch.title}
|
|
@@ -545,6 +682,17 @@ function TaskPage(props: {
|
|
|
545
682
|
void act(() => data.actions.updateDecision(decision.id, patch))
|
|
546
683
|
}
|
|
547
684
|
onAbandonRun={(run) => void act(() => data.actions.abandonRun(run.id))}
|
|
685
|
+
onArchive={(target) =>
|
|
686
|
+
void act(async () => {
|
|
687
|
+
await data.actions.archiveTask(target.id);
|
|
688
|
+
navigation.go(projectHref(key, "archive"));
|
|
689
|
+
})
|
|
690
|
+
}
|
|
691
|
+
onRestore={(target) =>
|
|
692
|
+
void act(async () => {
|
|
693
|
+
setFetched(await data.actions.restoreTask(target.id));
|
|
694
|
+
})
|
|
695
|
+
}
|
|
548
696
|
onComment={(draft) =>
|
|
549
697
|
void act(async () => {
|
|
550
698
|
await data.actions.comment(task.id, {
|
|
@@ -615,6 +763,7 @@ function ProjectPage(props: {
|
|
|
615
763
|
taskHref={href}
|
|
616
764
|
onOpen={(task) => navigation.go(href(task))}
|
|
617
765
|
onMove={onMove}
|
|
766
|
+
archiveHref={projectHref(key, "archive")}
|
|
618
767
|
empty={
|
|
619
768
|
data.tasks.length === 0 ? (
|
|
620
769
|
<EmptyState
|
|
@@ -850,6 +999,10 @@ function ProjectPage(props: {
|
|
|
850
999
|
</>
|
|
851
1000
|
);
|
|
852
1001
|
}
|
|
1002
|
+
const now = today();
|
|
1003
|
+
const towards = data.tasks.filter((task) => task.goalId === goal.id);
|
|
1004
|
+
const month = route.month ?? goalMonth(goal, now);
|
|
1005
|
+
const viewHref = (view: GoalTaskView) => goalHref(key, goal.id, { view });
|
|
853
1006
|
return (
|
|
854
1007
|
<>
|
|
855
1008
|
<div className="sc-page-head">
|
|
@@ -862,13 +1015,60 @@ function ProjectPage(props: {
|
|
|
862
1015
|
tasks={data.tasks}
|
|
863
1016
|
taskHref={href}
|
|
864
1017
|
docHref={toDoc}
|
|
865
|
-
|
|
1018
|
+
today={now}
|
|
1019
|
+
editHref={owner ? goalHref(key, goal.id, { form: "edit" }) : undefined}
|
|
866
1020
|
onMove={
|
|
867
1021
|
owner
|
|
868
|
-
? (target, state) =>
|
|
1022
|
+
? (target, state) =>
|
|
1023
|
+
void act(async () => {
|
|
1024
|
+
await data.actions.updateGoal(target.id, { state });
|
|
1025
|
+
// A goal closed takes its done and dropped tasks to the archive, if the person says so (ADR-0029).
|
|
1026
|
+
const closed = towards.filter(
|
|
1027
|
+
(task) => !OPEN_TASK_STATES.includes(task.state),
|
|
1028
|
+
).length;
|
|
1029
|
+
if (
|
|
1030
|
+
(state === "done" || state === "dropped") &&
|
|
1031
|
+
closed > 0 &&
|
|
1032
|
+
window.confirm(roadmap.archiveAsk(closed))
|
|
1033
|
+
) {
|
|
1034
|
+
await data.actions.archiveGoalTasks(target.id);
|
|
1035
|
+
}
|
|
1036
|
+
})
|
|
1037
|
+
: undefined
|
|
1038
|
+
}
|
|
1039
|
+
onArchiveTasks={
|
|
1040
|
+
owner && towards.some((task) => !OPEN_TASK_STATES.includes(task.state))
|
|
1041
|
+
? () => void act(() => data.actions.archiveGoalTasks(goal.id))
|
|
869
1042
|
: undefined
|
|
870
1043
|
}
|
|
871
1044
|
busy={busy}
|
|
1045
|
+
view={route.view}
|
|
1046
|
+
viewHref={viewHref}
|
|
1047
|
+
board={
|
|
1048
|
+
<components.Board
|
|
1049
|
+
tasks={towards}
|
|
1050
|
+
taskHref={href}
|
|
1051
|
+
onOpen={(task) => navigation.go(href(task))}
|
|
1052
|
+
onMove={onMove}
|
|
1053
|
+
today={now}
|
|
1054
|
+
archiveHref={projectHref(key, "archive")}
|
|
1055
|
+
empty={
|
|
1056
|
+
towards.length === 0 ? <p className="sc-muted">{roadmap.noTasks}</p> : undefined
|
|
1057
|
+
}
|
|
1058
|
+
/>
|
|
1059
|
+
}
|
|
1060
|
+
calendar={
|
|
1061
|
+
<components.Calendar
|
|
1062
|
+
tasks={towards}
|
|
1063
|
+
month={month}
|
|
1064
|
+
today={now}
|
|
1065
|
+
categories={data.categories}
|
|
1066
|
+
goals={[goal]}
|
|
1067
|
+
taskHref={href}
|
|
1068
|
+
goalHref={(target) => goalHref(key, target.id)}
|
|
1069
|
+
monthHref={(wanted) => goalHref(key, goal.id, { view: "calendar", month: wanted })}
|
|
1070
|
+
/>
|
|
1071
|
+
}
|
|
872
1072
|
/>
|
|
873
1073
|
</>
|
|
874
1074
|
);
|
|
@@ -958,6 +1158,9 @@ function ProjectPage(props: {
|
|
|
958
1158
|
</>
|
|
959
1159
|
);
|
|
960
1160
|
}
|
|
1161
|
+
if (route.name === "archive") {
|
|
1162
|
+
return <ArchivePage project={project} data={data} />;
|
|
1163
|
+
}
|
|
961
1164
|
if (route.name === "feed") {
|
|
962
1165
|
return (
|
|
963
1166
|
<>
|
|
@@ -1156,6 +1359,39 @@ function ProjectSettingsPage(props: {
|
|
|
1156
1359
|
</>
|
|
1157
1360
|
);
|
|
1158
1361
|
}
|
|
1362
|
+
if (route.form === "pack-token") {
|
|
1363
|
+
if (!owner) {
|
|
1364
|
+
return <NothingHere />;
|
|
1365
|
+
}
|
|
1366
|
+
return (
|
|
1367
|
+
<>
|
|
1368
|
+
{head}
|
|
1369
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1370
|
+
<section className="sc-panel sc-panel-form" aria-label={words.projects.packToken.aria}>
|
|
1371
|
+
<PackTokenForm
|
|
1372
|
+
set={project.packToken !== null}
|
|
1373
|
+
busy={busy}
|
|
1374
|
+
onSubmit={(token) =>
|
|
1375
|
+
void act(async () => {
|
|
1376
|
+
await data.actions.setPackToken(token);
|
|
1377
|
+
navigation.replace(here);
|
|
1378
|
+
})
|
|
1379
|
+
}
|
|
1380
|
+
onRemove={
|
|
1381
|
+
project.packToken === null
|
|
1382
|
+
? undefined
|
|
1383
|
+
: () =>
|
|
1384
|
+
void act(async () => {
|
|
1385
|
+
await data.actions.removePackToken();
|
|
1386
|
+
navigation.replace(here);
|
|
1387
|
+
})
|
|
1388
|
+
}
|
|
1389
|
+
onCancel={() => navigation.back(here)}
|
|
1390
|
+
/>
|
|
1391
|
+
</section>
|
|
1392
|
+
</>
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1159
1395
|
return (
|
|
1160
1396
|
<>
|
|
1161
1397
|
{head}
|
|
@@ -1191,6 +1427,24 @@ function ProjectSettingsPage(props: {
|
|
|
1191
1427
|
)}
|
|
1192
1428
|
</dd>
|
|
1193
1429
|
</div>
|
|
1430
|
+
<div>
|
|
1431
|
+
<dt>{settings.packToken}</dt>
|
|
1432
|
+
<dd>
|
|
1433
|
+
{project.packToken === null ? (
|
|
1434
|
+
<span className="sc-muted">{settings.packTokenNone}</span>
|
|
1435
|
+
) : (
|
|
1436
|
+
<>
|
|
1437
|
+
{settings.packTokenSet} <Time iso={project.packToken.setAt} />
|
|
1438
|
+
</>
|
|
1439
|
+
)}
|
|
1440
|
+
{owner && (
|
|
1441
|
+
<>
|
|
1442
|
+
{" "}
|
|
1443
|
+
<a href={projectSettingsHref(key, "pack-token")}>{settings.packTokenEdit}</a>
|
|
1444
|
+
</>
|
|
1445
|
+
)}
|
|
1446
|
+
</dd>
|
|
1447
|
+
</div>
|
|
1194
1448
|
<div>
|
|
1195
1449
|
<dt>{form.symbol}</dt>
|
|
1196
1450
|
<dd>
|
|
@@ -2073,6 +2327,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
2073
2327
|
wide={
|
|
2074
2328
|
route.name === "board" ||
|
|
2075
2329
|
route.name === "calendar" ||
|
|
2330
|
+
(route.name === "goal" && route.form === undefined) ||
|
|
2076
2331
|
(route.name === "custom" &&
|
|
2077
2332
|
pages.some((candidate) => candidate.key === route.page && candidate.wide === true))
|
|
2078
2333
|
}
|
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/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
|
|
|
@@ -210,6 +222,8 @@ export const en = {
|
|
|
210
222
|
body: "Write the first task: what is to be done, and for whom.",
|
|
211
223
|
},
|
|
212
224
|
moveTo: "Move to",
|
|
225
|
+
grip: "Drag to another column",
|
|
226
|
+
archive: "Archive",
|
|
213
227
|
decisions: (n: number) => count(n, "decision", "decisions"),
|
|
214
228
|
decisionsTitle: "Decisions waiting for a person",
|
|
215
229
|
filters: {
|
|
@@ -265,6 +279,10 @@ export const en = {
|
|
|
265
279
|
: "",
|
|
266
280
|
started: "Started",
|
|
267
281
|
done: "Done",
|
|
282
|
+
archived: "Put away",
|
|
283
|
+
archivedAt: "Put away",
|
|
284
|
+
archive: "Put away",
|
|
285
|
+
restore: "Bring back",
|
|
268
286
|
form: {
|
|
269
287
|
title: "Title",
|
|
270
288
|
titlePlaceholder: "What is to be done, in plain words that stand on their own.",
|
|
@@ -394,6 +412,12 @@ export const en = {
|
|
|
394
412
|
setBy: "Set by",
|
|
395
413
|
state: "State",
|
|
396
414
|
nothingMore: "Nothing more was written about it.",
|
|
415
|
+
about: "What reaching it means",
|
|
416
|
+
archiveTasks: "Put the done tasks away",
|
|
417
|
+
archiveAsk: (n: number) =>
|
|
418
|
+
`Put the ${count(n, "task", "tasks")} done or dropped under this goal away as well?`,
|
|
419
|
+
view: "View",
|
|
420
|
+
views: { board: "Board", list: "List", calendar: "Calendar" },
|
|
397
421
|
timeline: {
|
|
398
422
|
title: "The months",
|
|
399
423
|
legend: "Category colours",
|
|
@@ -555,6 +579,8 @@ export const en = {
|
|
|
555
579
|
taskUpdated: (fields: string, task: string) => `changed ${fields} of ${task}`,
|
|
556
580
|
taskCommented: (task: string, excerpt: string | undefined) =>
|
|
557
581
|
`commented on ${task}${excerpt === undefined || excerpt === "" ? "" : `: ${excerpt}`}`,
|
|
582
|
+
taskArchived: (task: string) => `put ${task} away`,
|
|
583
|
+
taskRestored: (task: string) => `brought ${task} back`,
|
|
558
584
|
decisionRaised: (question: string) => `asked: ${question}`,
|
|
559
585
|
decisionAnswered: (question: string, option: string) => `answered "${question}": ${option}`,
|
|
560
586
|
decisionUpdated: (fields: string, question: string) =>
|
|
@@ -575,6 +601,19 @@ export const en = {
|
|
|
575
601
|
},
|
|
576
602
|
},
|
|
577
603
|
|
|
604
|
+
archive: {
|
|
605
|
+
title: "Archive",
|
|
606
|
+
lead: "Tasks done or dropped and put away: out of the board, the lists and the calendar, each still open by its number. Brought back from here.",
|
|
607
|
+
loading: "Loading the archive",
|
|
608
|
+
empty: {
|
|
609
|
+
title: "Nothing is put away",
|
|
610
|
+
body: "A task done or dropped is put away from its page, with its goal, or by the console after a while.",
|
|
611
|
+
},
|
|
612
|
+
since: "put away",
|
|
613
|
+
restore: "Bring back",
|
|
614
|
+
more: "Show more",
|
|
615
|
+
},
|
|
616
|
+
|
|
578
617
|
runs: {
|
|
579
618
|
for: "for",
|
|
580
619
|
since: "since",
|
|
@@ -601,6 +640,16 @@ export const en = {
|
|
|
601
640
|
picture: (name: string) => `The picture ${name}`,
|
|
602
641
|
label: (name: string) => `A comment by ${name}`,
|
|
603
642
|
},
|
|
643
|
+
pictures: {
|
|
644
|
+
open: (name: string) => `Open the picture ${name}`,
|
|
645
|
+
label: (name: string) => `The picture ${name}, large`,
|
|
646
|
+
close: "Close",
|
|
647
|
+
zoomIn: "Zoom in",
|
|
648
|
+
zoomOut: "Zoom out",
|
|
649
|
+
fit: "Fit to the screen",
|
|
650
|
+
file: "Open the file itself",
|
|
651
|
+
hint: "Zoom with the wheel, the buttons or two fingers; drag to move; Escape closes.",
|
|
652
|
+
},
|
|
604
653
|
|
|
605
654
|
skills: {
|
|
606
655
|
loading: "Loading the skills",
|
|
@@ -612,13 +661,15 @@ export const en = {
|
|
|
612
661
|
servedBy: "Served by",
|
|
613
662
|
at: "at",
|
|
614
663
|
connect:
|
|
615
|
-
"An agent reads these through its own MCP connection to this address, which a person makes once:",
|
|
664
|
+
"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
665
|
connectLink: "how, on the address's page at SkillCDN",
|
|
617
666
|
status: {
|
|
618
667
|
indexing: "SkillCDN is still indexing the repository. Look again in a moment.",
|
|
619
668
|
failed: "SkillCDN could not index the repository. What it found is on the address's page.",
|
|
620
669
|
not_found:
|
|
621
|
-
"SkillCDN does not serve this address, or
|
|
670
|
+
"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).",
|
|
671
|
+
token_invalid:
|
|
672
|
+
"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
673
|
unavailable: "SkillCDN could not be reached. The skills are as they were; look again later.",
|
|
623
674
|
},
|
|
624
675
|
none: {
|