@skillcdn/console 0.1.13 → 0.1.15
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 -4
- package/SKILLCDN.md +2 -0
- package/dist/agent-name.d.ts +3 -0
- package/dist/agent-name.d.ts.map +1 -0
- package/dist/agent-name.js +9 -0
- package/dist/agent-name.js.map +1 -0
- package/dist/api.d.ts +6 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +215 -22
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +14 -4
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +59 -10
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +27 -11
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +5 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +20 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/client.js.map +1 -1
- package/dist/components/calendar.d.ts +15 -0
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +78 -41
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/connect.d.ts +4 -1
- package/dist/components/connect.d.ts.map +1 -1
- package/dist/components/connect.js +7 -3
- package/dist/components/connect.js.map +1 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +3 -3
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/documents.d.ts +5 -0
- package/dist/components/documents.d.ts.map +1 -1
- package/dist/components/documents.js +3 -2
- package/dist/components/documents.js.map +1 -1
- package/dist/components/event-feed.d.ts +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +10 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/people.d.ts +24 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +35 -3
- package/dist/components/people.js.map +1 -1
- package/dist/components/projects.d.ts +6 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -3
- package/dist/components/projects.js.map +1 -1
- package/dist/components/rules.d.ts +40 -0
- package/dist/components/rules.d.ts.map +1 -0
- package/dist/components/rules.js +46 -0
- package/dist/components/rules.js.map +1 -0
- package/dist/components/runs.d.ts +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +2 -1
- package/dist/components/runs.js.map +1 -1
- package/dist/components/skills.d.ts.map +1 -1
- package/dist/components/skills.js +5 -1
- package/dist/components/skills.js.map +1 -1
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +2 -9
- package/dist/components/tokens.js.map +1 -1
- package/dist/components/ui.d.ts +7 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +13 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +9 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +8 -3
- package/dist/components/work.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +1 -1
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +253 -13
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +203 -49
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +19 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -1
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +4 -0
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +26 -0
- package/dist/dates.js.map +1 -1
- package/dist/i18n/en.d.ts +90 -28
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +89 -31
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +89 -31
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +4 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +4 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +34 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +55 -7
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +14 -4
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +17 -2
- package/dist/routes.js.map +1 -1
- package/dist/rules.d.ts +28 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +102 -0
- package/dist/rules.js.map +1 -0
- package/dist/schemas.d.ts +652 -4
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +96 -6
- 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 +21 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +13 -9
- package/docs/specs/rest.md +32 -8
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +1 -1
- package/skills/deploying-a-console/SKILL.md +5 -5
- package/skills/deploying-a-console/assets/secrets.example.json +2 -1
- package/skills/deploying-a-console/references/access.md +10 -6
- package/skills/deploying-a-console/references/resources.md +10 -7
- package/skills/working-the-board/SKILL.md +10 -9
- package/skills/working-the-board/references/project-rules.md +36 -0
- package/src/agent-name.ts +9 -0
- package/src/api.ts +37 -0
- package/src/cli/cli.ts +253 -24
- package/src/cli/format.ts +74 -10
- package/src/cli/help.ts +30 -10
- package/src/cli/main.ts +5 -0
- package/src/client.ts +47 -1
- package/src/components/calendar.tsx +242 -101
- package/src/components/connect.tsx +12 -6
- package/src/components/decision-list.tsx +2 -2
- package/src/components/documents.tsx +7 -3
- package/src/components/event-feed.tsx +16 -3
- package/src/components/people.tsx +244 -35
- package/src/components/projects.tsx +31 -8
- package/src/components/rules.tsx +167 -0
- package/src/components/runs.tsx +4 -2
- package/src/components/skills.tsx +22 -11
- package/src/components/tokens.tsx +2 -14
- package/src/components/ui.tsx +25 -2
- package/src/components/work.tsx +69 -43
- package/src/connect-page.tsx +0 -1
- package/src/console.tsx +531 -67
- package/src/data.ts +52 -2
- package/src/dates.ts +28 -0
- package/src/i18n/en.ts +99 -38
- package/src/i18n/ko.ts +95 -38
- package/src/index.ts +21 -1
- package/src/limits.ts +4 -0
- package/src/router.ts +77 -12
- package/src/routes.ts +23 -3
- package/src/rules.ts +109 -0
- package/src/schemas.ts +121 -4
- package/src/styles/console.css +253 -13
- package/src/vocabulary.ts +30 -0
package/src/console.tsx
CHANGED
|
@@ -9,10 +9,13 @@ import {
|
|
|
9
9
|
} from "react";
|
|
10
10
|
import { createRoot, type Root } from "react-dom/client";
|
|
11
11
|
import {
|
|
12
|
+
ApiError,
|
|
12
13
|
type ConsoleClient,
|
|
13
14
|
createClient,
|
|
14
15
|
type FileUpload,
|
|
15
16
|
OPEN_TASK_STATES,
|
|
17
|
+
PROJECT_RULES_TEMPLATE,
|
|
18
|
+
REFERENCE_RULES,
|
|
16
19
|
type RestAnswerInput,
|
|
17
20
|
type RestDecision,
|
|
18
21
|
type RestDecisionInput,
|
|
@@ -21,11 +24,15 @@ import {
|
|
|
21
24
|
type RestProject,
|
|
22
25
|
type RestProjectInput,
|
|
23
26
|
type RestProjectPatch,
|
|
27
|
+
type RestProjectRules,
|
|
28
|
+
type RestRules,
|
|
24
29
|
type RestTask,
|
|
25
30
|
type RestTaskInput,
|
|
26
31
|
type RestToken,
|
|
27
32
|
type RestTokenCreated,
|
|
28
33
|
type RestWork,
|
|
34
|
+
RULES_PATH,
|
|
35
|
+
RULES_TITLE,
|
|
29
36
|
type TaskState,
|
|
30
37
|
} from "./api.js";
|
|
31
38
|
import { Board, BoardFilters, type BoardProps, byDue, filterTasks } from "./components/board.js";
|
|
@@ -36,7 +43,7 @@ import { DecisionForm } from "./components/decision-form.js";
|
|
|
36
43
|
import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
|
|
37
44
|
import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
|
|
38
45
|
import { GoalForm, GoalList, type GoalListProps, GoalView } from "./components/goals.js";
|
|
39
|
-
import { PeopleList, type PeopleListProps } from "./components/people.js";
|
|
46
|
+
import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
|
|
40
47
|
import {
|
|
41
48
|
MemberList,
|
|
42
49
|
type MemberListProps,
|
|
@@ -45,6 +52,7 @@ import {
|
|
|
45
52
|
type ProjectListProps,
|
|
46
53
|
ProjectMark,
|
|
47
54
|
} from "./components/projects.js";
|
|
55
|
+
import { RulesForm, RulesView } from "./components/rules.js";
|
|
48
56
|
import {
|
|
49
57
|
type Brand,
|
|
50
58
|
isPlainClick,
|
|
@@ -58,7 +66,14 @@ import { TaskForm } from "./components/task-form.js";
|
|
|
58
66
|
import { TaskList } from "./components/task-list.js";
|
|
59
67
|
import { TaskView, type TaskViewProps } from "./components/task-view.js";
|
|
60
68
|
import { NewToken, TokenForm, TokenList, type TokenListProps } from "./components/tokens.js";
|
|
61
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
Button,
|
|
71
|
+
Callout,
|
|
72
|
+
CategoryBadge,
|
|
73
|
+
EmptyState,
|
|
74
|
+
personName,
|
|
75
|
+
Spinner,
|
|
76
|
+
} from "./components/ui.js";
|
|
62
77
|
import { WorkList, type WorkListProps } from "./components/work.js";
|
|
63
78
|
import { ConnectPage } from "./connect-page.js";
|
|
64
79
|
import { type ConsoleData, useConsoleData } from "./data.js";
|
|
@@ -86,11 +101,15 @@ import {
|
|
|
86
101
|
newTaskHref,
|
|
87
102
|
PATHS,
|
|
88
103
|
personAgentsHref,
|
|
104
|
+
personWorkHref,
|
|
89
105
|
projectHref,
|
|
106
|
+
projectSettingsHref,
|
|
90
107
|
type Route,
|
|
108
|
+
settingsHref,
|
|
91
109
|
signInFailureOf,
|
|
92
110
|
taskHref,
|
|
93
111
|
withoutSignInParam,
|
|
112
|
+
workHref,
|
|
94
113
|
} from "./router.js";
|
|
95
114
|
import { projectPath } from "./routes.js";
|
|
96
115
|
import { errorWords, useAction } from "./use-action.js";
|
|
@@ -384,12 +403,13 @@ function ProjectsPage(props: {
|
|
|
384
403
|
</LinkButton>
|
|
385
404
|
)}
|
|
386
405
|
</div>
|
|
387
|
-
<p className="sc-lead">{words.lead}</p>
|
|
388
406
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
389
407
|
{making && (
|
|
390
408
|
<section className="sc-panel sc-panel-form" aria-label={words.form.aria}>
|
|
391
409
|
<ProjectForm
|
|
392
410
|
busy={busy}
|
|
411
|
+
skillsSource={data.me?.workspace.skills.source}
|
|
412
|
+
organizationAddress={data.me?.workspace.skills.address ?? null}
|
|
393
413
|
onSubmit={(input, icon) =>
|
|
394
414
|
void act(async () => {
|
|
395
415
|
const made = await data.actions.createProject(
|
|
@@ -476,7 +496,7 @@ function TaskPage(props: {
|
|
|
476
496
|
return (
|
|
477
497
|
<components.TaskView
|
|
478
498
|
task={task}
|
|
479
|
-
people={data.people}
|
|
499
|
+
people={data.people.filter(inWorkspace)}
|
|
480
500
|
tasks={data.tasks}
|
|
481
501
|
decisions={data.decisions}
|
|
482
502
|
runs={data.runs}
|
|
@@ -558,7 +578,7 @@ function ProjectPage(props: {
|
|
|
558
578
|
filter={route.filter}
|
|
559
579
|
categories={data.categories}
|
|
560
580
|
goals={data.goals}
|
|
561
|
-
people={data.people}
|
|
581
|
+
people={data.people.filter(inWorkspace)}
|
|
562
582
|
me={me}
|
|
563
583
|
shown={shown.length}
|
|
564
584
|
total={data.tasks.length}
|
|
@@ -592,7 +612,7 @@ function ProjectPage(props: {
|
|
|
592
612
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
593
613
|
<section className="sc-panel sc-panel-form" aria-label={words.task.newAria}>
|
|
594
614
|
<TaskForm
|
|
595
|
-
people={data.people}
|
|
615
|
+
people={data.people.filter(inWorkspace)}
|
|
596
616
|
parents={data.tasks}
|
|
597
617
|
categories={data.categories}
|
|
598
618
|
goals={data.goals}
|
|
@@ -640,13 +660,12 @@ function ProjectPage(props: {
|
|
|
640
660
|
{words.board.writeTask}
|
|
641
661
|
</LinkButton>
|
|
642
662
|
</div>
|
|
643
|
-
<p className="sc-lead">{calendar.lead}</p>
|
|
644
663
|
{data.tasks.length > 0 && (
|
|
645
664
|
<BoardFilters
|
|
646
665
|
filter={route.filter}
|
|
647
666
|
categories={data.categories}
|
|
648
667
|
goals={data.goals}
|
|
649
|
-
people={data.people}
|
|
668
|
+
people={data.people.filter(inWorkspace)}
|
|
650
669
|
me={me}
|
|
651
670
|
shown={shown.length}
|
|
652
671
|
total={data.tasks.length}
|
|
@@ -690,7 +709,6 @@ function ProjectPage(props: {
|
|
|
690
709
|
{open.length > 0 && (
|
|
691
710
|
<section className="sc-task-section" aria-label={calendar.workload.title}>
|
|
692
711
|
<h2 className="sc-section-title">{calendar.workload.title}</h2>
|
|
693
|
-
<p className="sc-lead">{calendar.workload.lead}</p>
|
|
694
712
|
<Workload
|
|
695
713
|
tasks={shown}
|
|
696
714
|
today={now}
|
|
@@ -719,7 +737,6 @@ function ProjectPage(props: {
|
|
|
719
737
|
</LinkButton>
|
|
720
738
|
)}
|
|
721
739
|
</div>
|
|
722
|
-
<p className="sc-lead">{roadmap.lead}</p>
|
|
723
740
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
724
741
|
{making && (
|
|
725
742
|
<section className="sc-panel sc-panel-form" aria-label={roadmap.setGoalAria}>
|
|
@@ -919,6 +936,10 @@ function ProjectPage(props: {
|
|
|
919
936
|
if (event.goalId !== null) {
|
|
920
937
|
return goalHref(key, event.goalId);
|
|
921
938
|
}
|
|
939
|
+
// The rules are a page shown on the settings, not among the documents (ADR-0023).
|
|
940
|
+
if (event.data.path === RULES_PATH) {
|
|
941
|
+
return projectSettingsHref(key);
|
|
942
|
+
}
|
|
922
943
|
return event.data.path === undefined ? undefined : toDoc(event.data.path);
|
|
923
944
|
}}
|
|
924
945
|
empty={<EmptyState title={words.feed.empty} />}
|
|
@@ -945,14 +966,11 @@ function ProjectPage(props: {
|
|
|
945
966
|
<div className="sc-page-head">
|
|
946
967
|
<h1 className="sc-page-title">{members.title}</h1>
|
|
947
968
|
</div>
|
|
948
|
-
<p className="sc-lead">
|
|
949
|
-
{project.visibility === "workspace" ? members.leadWorkspace : members.leadPrivate}{" "}
|
|
950
|
-
{members.leadRoles}
|
|
951
|
-
</p>
|
|
969
|
+
<p className="sc-lead">{words.vocabulary.visibility[project.visibility]}</p>
|
|
952
970
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
953
971
|
<components.MemberList
|
|
954
972
|
members={data.members}
|
|
955
|
-
people={data.people}
|
|
973
|
+
people={data.people.filter(inWorkspace)}
|
|
956
974
|
me={me}
|
|
957
975
|
busy={busy}
|
|
958
976
|
onAdd={configures ? (input) => void act(() => data.actions.addMember(input)) : undefined}
|
|
@@ -967,6 +985,7 @@ function ProjectPage(props: {
|
|
|
967
985
|
? (member) => void act(() => data.actions.removeMember(member.person.id))
|
|
968
986
|
: undefined
|
|
969
987
|
}
|
|
988
|
+
workHref={owner ? (member) => personWorkHref(member.person.id) : undefined}
|
|
970
989
|
empty={
|
|
971
990
|
<EmptyState
|
|
972
991
|
title={members.empty.title}
|
|
@@ -980,25 +999,71 @@ function ProjectPage(props: {
|
|
|
980
999
|
);
|
|
981
1000
|
}
|
|
982
1001
|
if (route.name === "settings") {
|
|
983
|
-
|
|
1002
|
+
return (
|
|
1003
|
+
<ProjectSettingsPage route={route} project={project} data={data} navigation={navigation} />
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
return <NothingHere />;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* A project's settings (ADR-0023): read by every member, so that everyone sees what the project
|
|
1011
|
+
* is and what its agents are asked; changed by an owner, on the forms at their own addresses.
|
|
1012
|
+
*/
|
|
1013
|
+
function ProjectSettingsPage(props: {
|
|
1014
|
+
readonly route: Extract<Route, { name: "settings" }>;
|
|
1015
|
+
readonly project: RestProject;
|
|
1016
|
+
readonly data: ConsoleData;
|
|
1017
|
+
readonly navigation: Navigation;
|
|
1018
|
+
}) {
|
|
1019
|
+
const words = useWords();
|
|
1020
|
+
const { route, project, data, navigation } = props;
|
|
1021
|
+
const { busy, error, act } = useAction();
|
|
1022
|
+
const settings = words.projects.settings;
|
|
1023
|
+
const form = words.projects.form;
|
|
1024
|
+
const owner = project.role === "owner";
|
|
1025
|
+
const key = project.key;
|
|
1026
|
+
const here = projectSettingsHref(key);
|
|
1027
|
+
const skills = data.me?.workspace.skills;
|
|
1028
|
+
const [rules, setRules] = useState<RestProjectRules | undefined>(undefined);
|
|
1029
|
+
const feedLength = data.events.length;
|
|
1030
|
+
// The rules, asked for again whenever the project's feed grows.
|
|
1031
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: the feed's length is the trigger
|
|
1032
|
+
useEffect(() => {
|
|
1033
|
+
const controller = new AbortController();
|
|
1034
|
+
data.actions
|
|
1035
|
+
.projectRules(controller.signal)
|
|
1036
|
+
.then((found) => {
|
|
1037
|
+
if (!controller.signal.aborted) {
|
|
1038
|
+
setRules(found);
|
|
1039
|
+
}
|
|
1040
|
+
})
|
|
1041
|
+
.catch(() => undefined);
|
|
1042
|
+
return () => controller.abort();
|
|
1043
|
+
}, [data.actions, feedLength]);
|
|
1044
|
+
|
|
1045
|
+
const head = (
|
|
1046
|
+
<div className="sc-page-head">
|
|
1047
|
+
<h1 className="sc-page-title">{settings.title}</h1>
|
|
1048
|
+
{owner && route.form === undefined && (
|
|
1049
|
+
<LinkButton href={projectSettingsHref(key, "edit")}>{words.common.edit}</LinkButton>
|
|
1050
|
+
)}
|
|
1051
|
+
</div>
|
|
1052
|
+
);
|
|
1053
|
+
if (route.form === "edit") {
|
|
984
1054
|
if (!owner) {
|
|
985
|
-
return <
|
|
1055
|
+
return <NothingHere />;
|
|
986
1056
|
}
|
|
987
1057
|
return (
|
|
988
1058
|
<>
|
|
989
|
-
|
|
990
|
-
<h1 className="sc-page-title">{settings.title}</h1>
|
|
991
|
-
</div>
|
|
992
|
-
<p className="sc-lead">
|
|
993
|
-
{settings.leadBefore}
|
|
994
|
-
<code>{project.key}</code>
|
|
995
|
-
{settings.leadAfter}
|
|
996
|
-
</p>
|
|
1059
|
+
{head}
|
|
997
1060
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
998
1061
|
<section className="sc-panel sc-panel-form" aria-label={settings.aria}>
|
|
999
1062
|
<ProjectForm
|
|
1000
1063
|
project={project}
|
|
1001
1064
|
busy={busy}
|
|
1065
|
+
skillsSource={skills?.source}
|
|
1066
|
+
organizationAddress={skills?.address ?? null}
|
|
1002
1067
|
onSubmit={(input, icon) =>
|
|
1003
1068
|
void act(async () => {
|
|
1004
1069
|
await data.actions.updateProject(input as RestProjectPatch);
|
|
@@ -1007,17 +1072,288 @@ function ProjectPage(props: {
|
|
|
1007
1072
|
} else if (icon !== undefined) {
|
|
1008
1073
|
await data.actions.setProjectIcon(uploadOf(icon));
|
|
1009
1074
|
}
|
|
1075
|
+
navigation.replace(here);
|
|
1010
1076
|
})
|
|
1011
1077
|
}
|
|
1078
|
+
onCancel={() => navigation.back(here)}
|
|
1012
1079
|
/>
|
|
1013
1080
|
</section>
|
|
1014
1081
|
</>
|
|
1015
1082
|
);
|
|
1016
1083
|
}
|
|
1017
|
-
|
|
1084
|
+
if (route.form === "rules") {
|
|
1085
|
+
if (!owner) {
|
|
1086
|
+
return <NothingHere />;
|
|
1087
|
+
}
|
|
1088
|
+
return (
|
|
1089
|
+
<>
|
|
1090
|
+
{head}
|
|
1091
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1092
|
+
<section className="sc-panel sc-panel-form" aria-label={words.rules.projectRules}>
|
|
1093
|
+
{rules === undefined ? (
|
|
1094
|
+
<Spinner label={words.rules.loading} />
|
|
1095
|
+
) : (
|
|
1096
|
+
<RulesForm
|
|
1097
|
+
rules={rules.project}
|
|
1098
|
+
scope="project"
|
|
1099
|
+
template={PROJECT_RULES_TEMPLATE}
|
|
1100
|
+
busy={busy}
|
|
1101
|
+
onSubmit={(input) =>
|
|
1102
|
+
void act(async () => {
|
|
1103
|
+
await data.actions.writeDocument(RULES_PATH, {
|
|
1104
|
+
title: RULES_TITLE,
|
|
1105
|
+
body: input.body,
|
|
1106
|
+
...(input.baseVersion === undefined ? {} : { baseVersion: input.baseVersion }),
|
|
1107
|
+
});
|
|
1108
|
+
navigation.replace(here);
|
|
1109
|
+
})
|
|
1110
|
+
}
|
|
1111
|
+
onCancel={() => navigation.back(here)}
|
|
1112
|
+
/>
|
|
1113
|
+
)}
|
|
1114
|
+
</section>
|
|
1115
|
+
</>
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
return (
|
|
1119
|
+
<>
|
|
1120
|
+
{head}
|
|
1121
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1122
|
+
<section className="sc-panel" aria-label={settings.aria}>
|
|
1123
|
+
<dl className="sc-facts sc-facts-plain">
|
|
1124
|
+
<div>
|
|
1125
|
+
<dt>{form.name}</dt>
|
|
1126
|
+
<dd>{project.name}</dd>
|
|
1127
|
+
</div>
|
|
1128
|
+
<div>
|
|
1129
|
+
<dt>{form.key}</dt>
|
|
1130
|
+
<dd>
|
|
1131
|
+
<code className="sc-project-key">{project.key}</code>
|
|
1132
|
+
</dd>
|
|
1133
|
+
</div>
|
|
1134
|
+
<div>
|
|
1135
|
+
<dt>{form.whoIsMember}</dt>
|
|
1136
|
+
<dd>{words.vocabulary.visibility[project.visibility]}</dd>
|
|
1137
|
+
</div>
|
|
1138
|
+
<div>
|
|
1139
|
+
<dt>{form.skillsAddress}</dt>
|
|
1140
|
+
<dd>
|
|
1141
|
+
{project.skillsAddress !== null ? (
|
|
1142
|
+
<code>
|
|
1143
|
+
{skills?.source ?? ""}
|
|
1144
|
+
{project.skillsAddress}
|
|
1145
|
+
</code>
|
|
1146
|
+
) : skills?.address != null ? (
|
|
1147
|
+
settings.organizationsSkills(skills.address)
|
|
1148
|
+
) : (
|
|
1149
|
+
settings.noSkills
|
|
1150
|
+
)}
|
|
1151
|
+
</dd>
|
|
1152
|
+
</div>
|
|
1153
|
+
<div>
|
|
1154
|
+
<dt>{form.symbol}</dt>
|
|
1155
|
+
<dd>
|
|
1156
|
+
<ProjectMark project={project} size="sm" />
|
|
1157
|
+
</dd>
|
|
1158
|
+
</div>
|
|
1159
|
+
{project.description.length > 0 && (
|
|
1160
|
+
<div className="sc-facts-wide">
|
|
1161
|
+
<dt>{form.description}</dt>
|
|
1162
|
+
<dd>{project.description}</dd>
|
|
1163
|
+
</div>
|
|
1164
|
+
)}
|
|
1165
|
+
</dl>
|
|
1166
|
+
</section>
|
|
1167
|
+
<section className="sc-panel" aria-label={words.rules.projectRules}>
|
|
1168
|
+
<h2 className="sc-section-title">{words.rules.projectRules}</h2>
|
|
1169
|
+
{rules === undefined ? (
|
|
1170
|
+
<Spinner label={words.rules.loading} />
|
|
1171
|
+
) : (
|
|
1172
|
+
<RulesView
|
|
1173
|
+
rules={rules.project}
|
|
1174
|
+
scope="project"
|
|
1175
|
+
docHref={(path) => docHref(key, path)}
|
|
1176
|
+
editHref={owner ? projectSettingsHref(key, "rules") : undefined}
|
|
1177
|
+
onArchive={
|
|
1178
|
+
owner
|
|
1179
|
+
? () =>
|
|
1180
|
+
void act(async () => {
|
|
1181
|
+
await data.actions.archiveDocument(RULES_PATH);
|
|
1182
|
+
setRules(await data.actions.projectRules());
|
|
1183
|
+
})
|
|
1184
|
+
: undefined
|
|
1185
|
+
}
|
|
1186
|
+
busy={busy}
|
|
1187
|
+
/>
|
|
1188
|
+
)}
|
|
1189
|
+
</section>
|
|
1190
|
+
<section className="sc-panel" aria-label={words.rules.workspaceRules}>
|
|
1191
|
+
<div className="sc-section-head">
|
|
1192
|
+
<h2 className="sc-section-title">{words.rules.workspaceRules}</h2>
|
|
1193
|
+
<a className="sc-muted" href={settingsHref()}>
|
|
1194
|
+
{words.rules.onWorkspaceSettings}
|
|
1195
|
+
</a>
|
|
1196
|
+
</div>
|
|
1197
|
+
{rules !== undefined && <RulesView rules={rules.workspace} scope="workspace" folded />}
|
|
1198
|
+
</section>
|
|
1199
|
+
</>
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* The workspace's settings (ADR-0023): its rules, the console's own until an administrator
|
|
1205
|
+
* writes the organization's; its categories, kept on their own page; and where its skills come
|
|
1206
|
+
* from, which whoever runs the console set. Read by everyone; the rules written by an administrator.
|
|
1207
|
+
*/
|
|
1208
|
+
function WorkspaceSettingsPage(props: {
|
|
1209
|
+
readonly form: "rules" | undefined;
|
|
1210
|
+
readonly data: ConsoleData;
|
|
1211
|
+
readonly navigation: Navigation;
|
|
1212
|
+
}) {
|
|
1213
|
+
const words = useWords();
|
|
1214
|
+
const { form, data, navigation } = props;
|
|
1215
|
+
const { busy, error, act } = useAction();
|
|
1216
|
+
const administrator = data.me?.person?.role === "admin";
|
|
1217
|
+
const skills = data.me?.workspace.skills;
|
|
1218
|
+
const [rules, setRules] = useState<RestRules | undefined>(undefined);
|
|
1219
|
+
useEffect(() => {
|
|
1220
|
+
const controller = new AbortController();
|
|
1221
|
+
data.actions
|
|
1222
|
+
.rules(controller.signal)
|
|
1223
|
+
.then((found) => {
|
|
1224
|
+
if (!controller.signal.aborted) {
|
|
1225
|
+
setRules(found);
|
|
1226
|
+
}
|
|
1227
|
+
})
|
|
1228
|
+
.catch(() => undefined);
|
|
1229
|
+
return () => controller.abort();
|
|
1230
|
+
}, [data.actions]);
|
|
1231
|
+
|
|
1232
|
+
const head = (
|
|
1233
|
+
<div className="sc-page-head">
|
|
1234
|
+
<h1 className="sc-page-title">{words.settings.title}</h1>
|
|
1235
|
+
</div>
|
|
1236
|
+
);
|
|
1237
|
+
if (form === "rules") {
|
|
1238
|
+
if (!administrator) {
|
|
1239
|
+
return <NothingHere />;
|
|
1240
|
+
}
|
|
1241
|
+
return (
|
|
1242
|
+
<>
|
|
1243
|
+
{head}
|
|
1244
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1245
|
+
<section className="sc-panel sc-panel-form" aria-label={words.rules.title}>
|
|
1246
|
+
{rules === undefined ? (
|
|
1247
|
+
<Spinner label={words.rules.loading} />
|
|
1248
|
+
) : (
|
|
1249
|
+
<RulesForm
|
|
1250
|
+
rules={rules}
|
|
1251
|
+
scope="workspace"
|
|
1252
|
+
template={REFERENCE_RULES}
|
|
1253
|
+
busy={busy}
|
|
1254
|
+
onSubmit={(input) =>
|
|
1255
|
+
void act(async () => {
|
|
1256
|
+
setRules(
|
|
1257
|
+
await data.actions.writeRules({
|
|
1258
|
+
body: input.body,
|
|
1259
|
+
...(input.baseVersion === undefined
|
|
1260
|
+
? {}
|
|
1261
|
+
: { baseVersion: input.baseVersion }),
|
|
1262
|
+
}),
|
|
1263
|
+
);
|
|
1264
|
+
navigation.replace(PATHS.settings);
|
|
1265
|
+
})
|
|
1266
|
+
}
|
|
1267
|
+
onCancel={() => navigation.back(PATHS.settings)}
|
|
1268
|
+
onReset={() =>
|
|
1269
|
+
void act(async () => {
|
|
1270
|
+
setRules(await data.actions.archiveRules());
|
|
1271
|
+
navigation.replace(PATHS.settings);
|
|
1272
|
+
})
|
|
1273
|
+
}
|
|
1274
|
+
/>
|
|
1275
|
+
)}
|
|
1276
|
+
</section>
|
|
1277
|
+
</>
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
return (
|
|
1281
|
+
<>
|
|
1282
|
+
{head}
|
|
1283
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1284
|
+
<section className="sc-panel" aria-label={words.rules.title}>
|
|
1285
|
+
<h2 className="sc-section-title">{words.rules.title}</h2>
|
|
1286
|
+
{rules === undefined ? (
|
|
1287
|
+
<Spinner label={words.rules.loading} />
|
|
1288
|
+
) : (
|
|
1289
|
+
<RulesView
|
|
1290
|
+
rules={rules}
|
|
1291
|
+
scope="workspace"
|
|
1292
|
+
editHref={administrator ? settingsHref("rules") : undefined}
|
|
1293
|
+
onArchive={
|
|
1294
|
+
administrator
|
|
1295
|
+
? () =>
|
|
1296
|
+
void act(async () => {
|
|
1297
|
+
setRules(await data.actions.archiveRules());
|
|
1298
|
+
})
|
|
1299
|
+
: undefined
|
|
1300
|
+
}
|
|
1301
|
+
busy={busy}
|
|
1302
|
+
/>
|
|
1303
|
+
)}
|
|
1304
|
+
</section>
|
|
1305
|
+
<section className="sc-panel" aria-label={words.settings.categories}>
|
|
1306
|
+
<h2 className="sc-section-title">{words.settings.categories}</h2>
|
|
1307
|
+
{data.categories.length > 0 && (
|
|
1308
|
+
<p className="sc-category-chips">
|
|
1309
|
+
{data.categories.map((category) => (
|
|
1310
|
+
<CategoryBadge key={category.key} category={category} />
|
|
1311
|
+
))}
|
|
1312
|
+
</p>
|
|
1313
|
+
)}
|
|
1314
|
+
<LinkButton href={PATHS.categories}>{words.settings.manageCategories}</LinkButton>
|
|
1315
|
+
</section>
|
|
1316
|
+
{skills !== undefined && (
|
|
1317
|
+
<section className="sc-panel" aria-label={words.settings.skills}>
|
|
1318
|
+
<h2 className="sc-section-title">{words.settings.skills}</h2>
|
|
1319
|
+
<dl className="sc-facts sc-facts-plain">
|
|
1320
|
+
<div>
|
|
1321
|
+
<dt>{words.settings.skillsSource}</dt>
|
|
1322
|
+
<dd>
|
|
1323
|
+
<a href={skills.source} target="_blank" rel="noopener noreferrer">
|
|
1324
|
+
{skills.source}
|
|
1325
|
+
</a>
|
|
1326
|
+
</dd>
|
|
1327
|
+
</div>
|
|
1328
|
+
<div>
|
|
1329
|
+
<dt>{words.settings.skillsAddress}</dt>
|
|
1330
|
+
<dd>
|
|
1331
|
+
{skills.address === null ? (
|
|
1332
|
+
words.settings.noSkillsAddress
|
|
1333
|
+
) : (
|
|
1334
|
+
<a
|
|
1335
|
+
href={`${skills.source}${skills.address}`}
|
|
1336
|
+
target="_blank"
|
|
1337
|
+
rel="noopener noreferrer"
|
|
1338
|
+
>
|
|
1339
|
+
<code>{skills.address}</code>
|
|
1340
|
+
</a>
|
|
1341
|
+
)}
|
|
1342
|
+
</dd>
|
|
1343
|
+
</div>
|
|
1344
|
+
</dl>
|
|
1345
|
+
<p className="sc-muted">{words.settings.skillsFromConfiguration}</p>
|
|
1346
|
+
</section>
|
|
1347
|
+
)}
|
|
1348
|
+
</>
|
|
1349
|
+
);
|
|
1018
1350
|
}
|
|
1019
1351
|
|
|
1020
1352
|
/** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
|
|
1353
|
+
/** Whether a person is in: who a picker of assignees and of members offers (ADR-0022). */
|
|
1354
|
+
const inWorkspace = (person: { readonly status: "active" | "deactivated" }): boolean =>
|
|
1355
|
+
person.status === "active";
|
|
1356
|
+
|
|
1021
1357
|
const uploadOf = (file: File): FileUpload => ({
|
|
1022
1358
|
name: file.name,
|
|
1023
1359
|
bytes: file,
|
|
@@ -1082,9 +1418,6 @@ function PersonAgentsPage(props: {
|
|
|
1082
1418
|
</h1>
|
|
1083
1419
|
<LinkButton href={PATHS.people}>{words.nav.people}</LinkButton>
|
|
1084
1420
|
</div>
|
|
1085
|
-
<p className="sc-lead">
|
|
1086
|
-
{words.people.agentsLead(person?.name ?? person?.login ?? words.feed.someone)}
|
|
1087
|
-
</p>
|
|
1088
1421
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1089
1422
|
{tokens === undefined ? (
|
|
1090
1423
|
<div className="sc-loading">
|
|
@@ -1107,55 +1440,149 @@ function PersonAgentsPage(props: {
|
|
|
1107
1440
|
);
|
|
1108
1441
|
}
|
|
1109
1442
|
|
|
1110
|
-
/** What
|
|
1111
|
-
|
|
1443
|
+
/** What a plate's page shows: the tasks, and the decisions waiting on the person's own. */
|
|
1444
|
+
interface Plate {
|
|
1445
|
+
readonly tasks: RestWork["tasks"];
|
|
1446
|
+
readonly decisions?: RestWork["decisions"] | undefined;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* A plate, read when the page is opened: the person's own, or another person's for whoever
|
|
1451
|
+
* oversees them. The week at the top is the address's.
|
|
1452
|
+
*/
|
|
1453
|
+
function PlatePage(props: {
|
|
1454
|
+
readonly components: ConsoleComponents;
|
|
1455
|
+
/** Reads the plate; stable across renders, since the page reads again whenever it changes. */
|
|
1456
|
+
readonly load: (signal: AbortSignal) => Promise<Plate>;
|
|
1457
|
+
readonly title: string;
|
|
1458
|
+
/** What stands beside the title: the way back, on another person's plate. */
|
|
1459
|
+
readonly aside?: ReactNode;
|
|
1460
|
+
/** A day of the week shown at the top, from the address; today's week when it names none. */
|
|
1461
|
+
readonly week: string | undefined;
|
|
1462
|
+
readonly weekHref: (day: string) => string;
|
|
1463
|
+
readonly loading: string;
|
|
1464
|
+
readonly couldNotLoad: string;
|
|
1465
|
+
readonly empty: { readonly title: string; readonly body: string };
|
|
1466
|
+
/** What a refusal says where the plate may not be the asker's to see; the server's words otherwise. */
|
|
1467
|
+
readonly refused?: { readonly title: string; readonly body: string } | undefined;
|
|
1468
|
+
}) {
|
|
1112
1469
|
const words = useWords();
|
|
1113
|
-
const {
|
|
1114
|
-
const [
|
|
1470
|
+
const { components, load, couldNotLoad, refused } = props;
|
|
1471
|
+
const [plate, setPlate] = useState<Plate | undefined>(undefined);
|
|
1115
1472
|
const [problem, setProblem] = useState<string | undefined>(undefined);
|
|
1116
|
-
const
|
|
1117
|
-
const couldNotLoad = words.work.couldNotLoad;
|
|
1473
|
+
const [forbidden, setForbidden] = useState(false);
|
|
1118
1474
|
useEffect(() => {
|
|
1119
1475
|
const controller = new AbortController();
|
|
1476
|
+
setPlate(undefined);
|
|
1477
|
+
setProblem(undefined);
|
|
1478
|
+
setForbidden(false);
|
|
1120
1479
|
load(controller.signal)
|
|
1121
1480
|
.then((answer) => {
|
|
1122
1481
|
if (!controller.signal.aborted) {
|
|
1123
|
-
|
|
1482
|
+
setPlate(answer);
|
|
1124
1483
|
}
|
|
1125
1484
|
})
|
|
1126
1485
|
.catch((failure: unknown) => {
|
|
1127
|
-
if (
|
|
1486
|
+
if (controller.signal.aborted) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (
|
|
1490
|
+
refused !== undefined &&
|
|
1491
|
+
failure instanceof ApiError &&
|
|
1492
|
+
failure.code === "auth.forbidden"
|
|
1493
|
+
) {
|
|
1494
|
+
setForbidden(true);
|
|
1495
|
+
} else {
|
|
1128
1496
|
setProblem(errorWords(failure, couldNotLoad));
|
|
1129
1497
|
}
|
|
1130
1498
|
});
|
|
1131
1499
|
return () => controller.abort();
|
|
1132
|
-
}, [load, couldNotLoad]);
|
|
1500
|
+
}, [load, couldNotLoad, refused]);
|
|
1133
1501
|
return (
|
|
1134
1502
|
<>
|
|
1135
1503
|
<div className="sc-page-head">
|
|
1136
|
-
<h1 className="sc-page-title">{
|
|
1504
|
+
<h1 className="sc-page-title">{props.title}</h1>
|
|
1505
|
+
{props.aside}
|
|
1137
1506
|
</div>
|
|
1138
|
-
<p className="sc-lead">{words.work.lead}</p>
|
|
1139
1507
|
{problem !== undefined && <Callout tone="danger">{problem}</Callout>}
|
|
1140
|
-
{
|
|
1508
|
+
{forbidden && refused !== undefined ? (
|
|
1509
|
+
<EmptyState
|
|
1510
|
+
title={refused.title}
|
|
1511
|
+
body={refused.body}
|
|
1512
|
+
action={<LinkButton href={PATHS.work}>{words.nav.work}</LinkButton>}
|
|
1513
|
+
/>
|
|
1514
|
+
) : plate === undefined ? (
|
|
1141
1515
|
problem === undefined && (
|
|
1142
1516
|
<div className="sc-loading">
|
|
1143
|
-
<Spinner label={
|
|
1517
|
+
<Spinner label={props.loading} />
|
|
1144
1518
|
</div>
|
|
1145
1519
|
)
|
|
1146
|
-
) :
|
|
1147
|
-
<EmptyState title={
|
|
1520
|
+
) : plate.tasks.length === 0 && (plate.decisions?.length ?? 0) === 0 ? (
|
|
1521
|
+
<EmptyState title={props.empty.title} body={props.empty.body} />
|
|
1148
1522
|
) : (
|
|
1149
1523
|
<components.WorkList
|
|
1150
|
-
work={
|
|
1524
|
+
work={plate}
|
|
1525
|
+
week={props.week}
|
|
1151
1526
|
taskHref={(projectKey, task) => taskHref(projectKey, task.number)}
|
|
1152
1527
|
decisionHref={(projectKey, decisionId) => decisionHref(projectKey, decisionId)}
|
|
1528
|
+
weekHref={props.weekHref}
|
|
1153
1529
|
/>
|
|
1154
1530
|
)}
|
|
1155
1531
|
</>
|
|
1156
1532
|
);
|
|
1157
1533
|
}
|
|
1158
1534
|
|
|
1535
|
+
/** What is on the person's own plate, across their projects. */
|
|
1536
|
+
function WorkPage(props: {
|
|
1537
|
+
readonly data: ConsoleData;
|
|
1538
|
+
readonly components: ConsoleComponents;
|
|
1539
|
+
readonly week: string | undefined;
|
|
1540
|
+
}) {
|
|
1541
|
+
const words = useWords();
|
|
1542
|
+
const { work } = props.data.actions;
|
|
1543
|
+
return (
|
|
1544
|
+
<PlatePage
|
|
1545
|
+
components={props.components}
|
|
1546
|
+
load={work}
|
|
1547
|
+
title={words.work.title}
|
|
1548
|
+
week={props.week}
|
|
1549
|
+
weekHref={workHref}
|
|
1550
|
+
loading={words.work.loading}
|
|
1551
|
+
couldNotLoad={words.work.couldNotLoad}
|
|
1552
|
+
empty={words.work.empty}
|
|
1553
|
+
/>
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/** Another person's plate, for whoever oversees them: an administrator, or an owner of a project they are in. */
|
|
1558
|
+
function PersonWorkPage(props: {
|
|
1559
|
+
readonly id: string;
|
|
1560
|
+
readonly data: ConsoleData;
|
|
1561
|
+
readonly components: ConsoleComponents;
|
|
1562
|
+
readonly week: string | undefined;
|
|
1563
|
+
}) {
|
|
1564
|
+
const words = useWords();
|
|
1565
|
+
const { id, data } = props;
|
|
1566
|
+
const person = data.people.find((candidate) => candidate.id === id);
|
|
1567
|
+
const name = person === undefined ? "?" : personName(person);
|
|
1568
|
+
const { personWork } = data.actions;
|
|
1569
|
+
const load = useCallback((signal: AbortSignal) => personWork(id, signal), [personWork, id]);
|
|
1570
|
+
return (
|
|
1571
|
+
<PlatePage
|
|
1572
|
+
components={props.components}
|
|
1573
|
+
load={load}
|
|
1574
|
+
title={words.people.workOf(name)}
|
|
1575
|
+
aside={<LinkButton href={PATHS.people}>{words.nav.people}</LinkButton>}
|
|
1576
|
+
week={props.week}
|
|
1577
|
+
weekHref={(day) => personWorkHref(id, day)}
|
|
1578
|
+
loading={words.people.loadingWork}
|
|
1579
|
+
couldNotLoad={words.people.couldNotLoadWork}
|
|
1580
|
+
empty={{ title: words.people.noWork.title(name), body: words.people.noWork.body }}
|
|
1581
|
+
refused={words.people.notOverseen}
|
|
1582
|
+
/>
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1159
1586
|
function WorkspacePage(props: {
|
|
1160
1587
|
readonly route: Route;
|
|
1161
1588
|
readonly data: ConsoleData;
|
|
@@ -1168,7 +1595,13 @@ function WorkspacePage(props: {
|
|
|
1168
1595
|
const { busy, error, act } = useAction();
|
|
1169
1596
|
|
|
1170
1597
|
if (route.name === "work") {
|
|
1171
|
-
return <WorkPage data={data} components={components} />;
|
|
1598
|
+
return <WorkPage data={data} components={components} week={route.week} />;
|
|
1599
|
+
}
|
|
1600
|
+
if (route.name === "person-work") {
|
|
1601
|
+
return <PersonWorkPage id={route.id} data={data} components={components} week={route.week} />;
|
|
1602
|
+
}
|
|
1603
|
+
if (route.name === "workspace-settings") {
|
|
1604
|
+
return <WorkspaceSettingsPage form={route.form} data={data} navigation={navigation} />;
|
|
1172
1605
|
}
|
|
1173
1606
|
if (route.name === "categories") {
|
|
1174
1607
|
const me = data.me?.person ?? undefined;
|
|
@@ -1179,9 +1612,6 @@ function WorkspacePage(props: {
|
|
|
1179
1612
|
<div className="sc-page-head">
|
|
1180
1613
|
<h1 className="sc-page-title">{categories.title}</h1>
|
|
1181
1614
|
</div>
|
|
1182
|
-
<p className="sc-lead">
|
|
1183
|
-
{categories.lead} {categories.onlyAdmin}
|
|
1184
|
-
</p>
|
|
1185
1615
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1186
1616
|
<components.CategoryList
|
|
1187
1617
|
categories={data.categories}
|
|
@@ -1204,23 +1634,46 @@ function WorkspacePage(props: {
|
|
|
1204
1634
|
}
|
|
1205
1635
|
empty={<EmptyState title={categories.empty.title} body={categories.empty.body} />}
|
|
1206
1636
|
/>
|
|
1207
|
-
{administrator && <p className="sc-muted">{categories.inUse}</p>}
|
|
1208
1637
|
</>
|
|
1209
1638
|
);
|
|
1210
1639
|
}
|
|
1211
1640
|
|
|
1212
|
-
if (route.name === "people") {
|
|
1641
|
+
if (route.name === "people" || route.name === "new-listing") {
|
|
1213
1642
|
const me = data.me?.person ?? undefined;
|
|
1214
1643
|
const administrator = me?.role === "admin";
|
|
1644
|
+
const making = route.name === "new-listing" && administrator;
|
|
1645
|
+
// An owner sees the work of the people in their projects; the console refuses the rest.
|
|
1646
|
+
const oversees = administrator || data.projects.some((project) => project.role === "owner");
|
|
1215
1647
|
return (
|
|
1216
1648
|
<>
|
|
1217
1649
|
<div className="sc-page-head">
|
|
1218
1650
|
<h1 className="sc-page-title">{words.people.title}</h1>
|
|
1651
|
+
{administrator && !making && (
|
|
1652
|
+
<LinkButton href={PATHS.newListing} variant="primary">
|
|
1653
|
+
{words.people.letIn}
|
|
1654
|
+
</LinkButton>
|
|
1655
|
+
)}
|
|
1219
1656
|
</div>
|
|
1220
|
-
<p className="sc-lead">{words.people.lead}</p>
|
|
1221
1657
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1658
|
+
{making && (
|
|
1659
|
+
<section className="sc-panel sc-panel-form" aria-label={words.people.letInAria}>
|
|
1660
|
+
<ListingForm
|
|
1661
|
+
providers={data.me?.signIn ?? []}
|
|
1662
|
+
busy={busy}
|
|
1663
|
+
onSubmit={(input) =>
|
|
1664
|
+
void act(async () => {
|
|
1665
|
+
await data.actions.createListing(input);
|
|
1666
|
+
navigation.replace(PATHS.people);
|
|
1667
|
+
})
|
|
1668
|
+
}
|
|
1669
|
+
onCancel={() => navigation.back(PATHS.people)}
|
|
1670
|
+
/>
|
|
1671
|
+
</section>
|
|
1672
|
+
)}
|
|
1222
1673
|
<components.PeopleList
|
|
1223
1674
|
people={data.people}
|
|
1675
|
+
listed={data.listed}
|
|
1676
|
+
organization={data.organization}
|
|
1224
1677
|
me={me}
|
|
1225
1678
|
busy={busy}
|
|
1226
1679
|
onChangeRole={
|
|
@@ -1228,6 +1681,17 @@ function WorkspacePage(props: {
|
|
|
1228
1681
|
? (person, role) => void act(() => data.actions.updatePerson(person.id, { role }))
|
|
1229
1682
|
: undefined
|
|
1230
1683
|
}
|
|
1684
|
+
onChangeStatus={
|
|
1685
|
+
administrator
|
|
1686
|
+
? (person, status) => void act(() => data.actions.updatePerson(person.id, { status }))
|
|
1687
|
+
: undefined
|
|
1688
|
+
}
|
|
1689
|
+
onUnlist={
|
|
1690
|
+
administrator
|
|
1691
|
+
? (listing) => void act(() => data.actions.removeListing(listing.id))
|
|
1692
|
+
: undefined
|
|
1693
|
+
}
|
|
1694
|
+
workHref={oversees ? (person) => personWorkHref(person.id) : undefined}
|
|
1231
1695
|
agentsHref={administrator ? (person) => personAgentsHref(person.id) : undefined}
|
|
1232
1696
|
/>
|
|
1233
1697
|
</>
|
|
@@ -1253,7 +1717,6 @@ function WorkspacePage(props: {
|
|
|
1253
1717
|
</LinkButton>
|
|
1254
1718
|
)}
|
|
1255
1719
|
</div>
|
|
1256
|
-
<p className="sc-lead">{agents.lead}</p>
|
|
1257
1720
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1258
1721
|
{fresh !== undefined && (
|
|
1259
1722
|
<NewToken token={fresh.token} secret={fresh.secret} onDone={() => setFresh(undefined)} />
|
|
@@ -1376,7 +1839,10 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1376
1839
|
{
|
|
1377
1840
|
href: PATHS.people,
|
|
1378
1841
|
label: words.nav.people,
|
|
1379
|
-
current:
|
|
1842
|
+
current:
|
|
1843
|
+
route.name === "people" ||
|
|
1844
|
+
route.name === "new-listing" ||
|
|
1845
|
+
route.name === "person-agents",
|
|
1380
1846
|
},
|
|
1381
1847
|
...(viaToken
|
|
1382
1848
|
? []
|
|
@@ -1387,11 +1853,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1387
1853
|
current: route.name === "agents" || route.name === "new-token",
|
|
1388
1854
|
},
|
|
1389
1855
|
]),
|
|
1390
|
-
// The
|
|
1856
|
+
// The settings are read by everyone: the rules every agent follows, the categories and
|
|
1857
|
+
// what the colours mean, where the skills come from; kept by an administrator.
|
|
1391
1858
|
{
|
|
1392
|
-
href: PATHS.
|
|
1393
|
-
label: words.nav.
|
|
1394
|
-
current: route.name === "categories",
|
|
1859
|
+
href: PATHS.settings,
|
|
1860
|
+
label: words.nav.settings,
|
|
1861
|
+
current: route.name === "workspace-settings" || route.name === "categories",
|
|
1395
1862
|
},
|
|
1396
1863
|
...pages
|
|
1397
1864
|
.filter((candidate) => candidate.level === "workspace")
|
|
@@ -1454,15 +1921,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1454
1921
|
label: words.nav.members,
|
|
1455
1922
|
current: route.name === "members",
|
|
1456
1923
|
},
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
},
|
|
1464
|
-
]
|
|
1465
|
-
: []),
|
|
1924
|
+
// Read by every member: what the project is, and what its agents are asked (ADR-0023).
|
|
1925
|
+
{
|
|
1926
|
+
href: projectHref(projectKey, "settings"),
|
|
1927
|
+
label: words.nav.settings,
|
|
1928
|
+
current: route.name === "settings",
|
|
1929
|
+
},
|
|
1466
1930
|
...pages
|
|
1467
1931
|
.filter((candidate) => candidate.level === "project")
|
|
1468
1932
|
.map((candidate) => ({
|