@skillcdn/console 0.1.9 → 0.1.10
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 +5 -5
- package/dist/api.d.ts +5 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +255 -10
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +9 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +80 -0
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +62 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +31 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -3
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +27 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +75 -7
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +34 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +96 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/categories.d.ts +13 -0
- package/dist/components/categories.d.ts.map +1 -0
- package/dist/components/categories.js +52 -0
- package/dist/components/categories.js.map +1 -0
- package/dist/components/decision-list.d.ts +10 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +14 -1
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +12 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +35 -0
- package/dist/components/goals.d.ts.map +1 -0
- package/dist/components/goals.js +45 -0
- package/dist/components/goals.js.map +1 -0
- package/dist/components/task-form.d.ts +5 -1
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +13 -1
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.d.ts +16 -0
- package/dist/components/task-list.d.ts.map +1 -0
- package/dist/components/task-list.js +14 -0
- package/dist/components/task-list.js.map +1 -0
- package/dist/components/task-view.d.ts +9 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +10 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +30 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +60 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +10 -0
- package/dist/components/work.d.ts.map +1 -0
- package/dist/components/work.js +29 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +511 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +124 -8
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +36 -2
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +39 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/dates.js +120 -0
- package/dist/dates.js.map +1 -0
- package/dist/i18n/en.d.ts +162 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +174 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +169 -1
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +5 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +5 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +50 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +74 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +7 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +9 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +366 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +112 -3
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +15 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +26 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +11 -4
- package/docs/specs/rest.md +33 -8
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +30 -0
- package/src/cli/cli.ts +311 -10
- package/src/cli/format.ts +88 -0
- package/src/cli/help.ts +69 -7
- package/src/client.ts +70 -2
- package/src/components/board.tsx +205 -12
- package/src/components/calendar.tsx +255 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +62 -5
- package/src/components/event-feed.tsx +12 -0
- package/src/components/goals.tsx +264 -0
- package/src/components/task-form.tsx +78 -1
- package/src/components/task-list.tsx +61 -0
- package/src/components/task-view.tsx +74 -3
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +83 -0
- package/src/console.tsx +337 -4
- package/src/data.ts +63 -8
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +184 -1
- package/src/i18n/ko.ts +175 -1
- package/src/index.ts +58 -1
- package/src/limits.ts +5 -0
- package/src/router.ts +119 -6
- package/src/routes.ts +10 -0
- package/src/schemas.ts +141 -1
- package/src/styles/console.css +511 -0
- package/src/vocabulary.ts +33 -0
package/src/console.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { createRoot, type Root } from "react-dom/client";
|
|
|
3
3
|
import {
|
|
4
4
|
type ConsoleClient,
|
|
5
5
|
createClient,
|
|
6
|
+
OPEN_TASK_STATES,
|
|
6
7
|
type RestAnswerInput,
|
|
7
8
|
type RestDecision,
|
|
8
9
|
type RestDecisionInput,
|
|
@@ -15,13 +16,17 @@ import {
|
|
|
15
16
|
type RestTaskInput,
|
|
16
17
|
type RestToken,
|
|
17
18
|
type RestTokenCreated,
|
|
19
|
+
type RestWork,
|
|
18
20
|
type TaskState,
|
|
19
21
|
} from "./api.js";
|
|
20
|
-
import { Board, type BoardProps } from "./components/board.js";
|
|
22
|
+
import { Board, BoardFilters, type BoardProps, byDue, filterTasks } from "./components/board.js";
|
|
23
|
+
import { Calendar, type CalendarProps, Workload } from "./components/calendar.js";
|
|
24
|
+
import { CategoryList, type CategoryListProps } from "./components/categories.js";
|
|
21
25
|
import { ConnectWords } from "./components/connect.js";
|
|
22
26
|
import { DecisionForm } from "./components/decision-form.js";
|
|
23
27
|
import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
|
|
24
28
|
import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
|
|
29
|
+
import { GoalForm, GoalList, type GoalListProps, GoalView } from "./components/goals.js";
|
|
25
30
|
import { PeopleList, type PeopleListProps } from "./components/people.js";
|
|
26
31
|
import {
|
|
27
32
|
MemberList,
|
|
@@ -40,11 +45,14 @@ import {
|
|
|
40
45
|
import { SignIn, type SignInProps } from "./components/sign-in.js";
|
|
41
46
|
import { SkillList, type SkillListProps } from "./components/skills.js";
|
|
42
47
|
import { TaskForm } from "./components/task-form.js";
|
|
48
|
+
import { TaskList } from "./components/task-list.js";
|
|
43
49
|
import { TaskView, type TaskViewProps } from "./components/task-view.js";
|
|
44
50
|
import { NewToken, TokenForm, TokenList, type TokenListProps } from "./components/tokens.js";
|
|
45
51
|
import { Button, Callout, EmptyState, Spinner } from "./components/ui.js";
|
|
52
|
+
import { WorkList, type WorkListProps } from "./components/work.js";
|
|
46
53
|
import { ConnectPage } from "./connect-page.js";
|
|
47
54
|
import { type ConsoleData, useConsoleData } from "./data.js";
|
|
55
|
+
import { monthOf, today } from "./dates.js";
|
|
48
56
|
import { DocsPage } from "./docs-page.js";
|
|
49
57
|
import {
|
|
50
58
|
chooseLanguage,
|
|
@@ -54,11 +62,15 @@ import {
|
|
|
54
62
|
useWords,
|
|
55
63
|
} from "./i18n/index.js";
|
|
56
64
|
import {
|
|
65
|
+
boardHref,
|
|
66
|
+
calendarHref,
|
|
57
67
|
decisionHref,
|
|
58
68
|
docHref,
|
|
69
|
+
goalHref,
|
|
59
70
|
matchRoute,
|
|
60
71
|
type Navigation,
|
|
61
72
|
newDecisionHref,
|
|
73
|
+
newGoalHref,
|
|
62
74
|
newTaskHref,
|
|
63
75
|
PATHS,
|
|
64
76
|
personAgentsHref,
|
|
@@ -69,7 +81,7 @@ import {
|
|
|
69
81
|
withoutSignInParam,
|
|
70
82
|
} from "./router.js";
|
|
71
83
|
import { projectPath } from "./routes.js";
|
|
72
|
-
import { useAction } from "./use-action.js";
|
|
84
|
+
import { errorWords, useAction } from "./use-action.js";
|
|
73
85
|
|
|
74
86
|
// The composition of the default console: the pages assembled from the components, with the
|
|
75
87
|
// places a team may replace named. The UI the image serves is this, with the default config.
|
|
@@ -90,6 +102,10 @@ export interface ConsoleComponents {
|
|
|
90
102
|
readonly SkillList: ComponentType<SkillListProps>;
|
|
91
103
|
readonly ProjectList: ComponentType<ProjectListProps>;
|
|
92
104
|
readonly MemberList: ComponentType<MemberListProps>;
|
|
105
|
+
readonly Calendar: ComponentType<CalendarProps>;
|
|
106
|
+
readonly GoalList: ComponentType<GoalListProps>;
|
|
107
|
+
readonly WorkList: ComponentType<WorkListProps>;
|
|
108
|
+
readonly CategoryList: ComponentType<CategoryListProps>;
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
export const DEFAULT_COMPONENTS: ConsoleComponents = {
|
|
@@ -104,6 +120,10 @@ export const DEFAULT_COMPONENTS: ConsoleComponents = {
|
|
|
104
120
|
SkillList,
|
|
105
121
|
ProjectList,
|
|
106
122
|
MemberList,
|
|
123
|
+
Calendar,
|
|
124
|
+
GoalList,
|
|
125
|
+
WorkList,
|
|
126
|
+
CategoryList,
|
|
107
127
|
};
|
|
108
128
|
|
|
109
129
|
export interface ConsoleConfig {
|
|
@@ -426,8 +446,11 @@ function TaskPage(props: {
|
|
|
426
446
|
decisions={data.decisions}
|
|
427
447
|
runs={data.runs}
|
|
428
448
|
history={history}
|
|
449
|
+
categories={data.categories}
|
|
450
|
+
goals={data.goals}
|
|
429
451
|
taskHref={(candidate) => taskHref(key, candidate.number)}
|
|
430
452
|
decisionHref={(decisionId) => decisionHref(key, decisionId)}
|
|
453
|
+
goalHref={(goalId) => goalHref(key, goalId)}
|
|
431
454
|
fileHref={(artifact) => projectPath(key, "files", artifact.id)}
|
|
432
455
|
docHref={(path) => docHref(key, path)}
|
|
433
456
|
form={route.form}
|
|
@@ -483,6 +506,8 @@ function ProjectPage(props: {
|
|
|
483
506
|
void act(() => data.actions.updateDecision(decision.id, patch));
|
|
484
507
|
|
|
485
508
|
if (route.name === "board") {
|
|
509
|
+
const me = data.me?.person ?? undefined;
|
|
510
|
+
const shown = filterTasks(data.tasks, route.filter, me);
|
|
486
511
|
return (
|
|
487
512
|
<>
|
|
488
513
|
<div className="sc-page-head">
|
|
@@ -493,8 +518,21 @@ function ProjectPage(props: {
|
|
|
493
518
|
</div>
|
|
494
519
|
{project.description.length > 0 && <p className="sc-lead">{project.description}</p>}
|
|
495
520
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
521
|
+
{data.tasks.length > 0 && (
|
|
522
|
+
<BoardFilters
|
|
523
|
+
filter={route.filter}
|
|
524
|
+
categories={data.categories}
|
|
525
|
+
goals={data.goals}
|
|
526
|
+
people={data.people}
|
|
527
|
+
me={me}
|
|
528
|
+
shown={shown.length}
|
|
529
|
+
total={data.tasks.length}
|
|
530
|
+
onChange={(filter) => navigation.replace(boardHref(key, filter))}
|
|
531
|
+
/>
|
|
532
|
+
)}
|
|
496
533
|
<components.Board
|
|
497
|
-
tasks={
|
|
534
|
+
tasks={shown}
|
|
535
|
+
runs={data.runs}
|
|
498
536
|
taskHref={href}
|
|
499
537
|
onOpen={(task) => navigation.go(href(task))}
|
|
500
538
|
onMove={onMove}
|
|
@@ -522,6 +560,8 @@ function ProjectPage(props: {
|
|
|
522
560
|
<TaskForm
|
|
523
561
|
people={data.people}
|
|
524
562
|
parents={data.tasks}
|
|
563
|
+
categories={data.categories}
|
|
564
|
+
goals={data.goals}
|
|
525
565
|
busy={busy}
|
|
526
566
|
onSubmit={(input: RestTaskInput) =>
|
|
527
567
|
void act(async () => {
|
|
@@ -549,6 +589,183 @@ function ProjectPage(props: {
|
|
|
549
589
|
/>
|
|
550
590
|
);
|
|
551
591
|
}
|
|
592
|
+
if (route.name === "calendar") {
|
|
593
|
+
const now = today();
|
|
594
|
+
const month = route.month ?? monthOf(now);
|
|
595
|
+
const open = data.tasks.filter((task) => OPEN_TASK_STATES.includes(task.state));
|
|
596
|
+
const overdue = open.filter((task) => task.dueOn !== null && task.dueOn < now).sort(byDue);
|
|
597
|
+
const unscheduled = open.filter((task) => task.dueOn === null).sort(byDue);
|
|
598
|
+
const calendar = words.calendar;
|
|
599
|
+
return (
|
|
600
|
+
<>
|
|
601
|
+
<div className="sc-page-head">
|
|
602
|
+
<h1 className="sc-page-title">{calendar.title}</h1>
|
|
603
|
+
<LinkButton href={newTaskHref(key)} variant="primary">
|
|
604
|
+
{words.board.writeTask}
|
|
605
|
+
</LinkButton>
|
|
606
|
+
</div>
|
|
607
|
+
<p className="sc-lead">{calendar.lead}</p>
|
|
608
|
+
<components.Calendar
|
|
609
|
+
tasks={data.tasks}
|
|
610
|
+
month={month}
|
|
611
|
+
today={now}
|
|
612
|
+
taskHref={href}
|
|
613
|
+
monthHref={(wanted) => calendarHref(key, wanted)}
|
|
614
|
+
/>
|
|
615
|
+
<div className="sc-calendar-below">
|
|
616
|
+
<section className="sc-task-section" aria-label={calendar.overdue}>
|
|
617
|
+
<h2 className="sc-section-title">
|
|
618
|
+
{calendar.overdue} <span className="sc-column-count">{overdue.length}</span>
|
|
619
|
+
</h2>
|
|
620
|
+
<TaskList
|
|
621
|
+
tasks={overdue}
|
|
622
|
+
taskHref={href}
|
|
623
|
+
today={now}
|
|
624
|
+
showState
|
|
625
|
+
empty={<p className="sc-muted">{calendar.nothingOverdue}</p>}
|
|
626
|
+
/>
|
|
627
|
+
</section>
|
|
628
|
+
<section className="sc-task-section" aria-label={calendar.unscheduled}>
|
|
629
|
+
<h2 className="sc-section-title">
|
|
630
|
+
{calendar.unscheduled} <span className="sc-column-count">{unscheduled.length}</span>
|
|
631
|
+
</h2>
|
|
632
|
+
<TaskList
|
|
633
|
+
tasks={unscheduled}
|
|
634
|
+
taskHref={href}
|
|
635
|
+
today={now}
|
|
636
|
+
showState
|
|
637
|
+
empty={<p className="sc-muted">{calendar.allScheduled}</p>}
|
|
638
|
+
/>
|
|
639
|
+
</section>
|
|
640
|
+
</div>
|
|
641
|
+
{open.length > 0 && (
|
|
642
|
+
<section className="sc-task-section" aria-label={calendar.workload.title}>
|
|
643
|
+
<h2 className="sc-section-title">{calendar.workload.title}</h2>
|
|
644
|
+
<p className="sc-lead">{calendar.workload.lead}</p>
|
|
645
|
+
<Workload
|
|
646
|
+
tasks={data.tasks}
|
|
647
|
+
today={now}
|
|
648
|
+
personHref={(person) =>
|
|
649
|
+
boardHref(key, person === undefined ? {} : { person: person.id })
|
|
650
|
+
}
|
|
651
|
+
/>
|
|
652
|
+
</section>
|
|
653
|
+
)}
|
|
654
|
+
</>
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (route.name === "roadmap" || route.name === "new-goal") {
|
|
658
|
+
const roadmap = words.roadmap;
|
|
659
|
+
const making = route.name === "new-goal";
|
|
660
|
+
if (making && !owner) {
|
|
661
|
+
return <EmptyState title={roadmap.onlyOwner.title} body={roadmap.onlyOwner.body} />;
|
|
662
|
+
}
|
|
663
|
+
return (
|
|
664
|
+
<>
|
|
665
|
+
<div className="sc-page-head">
|
|
666
|
+
<h1 className="sc-page-title">{roadmap.title}</h1>
|
|
667
|
+
{!making && owner && (
|
|
668
|
+
<LinkButton href={newGoalHref(key)} variant="primary">
|
|
669
|
+
{roadmap.setGoal}
|
|
670
|
+
</LinkButton>
|
|
671
|
+
)}
|
|
672
|
+
</div>
|
|
673
|
+
<p className="sc-lead">{roadmap.lead}</p>
|
|
674
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
675
|
+
{making && (
|
|
676
|
+
<section className="sc-panel sc-panel-form" aria-label={roadmap.setGoalAria}>
|
|
677
|
+
<GoalForm
|
|
678
|
+
busy={busy}
|
|
679
|
+
onSubmit={(input) =>
|
|
680
|
+
void act(async () => {
|
|
681
|
+
const made = await data.actions.createGoal(input);
|
|
682
|
+
navigation.replace(goalHref(key, made.id));
|
|
683
|
+
})
|
|
684
|
+
}
|
|
685
|
+
onCancel={() => navigation.back(projectHref(key, "roadmap"))}
|
|
686
|
+
/>
|
|
687
|
+
</section>
|
|
688
|
+
)}
|
|
689
|
+
<components.GoalList
|
|
690
|
+
goals={data.goals}
|
|
691
|
+
goalHref={(goal) => goalHref(key, goal.id)}
|
|
692
|
+
empty={
|
|
693
|
+
<EmptyState
|
|
694
|
+
title={roadmap.empty.title}
|
|
695
|
+
body={roadmap.empty.body}
|
|
696
|
+
action={
|
|
697
|
+
owner ? (
|
|
698
|
+
<LinkButton href={newGoalHref(key)}>{roadmap.setGoal}</LinkButton>
|
|
699
|
+
) : undefined
|
|
700
|
+
}
|
|
701
|
+
/>
|
|
702
|
+
}
|
|
703
|
+
/>
|
|
704
|
+
</>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (route.name === "goal") {
|
|
708
|
+
const roadmap = words.roadmap;
|
|
709
|
+
const goal = data.goals.find((candidate) => candidate.id === route.id);
|
|
710
|
+
const here = goalHref(key, route.id);
|
|
711
|
+
if (goal === undefined) {
|
|
712
|
+
return (
|
|
713
|
+
<EmptyState
|
|
714
|
+
title={roadmap.noSuch.title}
|
|
715
|
+
body={roadmap.noSuch.body}
|
|
716
|
+
action={<LinkButton href={projectHref(key, "roadmap")}>{roadmap.all}</LinkButton>}
|
|
717
|
+
/>
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
if (route.form === "edit") {
|
|
721
|
+
if (!owner) {
|
|
722
|
+
return <EmptyState title={roadmap.onlyOwner.title} body={roadmap.onlyOwner.body} />;
|
|
723
|
+
}
|
|
724
|
+
return (
|
|
725
|
+
<>
|
|
726
|
+
<div className="sc-page-head">
|
|
727
|
+
<h1 className="sc-page-title">{goal.title}</h1>
|
|
728
|
+
</div>
|
|
729
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
730
|
+
<section className="sc-panel sc-panel-form" aria-label={roadmap.editAria}>
|
|
731
|
+
<GoalForm
|
|
732
|
+
goal={goal}
|
|
733
|
+
busy={busy}
|
|
734
|
+
onSubmit={(input) =>
|
|
735
|
+
void act(async () => {
|
|
736
|
+
await data.actions.updateGoal(goal.id, input);
|
|
737
|
+
navigation.replace(here);
|
|
738
|
+
})
|
|
739
|
+
}
|
|
740
|
+
onCancel={() => navigation.back(here)}
|
|
741
|
+
/>
|
|
742
|
+
</section>
|
|
743
|
+
</>
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
return (
|
|
747
|
+
<>
|
|
748
|
+
<div className="sc-page-head">
|
|
749
|
+
<h1 className="sc-page-title">{roadmap.one}</h1>
|
|
750
|
+
<LinkButton href={projectHref(key, "roadmap")}>{roadmap.all}</LinkButton>
|
|
751
|
+
</div>
|
|
752
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
753
|
+
<GoalView
|
|
754
|
+
goal={goal}
|
|
755
|
+
tasks={data.tasks}
|
|
756
|
+
taskHref={href}
|
|
757
|
+
docHref={toDoc}
|
|
758
|
+
editHref={owner ? goalHref(key, goal.id, "edit") : undefined}
|
|
759
|
+
onMove={
|
|
760
|
+
owner
|
|
761
|
+
? (target, state) => void act(() => data.actions.updateGoal(target.id, { state }))
|
|
762
|
+
: undefined
|
|
763
|
+
}
|
|
764
|
+
busy={busy}
|
|
765
|
+
/>
|
|
766
|
+
</>
|
|
767
|
+
);
|
|
768
|
+
}
|
|
552
769
|
if (route.name === "docs") {
|
|
553
770
|
return (
|
|
554
771
|
<DocsPage
|
|
@@ -650,6 +867,9 @@ function ProjectPage(props: {
|
|
|
650
867
|
if (event.decisionId !== null) {
|
|
651
868
|
return decisionHref(key, event.decisionId);
|
|
652
869
|
}
|
|
870
|
+
if (event.goalId !== null) {
|
|
871
|
+
return goalHref(key, event.goalId);
|
|
872
|
+
}
|
|
653
873
|
return event.data.path === undefined ? undefined : toDoc(event.data.path);
|
|
654
874
|
}}
|
|
655
875
|
empty={<EmptyState title={words.feed.empty} />}
|
|
@@ -822,6 +1042,55 @@ function PersonAgentsPage(props: {
|
|
|
822
1042
|
);
|
|
823
1043
|
}
|
|
824
1044
|
|
|
1045
|
+
/** What is on the person's plate, across their projects: read when the page is opened. */
|
|
1046
|
+
function WorkPage(props: { readonly data: ConsoleData; readonly components: ConsoleComponents }) {
|
|
1047
|
+
const words = useWords();
|
|
1048
|
+
const { data, components } = props;
|
|
1049
|
+
const [work, setWork] = useState<RestWork | undefined>(undefined);
|
|
1050
|
+
const [problem, setProblem] = useState<string | undefined>(undefined);
|
|
1051
|
+
const { work: load } = data.actions;
|
|
1052
|
+
const couldNotLoad = words.work.couldNotLoad;
|
|
1053
|
+
useEffect(() => {
|
|
1054
|
+
const controller = new AbortController();
|
|
1055
|
+
load(controller.signal)
|
|
1056
|
+
.then((answer) => {
|
|
1057
|
+
if (!controller.signal.aborted) {
|
|
1058
|
+
setWork(answer);
|
|
1059
|
+
}
|
|
1060
|
+
})
|
|
1061
|
+
.catch((failure: unknown) => {
|
|
1062
|
+
if (!controller.signal.aborted) {
|
|
1063
|
+
setProblem(errorWords(failure, couldNotLoad));
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
return () => controller.abort();
|
|
1067
|
+
}, [load, couldNotLoad]);
|
|
1068
|
+
return (
|
|
1069
|
+
<>
|
|
1070
|
+
<div className="sc-page-head">
|
|
1071
|
+
<h1 className="sc-page-title">{words.work.title}</h1>
|
|
1072
|
+
</div>
|
|
1073
|
+
<p className="sc-lead">{words.work.lead}</p>
|
|
1074
|
+
{problem !== undefined && <Callout tone="danger">{problem}</Callout>}
|
|
1075
|
+
{work === undefined ? (
|
|
1076
|
+
problem === undefined && (
|
|
1077
|
+
<div className="sc-loading">
|
|
1078
|
+
<Spinner label={words.work.loading} />
|
|
1079
|
+
</div>
|
|
1080
|
+
)
|
|
1081
|
+
) : work.tasks.length === 0 && work.decisions.length === 0 ? (
|
|
1082
|
+
<EmptyState title={words.work.empty.title} body={words.work.empty.body} />
|
|
1083
|
+
) : (
|
|
1084
|
+
<components.WorkList
|
|
1085
|
+
work={work}
|
|
1086
|
+
taskHref={(projectKey, task) => taskHref(projectKey, task.number)}
|
|
1087
|
+
decisionHref={(projectKey, decisionId) => decisionHref(projectKey, decisionId)}
|
|
1088
|
+
/>
|
|
1089
|
+
)}
|
|
1090
|
+
</>
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
825
1094
|
function WorkspacePage(props: {
|
|
826
1095
|
readonly route: Route;
|
|
827
1096
|
readonly data: ConsoleData;
|
|
@@ -833,6 +1102,48 @@ function WorkspacePage(props: {
|
|
|
833
1102
|
const [fresh, setFresh] = useState<RestTokenCreated | undefined>(undefined);
|
|
834
1103
|
const { busy, error, act } = useAction();
|
|
835
1104
|
|
|
1105
|
+
if (route.name === "work") {
|
|
1106
|
+
return <WorkPage data={data} components={components} />;
|
|
1107
|
+
}
|
|
1108
|
+
if (route.name === "categories") {
|
|
1109
|
+
const me = data.me?.person ?? undefined;
|
|
1110
|
+
const administrator = me?.role === "admin" && !holdsToken(data);
|
|
1111
|
+
const categories = words.categories;
|
|
1112
|
+
return (
|
|
1113
|
+
<>
|
|
1114
|
+
<div className="sc-page-head">
|
|
1115
|
+
<h1 className="sc-page-title">{categories.title}</h1>
|
|
1116
|
+
</div>
|
|
1117
|
+
<p className="sc-lead">
|
|
1118
|
+
{categories.lead} {categories.onlyAdmin}
|
|
1119
|
+
</p>
|
|
1120
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1121
|
+
<components.CategoryList
|
|
1122
|
+
categories={data.categories}
|
|
1123
|
+
busy={busy}
|
|
1124
|
+
onAdd={
|
|
1125
|
+
administrator
|
|
1126
|
+
? (input) => void act(() => data.actions.createCategory(input))
|
|
1127
|
+
: undefined
|
|
1128
|
+
}
|
|
1129
|
+
onChange={
|
|
1130
|
+
administrator
|
|
1131
|
+
? (category, patch) =>
|
|
1132
|
+
void act(() => data.actions.updateCategory(category.key, patch))
|
|
1133
|
+
: undefined
|
|
1134
|
+
}
|
|
1135
|
+
onRemove={
|
|
1136
|
+
administrator
|
|
1137
|
+
? (category) => void act(() => data.actions.removeCategory(category.key))
|
|
1138
|
+
: undefined
|
|
1139
|
+
}
|
|
1140
|
+
empty={<EmptyState title={categories.empty.title} body={categories.empty.body} />}
|
|
1141
|
+
/>
|
|
1142
|
+
{administrator && <p className="sc-muted">{categories.inUse}</p>}
|
|
1143
|
+
</>
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
836
1147
|
if (route.name === "people") {
|
|
837
1148
|
const me = data.me?.person ?? undefined;
|
|
838
1149
|
const administrator = me?.role === "admin" && !holdsToken(data);
|
|
@@ -998,6 +1309,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
998
1309
|
label: words.nav.projects,
|
|
999
1310
|
current: route.name === "projects" || route.name === "new-project",
|
|
1000
1311
|
},
|
|
1312
|
+
{ href: PATHS.work, label: words.nav.work, current: route.name === "work" },
|
|
1001
1313
|
{
|
|
1002
1314
|
href: PATHS.people,
|
|
1003
1315
|
label: words.nav.people,
|
|
@@ -1012,6 +1324,16 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1012
1324
|
current: route.name === "agents" || route.name === "new-token",
|
|
1013
1325
|
},
|
|
1014
1326
|
]),
|
|
1327
|
+
// The categories are read by everyone and kept by an administrator; only they need the page.
|
|
1328
|
+
...(data.me.person.role === "admin" && !viaToken
|
|
1329
|
+
? [
|
|
1330
|
+
{
|
|
1331
|
+
href: PATHS.categories,
|
|
1332
|
+
label: words.nav.categories,
|
|
1333
|
+
current: route.name === "categories",
|
|
1334
|
+
},
|
|
1335
|
+
]
|
|
1336
|
+
: []),
|
|
1015
1337
|
];
|
|
1016
1338
|
const nav: NavItem[] =
|
|
1017
1339
|
projectKey === undefined
|
|
@@ -1023,6 +1345,17 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1023
1345
|
current:
|
|
1024
1346
|
route.name === "board" || route.name === "task" || route.name === "new-task",
|
|
1025
1347
|
},
|
|
1348
|
+
{
|
|
1349
|
+
href: projectHref(projectKey, "roadmap"),
|
|
1350
|
+
label: words.nav.roadmap,
|
|
1351
|
+
current:
|
|
1352
|
+
route.name === "roadmap" || route.name === "goal" || route.name === "new-goal",
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
href: projectHref(projectKey, "calendar"),
|
|
1356
|
+
label: words.nav.calendar,
|
|
1357
|
+
current: route.name === "calendar",
|
|
1358
|
+
},
|
|
1026
1359
|
{
|
|
1027
1360
|
href: projectHref(projectKey, "docs"),
|
|
1028
1361
|
label: words.nav.docs,
|
|
@@ -1140,7 +1473,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1140
1473
|
}}
|
|
1141
1474
|
person={data.me.person}
|
|
1142
1475
|
live={projectKey === undefined ? undefined : data.live}
|
|
1143
|
-
wide={route.name === "board"}
|
|
1476
|
+
wide={route.name === "board" || route.name === "calendar"}
|
|
1144
1477
|
onNavigate={(href) => navigation.go(href)}
|
|
1145
1478
|
onSignOut={viaToken ? undefined : () => void data.actions.signOut()}
|
|
1146
1479
|
>
|
package/src/data.ts
CHANGED
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
EVENT_KINDS,
|
|
7
7
|
type FileUpload,
|
|
8
8
|
type RestAnswerInput,
|
|
9
|
+
type RestCategory,
|
|
10
|
+
type RestCategoryInput,
|
|
11
|
+
type RestCategoryPatch,
|
|
9
12
|
type RestConnectRequest,
|
|
10
13
|
type RestDecision,
|
|
11
14
|
type RestDecisionInput,
|
|
@@ -14,6 +17,9 @@ import {
|
|
|
14
17
|
type RestDocumentInput,
|
|
15
18
|
type RestDocuments,
|
|
16
19
|
type RestEvent,
|
|
20
|
+
type RestGoal,
|
|
21
|
+
type RestGoalInput,
|
|
22
|
+
type RestGoalPatch,
|
|
17
23
|
type RestMe,
|
|
18
24
|
type RestMember,
|
|
19
25
|
type RestMemberInput,
|
|
@@ -35,6 +41,7 @@ import {
|
|
|
35
41
|
type RestTokens,
|
|
36
42
|
type RestVersion,
|
|
37
43
|
type RestVersions,
|
|
44
|
+
type RestWork,
|
|
38
45
|
restEventSchema,
|
|
39
46
|
} from "./api.js";
|
|
40
47
|
|
|
@@ -64,6 +71,15 @@ export interface ConsoleActions {
|
|
|
64
71
|
answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
|
|
65
72
|
/** Grows a decision's record: its context, or what followed. */
|
|
66
73
|
updateDecision(id: string, patch: RestDecisionPatch): Promise<RestDecision>;
|
|
74
|
+
/** Sets a goal on the roadmap of the project the page is on; for an owner. */
|
|
75
|
+
createGoal(input: RestGoalInput): Promise<RestGoal>;
|
|
76
|
+
updateGoal(id: string, patch: RestGoalPatch): Promise<RestGoal>;
|
|
77
|
+
/** What is on the person's plate across their projects, read when the page asks. */
|
|
78
|
+
work(signal?: AbortSignal): Promise<RestWork>;
|
|
79
|
+
/** The categories tasks are sorted into: kept by an administrator. */
|
|
80
|
+
createCategory(input: RestCategoryInput): Promise<RestCategory>;
|
|
81
|
+
updateCategory(key: string, patch: RestCategoryPatch): Promise<RestCategory>;
|
|
82
|
+
removeCategory(key: string): Promise<void>;
|
|
67
83
|
/** A folder's pages and folders, or the pages a search finds, in the project the page is on. */
|
|
68
84
|
documents(filter?: DocumentFilter, signal?: AbortSignal): Promise<RestDocuments>;
|
|
69
85
|
document(path: string, signal?: AbortSignal): Promise<RestDocument>;
|
|
@@ -104,6 +120,8 @@ export interface ConsoleData {
|
|
|
104
120
|
/** The projects the person may see, by name. */
|
|
105
121
|
readonly projects: readonly RestProject[];
|
|
106
122
|
readonly people: readonly RestPerson[];
|
|
123
|
+
/** The categories tasks are sorted into, by name. */
|
|
124
|
+
readonly categories: readonly RestCategory[];
|
|
107
125
|
/** The tokens of whoever is signed in, newest first. */
|
|
108
126
|
readonly tokens: readonly RestToken[];
|
|
109
127
|
/** The project the page is on, once loaded; `undefined` while loading, or when the page is on none. */
|
|
@@ -116,6 +134,8 @@ export interface ConsoleData {
|
|
|
116
134
|
readonly live: boolean;
|
|
117
135
|
readonly tasks: readonly RestTask[];
|
|
118
136
|
readonly decisions: readonly RestDecision[];
|
|
137
|
+
/** The roadmap of the project: its goals, the active first. */
|
|
138
|
+
readonly goals: readonly RestGoal[];
|
|
119
139
|
/** The runs of the project, newest first. */
|
|
120
140
|
readonly runs: readonly RestRun[];
|
|
121
141
|
/** The project's events, oldest first. */
|
|
@@ -161,6 +181,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
161
181
|
const [loaded, setLoaded] = useState(false);
|
|
162
182
|
const [projects, setProjects] = useState<readonly RestProject[]>([]);
|
|
163
183
|
const [people, setPeople] = useState<readonly RestPerson[]>([]);
|
|
184
|
+
const [categories, setCategories] = useState<readonly RestCategory[]>([]);
|
|
164
185
|
const [tokens, setTokens] = useState<readonly RestToken[]>([]);
|
|
165
186
|
const [project, setProject] = useState<RestProject | undefined>(undefined);
|
|
166
187
|
const [projectLoaded, setProjectLoaded] = useState(false);
|
|
@@ -168,6 +189,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
168
189
|
const [live, setLive] = useState(false);
|
|
169
190
|
const [tasks, setTasks] = useState<readonly RestTask[]>([]);
|
|
170
191
|
const [decisions, setDecisions] = useState<readonly RestDecision[]>([]);
|
|
192
|
+
const [goals, setGoals] = useState<readonly RestGoal[]>([]);
|
|
171
193
|
const [runs, setRuns] = useState<readonly RestRun[]>([]);
|
|
172
194
|
const [events, setEvents] = useState<readonly RestEvent[]>([]);
|
|
173
195
|
const [members, setMembers] = useState<readonly RestMember[]>([]);
|
|
@@ -181,15 +203,17 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
181
203
|
/** Loads the workspace's lists again. What fails stays as it was. */
|
|
182
204
|
const refreshWorkspace = useCallback(
|
|
183
205
|
async (signal?: AbortSignal) => {
|
|
184
|
-
const [nextProjects, nextPeople] = await Promise.all([
|
|
206
|
+
const [nextProjects, nextPeople, nextCategories] = await Promise.all([
|
|
185
207
|
client.projects(signal),
|
|
186
208
|
client.people(signal),
|
|
209
|
+
client.categories(signal),
|
|
187
210
|
]);
|
|
188
211
|
if (signal?.aborted === true) {
|
|
189
212
|
return;
|
|
190
213
|
}
|
|
191
214
|
setProjects(nextProjects.items);
|
|
192
215
|
setPeople(nextPeople.items);
|
|
216
|
+
setCategories(nextCategories.items);
|
|
193
217
|
},
|
|
194
218
|
[client],
|
|
195
219
|
);
|
|
@@ -201,19 +225,22 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
201
225
|
return;
|
|
202
226
|
}
|
|
203
227
|
const scope = client.project(projectKey);
|
|
204
|
-
const [nextProject, nextTasks, nextDecisions, nextRuns, nextMembers] =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
const [nextProject, nextTasks, nextDecisions, nextGoals, nextRuns, nextMembers] =
|
|
229
|
+
await Promise.all([
|
|
230
|
+
scope.get(signal),
|
|
231
|
+
scope.tasks(undefined, signal),
|
|
232
|
+
scope.decisions(undefined, signal),
|
|
233
|
+
scope.goals(signal),
|
|
234
|
+
scope.runs(undefined, signal),
|
|
235
|
+
scope.members(signal),
|
|
236
|
+
]);
|
|
211
237
|
if (signal?.aborted === true) {
|
|
212
238
|
return;
|
|
213
239
|
}
|
|
214
240
|
setProject(nextProject);
|
|
215
241
|
setTasks(nextTasks.items);
|
|
216
242
|
setDecisions(nextDecisions.items);
|
|
243
|
+
setGoals(nextGoals.items);
|
|
217
244
|
setRuns(nextRuns.items);
|
|
218
245
|
setMembers(nextMembers.items);
|
|
219
246
|
},
|
|
@@ -274,6 +301,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
274
301
|
setProjectError(undefined);
|
|
275
302
|
setTasks([]);
|
|
276
303
|
setDecisions([]);
|
|
304
|
+
setGoals([]);
|
|
277
305
|
setRuns([]);
|
|
278
306
|
setEvents([]);
|
|
279
307
|
setMembers([]);
|
|
@@ -411,6 +439,31 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
411
439
|
);
|
|
412
440
|
return decision;
|
|
413
441
|
},
|
|
442
|
+
async createGoal(input) {
|
|
443
|
+
const goal = await scope().createGoal(input);
|
|
444
|
+
await refreshProject();
|
|
445
|
+
return goal;
|
|
446
|
+
},
|
|
447
|
+
async updateGoal(id, patch) {
|
|
448
|
+
const goal = await scope().updateGoal(id, patch);
|
|
449
|
+
setGoals((current) => current.map((candidate) => (candidate.id === id ? goal : candidate)));
|
|
450
|
+
return goal;
|
|
451
|
+
},
|
|
452
|
+
work: (signal) => client.work(signal),
|
|
453
|
+
async createCategory(input) {
|
|
454
|
+
const category = await client.createCategory(input);
|
|
455
|
+
setCategories((await client.categories()).items);
|
|
456
|
+
return category;
|
|
457
|
+
},
|
|
458
|
+
async updateCategory(key, patch) {
|
|
459
|
+
const category = await client.updateCategory(key, patch);
|
|
460
|
+
setCategories((await client.categories()).items);
|
|
461
|
+
return category;
|
|
462
|
+
},
|
|
463
|
+
async removeCategory(key) {
|
|
464
|
+
await client.removeCategory(key);
|
|
465
|
+
setCategories((current) => current.filter((category) => category.key !== key));
|
|
466
|
+
},
|
|
414
467
|
documents: (filter, signal) => scope().documents(filter, signal),
|
|
415
468
|
document: (path, signal) => scope().document(path, signal),
|
|
416
469
|
writeDocument: (path, input) => scope().writeDocument(path, input),
|
|
@@ -476,6 +529,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
476
529
|
loaded,
|
|
477
530
|
projects,
|
|
478
531
|
people,
|
|
532
|
+
categories,
|
|
479
533
|
tokens,
|
|
480
534
|
project,
|
|
481
535
|
projectLoaded,
|
|
@@ -483,6 +537,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
483
537
|
live,
|
|
484
538
|
tasks,
|
|
485
539
|
decisions,
|
|
540
|
+
goals,
|
|
486
541
|
runs,
|
|
487
542
|
events,
|
|
488
543
|
members,
|