@skillcdn/console 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/api.d.ts +6 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +303 -18
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +11 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +87 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +70 -14
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +44 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -4
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +24 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +66 -5
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +54 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +156 -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 +16 -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 +23 -7
- 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/paged.d.ts +14 -0
- package/dist/components/paged.d.ts.map +1 -0
- package/dist/components/paged.js +28 -0
- package/dist/components/paged.js.map +1 -0
- package/dist/components/projects.d.ts +22 -2
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +35 -7
- package/dist/components/projects.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +2 -2
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts +2 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +1 -1
- package/dist/components/shell.js.map +1 -1
- 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 +16 -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 +33 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +658 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +145 -13
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +19 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +51 -4
- 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 +172 -5
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +183 -6
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +178 -6
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +11 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -0
- package/dist/limits.js.map +1 -1
- package/dist/marks.d.ts +12 -0
- package/dist/marks.d.ts.map +1 -0
- package/dist/marks.js +31 -0
- package/dist/marks.js.map +1 -0
- package/dist/router.d.ts +51 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +80 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +14 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +17 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +489 -3
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +150 -9
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +21 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +31 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +17 -8
- package/docs/specs/rest.md +48 -17
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +42 -0
- package/src/cli/cli.ts +362 -18
- package/src/cli/format.ts +96 -6
- package/src/cli/help.ts +77 -15
- package/src/client.ts +98 -7
- package/src/components/board.tsx +173 -13
- package/src/components/calendar.tsx +367 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +67 -5
- package/src/components/event-feed.tsx +46 -27
- package/src/components/goals.tsx +264 -0
- package/src/components/paged.tsx +39 -0
- package/src/components/projects.tsx +128 -10
- package/src/components/runs.tsx +7 -1
- package/src/components/shell.tsx +5 -2
- package/src/components/task-form.tsx +78 -1
- package/src/components/task-list.tsx +64 -0
- package/src/components/task-view.tsx +75 -4
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +93 -0
- package/src/console.tsx +376 -9
- package/src/data.ts +83 -12
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +193 -6
- package/src/i18n/ko.ts +184 -6
- package/src/index.ts +62 -1
- package/src/limits.ts +11 -0
- package/src/marks.ts +40 -0
- package/src/router.ts +131 -6
- package/src/routes.ts +19 -0
- package/src/schemas.ts +187 -7
- package/src/styles/console.css +658 -0
- package/src/vocabulary.ts +43 -0
package/src/console.tsx
CHANGED
|
@@ -3,6 +3,8 @@ import { createRoot, type Root } from "react-dom/client";
|
|
|
3
3
|
import {
|
|
4
4
|
type ConsoleClient,
|
|
5
5
|
createClient,
|
|
6
|
+
type FileUpload,
|
|
7
|
+
OPEN_TASK_STATES,
|
|
6
8
|
type RestAnswerInput,
|
|
7
9
|
type RestDecision,
|
|
8
10
|
type RestDecisionInput,
|
|
@@ -15,13 +17,17 @@ import {
|
|
|
15
17
|
type RestTaskInput,
|
|
16
18
|
type RestToken,
|
|
17
19
|
type RestTokenCreated,
|
|
20
|
+
type RestWork,
|
|
18
21
|
type TaskState,
|
|
19
22
|
} from "./api.js";
|
|
20
|
-
import { Board, type BoardProps } from "./components/board.js";
|
|
23
|
+
import { Board, BoardFilters, type BoardProps, byDue, filterTasks } from "./components/board.js";
|
|
24
|
+
import { Calendar, type CalendarProps, Workload } from "./components/calendar.js";
|
|
25
|
+
import { CategoryList, type CategoryListProps } from "./components/categories.js";
|
|
21
26
|
import { ConnectWords } from "./components/connect.js";
|
|
22
27
|
import { DecisionForm } from "./components/decision-form.js";
|
|
23
28
|
import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
|
|
24
29
|
import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
|
|
30
|
+
import { GoalForm, GoalList, type GoalListProps, GoalView } from "./components/goals.js";
|
|
25
31
|
import { PeopleList, type PeopleListProps } from "./components/people.js";
|
|
26
32
|
import {
|
|
27
33
|
MemberList,
|
|
@@ -29,6 +35,7 @@ import {
|
|
|
29
35
|
ProjectForm,
|
|
30
36
|
ProjectList,
|
|
31
37
|
type ProjectListProps,
|
|
38
|
+
ProjectMark,
|
|
32
39
|
} from "./components/projects.js";
|
|
33
40
|
import {
|
|
34
41
|
type Brand,
|
|
@@ -40,11 +47,14 @@ import {
|
|
|
40
47
|
import { SignIn, type SignInProps } from "./components/sign-in.js";
|
|
41
48
|
import { SkillList, type SkillListProps } from "./components/skills.js";
|
|
42
49
|
import { TaskForm } from "./components/task-form.js";
|
|
50
|
+
import { TaskList } from "./components/task-list.js";
|
|
43
51
|
import { TaskView, type TaskViewProps } from "./components/task-view.js";
|
|
44
52
|
import { NewToken, TokenForm, TokenList, type TokenListProps } from "./components/tokens.js";
|
|
45
53
|
import { Button, Callout, EmptyState, Spinner } from "./components/ui.js";
|
|
54
|
+
import { WorkList, type WorkListProps } from "./components/work.js";
|
|
46
55
|
import { ConnectPage } from "./connect-page.js";
|
|
47
56
|
import { type ConsoleData, useConsoleData } from "./data.js";
|
|
57
|
+
import { monthOf, today } from "./dates.js";
|
|
48
58
|
import { DocsPage } from "./docs-page.js";
|
|
49
59
|
import {
|
|
50
60
|
chooseLanguage,
|
|
@@ -54,11 +64,15 @@ import {
|
|
|
54
64
|
useWords,
|
|
55
65
|
} from "./i18n/index.js";
|
|
56
66
|
import {
|
|
67
|
+
boardHref,
|
|
68
|
+
calendarHref,
|
|
57
69
|
decisionHref,
|
|
58
70
|
docHref,
|
|
71
|
+
goalHref,
|
|
59
72
|
matchRoute,
|
|
60
73
|
type Navigation,
|
|
61
74
|
newDecisionHref,
|
|
75
|
+
newGoalHref,
|
|
62
76
|
newTaskHref,
|
|
63
77
|
PATHS,
|
|
64
78
|
personAgentsHref,
|
|
@@ -69,7 +83,7 @@ import {
|
|
|
69
83
|
withoutSignInParam,
|
|
70
84
|
} from "./router.js";
|
|
71
85
|
import { projectPath } from "./routes.js";
|
|
72
|
-
import { useAction } from "./use-action.js";
|
|
86
|
+
import { errorWords, useAction } from "./use-action.js";
|
|
73
87
|
|
|
74
88
|
// The composition of the default console: the pages assembled from the components, with the
|
|
75
89
|
// places a team may replace named. The UI the image serves is this, with the default config.
|
|
@@ -90,6 +104,10 @@ export interface ConsoleComponents {
|
|
|
90
104
|
readonly SkillList: ComponentType<SkillListProps>;
|
|
91
105
|
readonly ProjectList: ComponentType<ProjectListProps>;
|
|
92
106
|
readonly MemberList: ComponentType<MemberListProps>;
|
|
107
|
+
readonly Calendar: ComponentType<CalendarProps>;
|
|
108
|
+
readonly GoalList: ComponentType<GoalListProps>;
|
|
109
|
+
readonly WorkList: ComponentType<WorkListProps>;
|
|
110
|
+
readonly CategoryList: ComponentType<CategoryListProps>;
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
export const DEFAULT_COMPONENTS: ConsoleComponents = {
|
|
@@ -104,6 +122,10 @@ export const DEFAULT_COMPONENTS: ConsoleComponents = {
|
|
|
104
122
|
SkillList,
|
|
105
123
|
ProjectList,
|
|
106
124
|
MemberList,
|
|
125
|
+
Calendar,
|
|
126
|
+
GoalList,
|
|
127
|
+
WorkList,
|
|
128
|
+
CategoryList,
|
|
107
129
|
};
|
|
108
130
|
|
|
109
131
|
export interface ConsoleConfig {
|
|
@@ -334,9 +356,12 @@ function ProjectsPage(props: {
|
|
|
334
356
|
<section className="sc-panel sc-panel-form" aria-label={words.form.aria}>
|
|
335
357
|
<ProjectForm
|
|
336
358
|
busy={busy}
|
|
337
|
-
onSubmit={(input) =>
|
|
359
|
+
onSubmit={(input, icon) =>
|
|
338
360
|
void act(async () => {
|
|
339
|
-
const made = await data.actions.createProject(
|
|
361
|
+
const made = await data.actions.createProject(
|
|
362
|
+
input as RestProjectInput,
|
|
363
|
+
icon instanceof File ? uploadOf(icon) : undefined,
|
|
364
|
+
);
|
|
340
365
|
navigation.replace(projectHref(made.key));
|
|
341
366
|
})
|
|
342
367
|
}
|
|
@@ -426,8 +451,11 @@ function TaskPage(props: {
|
|
|
426
451
|
decisions={data.decisions}
|
|
427
452
|
runs={data.runs}
|
|
428
453
|
history={history}
|
|
454
|
+
categories={data.categories}
|
|
455
|
+
goals={data.goals}
|
|
429
456
|
taskHref={(candidate) => taskHref(key, candidate.number)}
|
|
430
457
|
decisionHref={(decisionId) => decisionHref(key, decisionId)}
|
|
458
|
+
goalHref={(goalId) => goalHref(key, goalId)}
|
|
431
459
|
fileHref={(artifact) => projectPath(key, "files", artifact.id)}
|
|
432
460
|
docHref={(path) => docHref(key, path)}
|
|
433
461
|
form={route.form}
|
|
@@ -483,6 +511,8 @@ function ProjectPage(props: {
|
|
|
483
511
|
void act(() => data.actions.updateDecision(decision.id, patch));
|
|
484
512
|
|
|
485
513
|
if (route.name === "board") {
|
|
514
|
+
const me = data.me?.person ?? undefined;
|
|
515
|
+
const shown = filterTasks(data.tasks, route.filter, me);
|
|
486
516
|
return (
|
|
487
517
|
<>
|
|
488
518
|
<div className="sc-page-head">
|
|
@@ -493,8 +523,20 @@ function ProjectPage(props: {
|
|
|
493
523
|
</div>
|
|
494
524
|
{project.description.length > 0 && <p className="sc-lead">{project.description}</p>}
|
|
495
525
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
526
|
+
{data.tasks.length > 0 && (
|
|
527
|
+
<BoardFilters
|
|
528
|
+
filter={route.filter}
|
|
529
|
+
categories={data.categories}
|
|
530
|
+
goals={data.goals}
|
|
531
|
+
people={data.people}
|
|
532
|
+
me={me}
|
|
533
|
+
shown={shown.length}
|
|
534
|
+
total={data.tasks.length}
|
|
535
|
+
onChange={(filter) => navigation.replace(boardHref(key, filter))}
|
|
536
|
+
/>
|
|
537
|
+
)}
|
|
496
538
|
<components.Board
|
|
497
|
-
tasks={
|
|
539
|
+
tasks={shown}
|
|
498
540
|
taskHref={href}
|
|
499
541
|
onOpen={(task) => navigation.go(href(task))}
|
|
500
542
|
onMove={onMove}
|
|
@@ -522,6 +564,8 @@ function ProjectPage(props: {
|
|
|
522
564
|
<TaskForm
|
|
523
565
|
people={data.people}
|
|
524
566
|
parents={data.tasks}
|
|
567
|
+
categories={data.categories}
|
|
568
|
+
goals={data.goals}
|
|
525
569
|
busy={busy}
|
|
526
570
|
onSubmit={(input: RestTaskInput) =>
|
|
527
571
|
void act(async () => {
|
|
@@ -549,6 +593,198 @@ function ProjectPage(props: {
|
|
|
549
593
|
/>
|
|
550
594
|
);
|
|
551
595
|
}
|
|
596
|
+
if (route.name === "calendar") {
|
|
597
|
+
const now = today();
|
|
598
|
+
const month = route.month ?? monthOf(now);
|
|
599
|
+
const me = data.me?.person ?? undefined;
|
|
600
|
+
const shown = filterTasks(data.tasks, route.filter, me);
|
|
601
|
+
const open = shown.filter((task) => OPEN_TASK_STATES.includes(task.state));
|
|
602
|
+
const overdue = open.filter((task) => task.dueOn !== null && task.dueOn < now).sort(byDue);
|
|
603
|
+
const unscheduled = open.filter((task) => task.dueOn === null).sort(byDue);
|
|
604
|
+
const calendar = words.calendar;
|
|
605
|
+
return (
|
|
606
|
+
<>
|
|
607
|
+
<div className="sc-page-head">
|
|
608
|
+
<h1 className="sc-page-title">{calendar.title}</h1>
|
|
609
|
+
<LinkButton href={newTaskHref(key)} variant="primary">
|
|
610
|
+
{words.board.writeTask}
|
|
611
|
+
</LinkButton>
|
|
612
|
+
</div>
|
|
613
|
+
<p className="sc-lead">{calendar.lead}</p>
|
|
614
|
+
{data.tasks.length > 0 && (
|
|
615
|
+
<BoardFilters
|
|
616
|
+
filter={route.filter}
|
|
617
|
+
categories={data.categories}
|
|
618
|
+
goals={data.goals}
|
|
619
|
+
people={data.people}
|
|
620
|
+
me={me}
|
|
621
|
+
shown={shown.length}
|
|
622
|
+
total={data.tasks.length}
|
|
623
|
+
onChange={(filter) => navigation.replace(calendarHref(key, route.month, filter))}
|
|
624
|
+
/>
|
|
625
|
+
)}
|
|
626
|
+
<components.Calendar
|
|
627
|
+
tasks={shown}
|
|
628
|
+
month={month}
|
|
629
|
+
today={now}
|
|
630
|
+
categories={data.categories}
|
|
631
|
+
taskHref={href}
|
|
632
|
+
monthHref={(wanted) => calendarHref(key, wanted, route.filter)}
|
|
633
|
+
/>
|
|
634
|
+
<div className="sc-calendar-below">
|
|
635
|
+
<section className="sc-task-section" aria-label={calendar.overdue}>
|
|
636
|
+
<h2 className="sc-section-title">
|
|
637
|
+
{calendar.overdue} <span className="sc-column-count">{overdue.length}</span>
|
|
638
|
+
</h2>
|
|
639
|
+
<TaskList
|
|
640
|
+
tasks={overdue}
|
|
641
|
+
taskHref={href}
|
|
642
|
+
today={now}
|
|
643
|
+
showState
|
|
644
|
+
empty={<p className="sc-muted">{calendar.nothingOverdue}</p>}
|
|
645
|
+
/>
|
|
646
|
+
</section>
|
|
647
|
+
<section className="sc-task-section" aria-label={calendar.unscheduled}>
|
|
648
|
+
<h2 className="sc-section-title">
|
|
649
|
+
{calendar.unscheduled} <span className="sc-column-count">{unscheduled.length}</span>
|
|
650
|
+
</h2>
|
|
651
|
+
<TaskList
|
|
652
|
+
tasks={unscheduled}
|
|
653
|
+
taskHref={href}
|
|
654
|
+
today={now}
|
|
655
|
+
showState
|
|
656
|
+
empty={<p className="sc-muted">{calendar.allScheduled}</p>}
|
|
657
|
+
/>
|
|
658
|
+
</section>
|
|
659
|
+
</div>
|
|
660
|
+
{open.length > 0 && (
|
|
661
|
+
<section className="sc-task-section" aria-label={calendar.workload.title}>
|
|
662
|
+
<h2 className="sc-section-title">{calendar.workload.title}</h2>
|
|
663
|
+
<p className="sc-lead">{calendar.workload.lead}</p>
|
|
664
|
+
<Workload
|
|
665
|
+
tasks={shown}
|
|
666
|
+
today={now}
|
|
667
|
+
personHref={(person) =>
|
|
668
|
+
boardHref(key, person === undefined ? {} : { person: person.id })
|
|
669
|
+
}
|
|
670
|
+
/>
|
|
671
|
+
</section>
|
|
672
|
+
)}
|
|
673
|
+
</>
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (route.name === "roadmap" || route.name === "new-goal") {
|
|
677
|
+
const roadmap = words.roadmap;
|
|
678
|
+
const making = route.name === "new-goal";
|
|
679
|
+
if (making && !owner) {
|
|
680
|
+
return <EmptyState title={roadmap.onlyOwner.title} body={roadmap.onlyOwner.body} />;
|
|
681
|
+
}
|
|
682
|
+
return (
|
|
683
|
+
<>
|
|
684
|
+
<div className="sc-page-head">
|
|
685
|
+
<h1 className="sc-page-title">{roadmap.title}</h1>
|
|
686
|
+
{!making && owner && (
|
|
687
|
+
<LinkButton href={newGoalHref(key)} variant="primary">
|
|
688
|
+
{roadmap.setGoal}
|
|
689
|
+
</LinkButton>
|
|
690
|
+
)}
|
|
691
|
+
</div>
|
|
692
|
+
<p className="sc-lead">{roadmap.lead}</p>
|
|
693
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
694
|
+
{making && (
|
|
695
|
+
<section className="sc-panel sc-panel-form" aria-label={roadmap.setGoalAria}>
|
|
696
|
+
<GoalForm
|
|
697
|
+
busy={busy}
|
|
698
|
+
onSubmit={(input) =>
|
|
699
|
+
void act(async () => {
|
|
700
|
+
const made = await data.actions.createGoal(input);
|
|
701
|
+
navigation.replace(goalHref(key, made.id));
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
onCancel={() => navigation.back(projectHref(key, "roadmap"))}
|
|
705
|
+
/>
|
|
706
|
+
</section>
|
|
707
|
+
)}
|
|
708
|
+
<components.GoalList
|
|
709
|
+
goals={data.goals}
|
|
710
|
+
goalHref={(goal) => goalHref(key, goal.id)}
|
|
711
|
+
empty={
|
|
712
|
+
<EmptyState
|
|
713
|
+
title={roadmap.empty.title}
|
|
714
|
+
body={roadmap.empty.body}
|
|
715
|
+
action={
|
|
716
|
+
owner ? (
|
|
717
|
+
<LinkButton href={newGoalHref(key)}>{roadmap.setGoal}</LinkButton>
|
|
718
|
+
) : undefined
|
|
719
|
+
}
|
|
720
|
+
/>
|
|
721
|
+
}
|
|
722
|
+
/>
|
|
723
|
+
</>
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
if (route.name === "goal") {
|
|
727
|
+
const roadmap = words.roadmap;
|
|
728
|
+
const goal = data.goals.find((candidate) => candidate.id === route.id);
|
|
729
|
+
const here = goalHref(key, route.id);
|
|
730
|
+
if (goal === undefined) {
|
|
731
|
+
return (
|
|
732
|
+
<EmptyState
|
|
733
|
+
title={roadmap.noSuch.title}
|
|
734
|
+
body={roadmap.noSuch.body}
|
|
735
|
+
action={<LinkButton href={projectHref(key, "roadmap")}>{roadmap.all}</LinkButton>}
|
|
736
|
+
/>
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
if (route.form === "edit") {
|
|
740
|
+
if (!owner) {
|
|
741
|
+
return <EmptyState title={roadmap.onlyOwner.title} body={roadmap.onlyOwner.body} />;
|
|
742
|
+
}
|
|
743
|
+
return (
|
|
744
|
+
<>
|
|
745
|
+
<div className="sc-page-head">
|
|
746
|
+
<h1 className="sc-page-title">{goal.title}</h1>
|
|
747
|
+
</div>
|
|
748
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
749
|
+
<section className="sc-panel sc-panel-form" aria-label={roadmap.editAria}>
|
|
750
|
+
<GoalForm
|
|
751
|
+
goal={goal}
|
|
752
|
+
busy={busy}
|
|
753
|
+
onSubmit={(input) =>
|
|
754
|
+
void act(async () => {
|
|
755
|
+
await data.actions.updateGoal(goal.id, input);
|
|
756
|
+
navigation.replace(here);
|
|
757
|
+
})
|
|
758
|
+
}
|
|
759
|
+
onCancel={() => navigation.back(here)}
|
|
760
|
+
/>
|
|
761
|
+
</section>
|
|
762
|
+
</>
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
return (
|
|
766
|
+
<>
|
|
767
|
+
<div className="sc-page-head">
|
|
768
|
+
<h1 className="sc-page-title">{roadmap.one}</h1>
|
|
769
|
+
<LinkButton href={projectHref(key, "roadmap")}>{roadmap.all}</LinkButton>
|
|
770
|
+
</div>
|
|
771
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
772
|
+
<GoalView
|
|
773
|
+
goal={goal}
|
|
774
|
+
tasks={data.tasks}
|
|
775
|
+
taskHref={href}
|
|
776
|
+
docHref={toDoc}
|
|
777
|
+
editHref={owner ? goalHref(key, goal.id, "edit") : undefined}
|
|
778
|
+
onMove={
|
|
779
|
+
owner
|
|
780
|
+
? (target, state) => void act(() => data.actions.updateGoal(target.id, { state }))
|
|
781
|
+
: undefined
|
|
782
|
+
}
|
|
783
|
+
busy={busy}
|
|
784
|
+
/>
|
|
785
|
+
</>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
552
788
|
if (route.name === "docs") {
|
|
553
789
|
return (
|
|
554
790
|
<DocsPage
|
|
@@ -650,6 +886,9 @@ function ProjectPage(props: {
|
|
|
650
886
|
if (event.decisionId !== null) {
|
|
651
887
|
return decisionHref(key, event.decisionId);
|
|
652
888
|
}
|
|
889
|
+
if (event.goalId !== null) {
|
|
890
|
+
return goalHref(key, event.goalId);
|
|
891
|
+
}
|
|
653
892
|
return event.data.path === undefined ? undefined : toDoc(event.data.path);
|
|
654
893
|
}}
|
|
655
894
|
empty={<EmptyState title={words.feed.empty} />}
|
|
@@ -730,8 +969,15 @@ function ProjectPage(props: {
|
|
|
730
969
|
<ProjectForm
|
|
731
970
|
project={project}
|
|
732
971
|
busy={busy}
|
|
733
|
-
onSubmit={(input) =>
|
|
734
|
-
void act(() =>
|
|
972
|
+
onSubmit={(input, icon) =>
|
|
973
|
+
void act(async () => {
|
|
974
|
+
await data.actions.updateProject(input as RestProjectPatch);
|
|
975
|
+
if (icon === null) {
|
|
976
|
+
await data.actions.removeProjectIcon();
|
|
977
|
+
} else if (icon !== undefined) {
|
|
978
|
+
await data.actions.setProjectIcon(uploadOf(icon));
|
|
979
|
+
}
|
|
980
|
+
})
|
|
735
981
|
}
|
|
736
982
|
/>
|
|
737
983
|
</section>
|
|
@@ -741,6 +987,13 @@ function ProjectPage(props: {
|
|
|
741
987
|
return <NothingHere />;
|
|
742
988
|
}
|
|
743
989
|
|
|
990
|
+
/** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
|
|
991
|
+
const uploadOf = (file: File): FileUpload => ({
|
|
992
|
+
name: file.name,
|
|
993
|
+
bytes: file,
|
|
994
|
+
...(file.type === "" ? {} : { contentType: file.type }),
|
|
995
|
+
});
|
|
996
|
+
|
|
744
997
|
function NothingHere() {
|
|
745
998
|
const words = useWords();
|
|
746
999
|
return (
|
|
@@ -822,6 +1075,55 @@ function PersonAgentsPage(props: {
|
|
|
822
1075
|
);
|
|
823
1076
|
}
|
|
824
1077
|
|
|
1078
|
+
/** What is on the person's plate, across their projects: read when the page is opened. */
|
|
1079
|
+
function WorkPage(props: { readonly data: ConsoleData; readonly components: ConsoleComponents }) {
|
|
1080
|
+
const words = useWords();
|
|
1081
|
+
const { data, components } = props;
|
|
1082
|
+
const [work, setWork] = useState<RestWork | undefined>(undefined);
|
|
1083
|
+
const [problem, setProblem] = useState<string | undefined>(undefined);
|
|
1084
|
+
const { work: load } = data.actions;
|
|
1085
|
+
const couldNotLoad = words.work.couldNotLoad;
|
|
1086
|
+
useEffect(() => {
|
|
1087
|
+
const controller = new AbortController();
|
|
1088
|
+
load(controller.signal)
|
|
1089
|
+
.then((answer) => {
|
|
1090
|
+
if (!controller.signal.aborted) {
|
|
1091
|
+
setWork(answer);
|
|
1092
|
+
}
|
|
1093
|
+
})
|
|
1094
|
+
.catch((failure: unknown) => {
|
|
1095
|
+
if (!controller.signal.aborted) {
|
|
1096
|
+
setProblem(errorWords(failure, couldNotLoad));
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
return () => controller.abort();
|
|
1100
|
+
}, [load, couldNotLoad]);
|
|
1101
|
+
return (
|
|
1102
|
+
<>
|
|
1103
|
+
<div className="sc-page-head">
|
|
1104
|
+
<h1 className="sc-page-title">{words.work.title}</h1>
|
|
1105
|
+
</div>
|
|
1106
|
+
<p className="sc-lead">{words.work.lead}</p>
|
|
1107
|
+
{problem !== undefined && <Callout tone="danger">{problem}</Callout>}
|
|
1108
|
+
{work === undefined ? (
|
|
1109
|
+
problem === undefined && (
|
|
1110
|
+
<div className="sc-loading">
|
|
1111
|
+
<Spinner label={words.work.loading} />
|
|
1112
|
+
</div>
|
|
1113
|
+
)
|
|
1114
|
+
) : work.tasks.length === 0 && work.decisions.length === 0 ? (
|
|
1115
|
+
<EmptyState title={words.work.empty.title} body={words.work.empty.body} />
|
|
1116
|
+
) : (
|
|
1117
|
+
<components.WorkList
|
|
1118
|
+
work={work}
|
|
1119
|
+
taskHref={(projectKey, task) => taskHref(projectKey, task.number)}
|
|
1120
|
+
decisionHref={(projectKey, decisionId) => decisionHref(projectKey, decisionId)}
|
|
1121
|
+
/>
|
|
1122
|
+
)}
|
|
1123
|
+
</>
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
825
1127
|
function WorkspacePage(props: {
|
|
826
1128
|
readonly route: Route;
|
|
827
1129
|
readonly data: ConsoleData;
|
|
@@ -833,6 +1135,48 @@ function WorkspacePage(props: {
|
|
|
833
1135
|
const [fresh, setFresh] = useState<RestTokenCreated | undefined>(undefined);
|
|
834
1136
|
const { busy, error, act } = useAction();
|
|
835
1137
|
|
|
1138
|
+
if (route.name === "work") {
|
|
1139
|
+
return <WorkPage data={data} components={components} />;
|
|
1140
|
+
}
|
|
1141
|
+
if (route.name === "categories") {
|
|
1142
|
+
const me = data.me?.person ?? undefined;
|
|
1143
|
+
const administrator = me?.role === "admin" && !holdsToken(data);
|
|
1144
|
+
const categories = words.categories;
|
|
1145
|
+
return (
|
|
1146
|
+
<>
|
|
1147
|
+
<div className="sc-page-head">
|
|
1148
|
+
<h1 className="sc-page-title">{categories.title}</h1>
|
|
1149
|
+
</div>
|
|
1150
|
+
<p className="sc-lead">
|
|
1151
|
+
{categories.lead} {categories.onlyAdmin}
|
|
1152
|
+
</p>
|
|
1153
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1154
|
+
<components.CategoryList
|
|
1155
|
+
categories={data.categories}
|
|
1156
|
+
busy={busy}
|
|
1157
|
+
onAdd={
|
|
1158
|
+
administrator
|
|
1159
|
+
? (input) => void act(() => data.actions.createCategory(input))
|
|
1160
|
+
: undefined
|
|
1161
|
+
}
|
|
1162
|
+
onChange={
|
|
1163
|
+
administrator
|
|
1164
|
+
? (category, patch) =>
|
|
1165
|
+
void act(() => data.actions.updateCategory(category.key, patch))
|
|
1166
|
+
: undefined
|
|
1167
|
+
}
|
|
1168
|
+
onRemove={
|
|
1169
|
+
administrator
|
|
1170
|
+
? (category) => void act(() => data.actions.removeCategory(category.key))
|
|
1171
|
+
: undefined
|
|
1172
|
+
}
|
|
1173
|
+
empty={<EmptyState title={categories.empty.title} body={categories.empty.body} />}
|
|
1174
|
+
/>
|
|
1175
|
+
{administrator && <p className="sc-muted">{categories.inUse}</p>}
|
|
1176
|
+
</>
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
836
1180
|
if (route.name === "people") {
|
|
837
1181
|
const me = data.me?.person ?? undefined;
|
|
838
1182
|
const administrator = me?.role === "admin" && !holdsToken(data);
|
|
@@ -998,6 +1342,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
998
1342
|
label: words.nav.projects,
|
|
999
1343
|
current: route.name === "projects" || route.name === "new-project",
|
|
1000
1344
|
},
|
|
1345
|
+
{ href: PATHS.work, label: words.nav.work, current: route.name === "work" },
|
|
1001
1346
|
{
|
|
1002
1347
|
href: PATHS.people,
|
|
1003
1348
|
label: words.nav.people,
|
|
@@ -1012,6 +1357,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1012
1357
|
current: route.name === "agents" || route.name === "new-token",
|
|
1013
1358
|
},
|
|
1014
1359
|
]),
|
|
1360
|
+
// The categories are read by everyone, for what the colours mean, and kept by an administrator.
|
|
1361
|
+
{
|
|
1362
|
+
href: PATHS.categories,
|
|
1363
|
+
label: words.nav.categories,
|
|
1364
|
+
current: route.name === "categories",
|
|
1365
|
+
},
|
|
1015
1366
|
];
|
|
1016
1367
|
const nav: NavItem[] =
|
|
1017
1368
|
projectKey === undefined
|
|
@@ -1023,6 +1374,17 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1023
1374
|
current:
|
|
1024
1375
|
route.name === "board" || route.name === "task" || route.name === "new-task",
|
|
1025
1376
|
},
|
|
1377
|
+
{
|
|
1378
|
+
href: projectHref(projectKey, "roadmap"),
|
|
1379
|
+
label: words.nav.roadmap,
|
|
1380
|
+
current:
|
|
1381
|
+
route.name === "roadmap" || route.name === "goal" || route.name === "new-goal",
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
href: projectHref(projectKey, "calendar"),
|
|
1385
|
+
label: words.nav.calendar,
|
|
1386
|
+
current: route.name === "calendar",
|
|
1387
|
+
},
|
|
1026
1388
|
{
|
|
1027
1389
|
href: projectHref(projectKey, "docs"),
|
|
1028
1390
|
label: words.nav.docs,
|
|
@@ -1121,7 +1483,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1121
1483
|
project={
|
|
1122
1484
|
projectKey === undefined
|
|
1123
1485
|
? undefined
|
|
1124
|
-
: {
|
|
1486
|
+
: {
|
|
1487
|
+
name: project?.name ?? projectKey,
|
|
1488
|
+
href: projectHref(projectKey),
|
|
1489
|
+
mark:
|
|
1490
|
+
project === undefined ? undefined : <ProjectMark project={project} size="sm" />,
|
|
1491
|
+
}
|
|
1125
1492
|
}
|
|
1126
1493
|
nav={nav}
|
|
1127
1494
|
menu={workspaceNav}
|
|
@@ -1140,7 +1507,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1140
1507
|
}}
|
|
1141
1508
|
person={data.me.person}
|
|
1142
1509
|
live={projectKey === undefined ? undefined : data.live}
|
|
1143
|
-
wide={route.name === "board"}
|
|
1510
|
+
wide={route.name === "board" || route.name === "calendar"}
|
|
1144
1511
|
onNavigate={(href) => navigation.go(href)}
|
|
1145
1512
|
onSignOut={viaToken ? undefined : () => void data.actions.signOut()}
|
|
1146
1513
|
>
|