@skillcdn/console 0.1.14 → 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.map +1 -1
- package/dist/components/calendar.js +39 -29
- 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 +2 -2
- 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 +5 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +1 -1
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +219 -13
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +197 -43
- 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/i18n/en.d.ts +83 -9
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +81 -11
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +81 -11
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- 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 +30 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +50 -10
- 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 +63 -50
- package/src/components/connect.tsx +12 -6
- package/src/components/decision-list.tsx +2 -1
- 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 -10
- package/src/components/tokens.tsx +2 -14
- package/src/components/ui.tsx +25 -2
- package/src/components/work.tsx +34 -28
- package/src/console.tsx +523 -45
- package/src/data.ts +52 -2
- package/src/i18n/en.ts +91 -12
- package/src/i18n/ko.ts +87 -12
- package/src/index.ts +16 -1
- package/src/limits.ts +4 -0
- package/src/router.ts +67 -12
- package/src/routes.ts +23 -3
- package/src/rules.ts +109 -0
- package/src/schemas.ts +121 -4
- package/src/styles/console.css +219 -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,8 +101,11 @@ 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,
|
|
@@ -390,6 +408,8 @@ function ProjectsPage(props: {
|
|
|
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}
|
|
@@ -645,7 +665,7 @@ function ProjectPage(props: {
|
|
|
645
665
|
filter={route.filter}
|
|
646
666
|
categories={data.categories}
|
|
647
667
|
goals={data.goals}
|
|
648
|
-
people={data.people}
|
|
668
|
+
people={data.people.filter(inWorkspace)}
|
|
649
669
|
me={me}
|
|
650
670
|
shown={shown.length}
|
|
651
671
|
total={data.tasks.length}
|
|
@@ -916,6 +936,10 @@ function ProjectPage(props: {
|
|
|
916
936
|
if (event.goalId !== null) {
|
|
917
937
|
return goalHref(key, event.goalId);
|
|
918
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
|
+
}
|
|
919
943
|
return event.data.path === undefined ? undefined : toDoc(event.data.path);
|
|
920
944
|
}}
|
|
921
945
|
empty={<EmptyState title={words.feed.empty} />}
|
|
@@ -946,7 +970,7 @@ function ProjectPage(props: {
|
|
|
946
970
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
947
971
|
<components.MemberList
|
|
948
972
|
members={data.members}
|
|
949
|
-
people={data.people}
|
|
973
|
+
people={data.people.filter(inWorkspace)}
|
|
950
974
|
me={me}
|
|
951
975
|
busy={busy}
|
|
952
976
|
onAdd={configures ? (input) => void act(() => data.actions.addMember(input)) : undefined}
|
|
@@ -961,6 +985,7 @@ function ProjectPage(props: {
|
|
|
961
985
|
? (member) => void act(() => data.actions.removeMember(member.person.id))
|
|
962
986
|
: undefined
|
|
963
987
|
}
|
|
988
|
+
workHref={owner ? (member) => personWorkHref(member.person.id) : undefined}
|
|
964
989
|
empty={
|
|
965
990
|
<EmptyState
|
|
966
991
|
title={members.empty.title}
|
|
@@ -974,20 +999,71 @@ function ProjectPage(props: {
|
|
|
974
999
|
);
|
|
975
1000
|
}
|
|
976
1001
|
if (route.name === "settings") {
|
|
977
|
-
|
|
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") {
|
|
978
1054
|
if (!owner) {
|
|
979
|
-
return <
|
|
1055
|
+
return <NothingHere />;
|
|
980
1056
|
}
|
|
981
1057
|
return (
|
|
982
1058
|
<>
|
|
983
|
-
|
|
984
|
-
<h1 className="sc-page-title">{settings.title}</h1>
|
|
985
|
-
</div>
|
|
1059
|
+
{head}
|
|
986
1060
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
987
1061
|
<section className="sc-panel sc-panel-form" aria-label={settings.aria}>
|
|
988
1062
|
<ProjectForm
|
|
989
1063
|
project={project}
|
|
990
1064
|
busy={busy}
|
|
1065
|
+
skillsSource={skills?.source}
|
|
1066
|
+
organizationAddress={skills?.address ?? null}
|
|
991
1067
|
onSubmit={(input, icon) =>
|
|
992
1068
|
void act(async () => {
|
|
993
1069
|
await data.actions.updateProject(input as RestProjectPatch);
|
|
@@ -996,17 +1072,288 @@ function ProjectPage(props: {
|
|
|
996
1072
|
} else if (icon !== undefined) {
|
|
997
1073
|
await data.actions.setProjectIcon(uploadOf(icon));
|
|
998
1074
|
}
|
|
1075
|
+
navigation.replace(here);
|
|
999
1076
|
})
|
|
1000
1077
|
}
|
|
1078
|
+
onCancel={() => navigation.back(here)}
|
|
1001
1079
|
/>
|
|
1002
1080
|
</section>
|
|
1003
1081
|
</>
|
|
1004
1082
|
);
|
|
1005
1083
|
}
|
|
1006
|
-
|
|
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
|
+
);
|
|
1007
1350
|
}
|
|
1008
1351
|
|
|
1009
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
|
+
|
|
1010
1357
|
const uploadOf = (file: File): FileUpload => ({
|
|
1011
1358
|
name: file.name,
|
|
1012
1359
|
bytes: file,
|
|
@@ -1093,61 +1440,149 @@ function PersonAgentsPage(props: {
|
|
|
1093
1440
|
);
|
|
1094
1441
|
}
|
|
1095
1442
|
|
|
1096
|
-
/** What
|
|
1097
|
-
|
|
1098
|
-
readonly
|
|
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: {
|
|
1099
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;
|
|
1100
1460
|
/** A day of the week shown at the top, from the address; today's week when it names none. */
|
|
1101
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;
|
|
1102
1468
|
}) {
|
|
1103
1469
|
const words = useWords();
|
|
1104
|
-
const {
|
|
1105
|
-
const [
|
|
1470
|
+
const { components, load, couldNotLoad, refused } = props;
|
|
1471
|
+
const [plate, setPlate] = useState<Plate | undefined>(undefined);
|
|
1106
1472
|
const [problem, setProblem] = useState<string | undefined>(undefined);
|
|
1107
|
-
const
|
|
1108
|
-
const couldNotLoad = words.work.couldNotLoad;
|
|
1473
|
+
const [forbidden, setForbidden] = useState(false);
|
|
1109
1474
|
useEffect(() => {
|
|
1110
1475
|
const controller = new AbortController();
|
|
1476
|
+
setPlate(undefined);
|
|
1477
|
+
setProblem(undefined);
|
|
1478
|
+
setForbidden(false);
|
|
1111
1479
|
load(controller.signal)
|
|
1112
1480
|
.then((answer) => {
|
|
1113
1481
|
if (!controller.signal.aborted) {
|
|
1114
|
-
|
|
1482
|
+
setPlate(answer);
|
|
1115
1483
|
}
|
|
1116
1484
|
})
|
|
1117
1485
|
.catch((failure: unknown) => {
|
|
1118
|
-
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 {
|
|
1119
1496
|
setProblem(errorWords(failure, couldNotLoad));
|
|
1120
1497
|
}
|
|
1121
1498
|
});
|
|
1122
1499
|
return () => controller.abort();
|
|
1123
|
-
}, [load, couldNotLoad]);
|
|
1500
|
+
}, [load, couldNotLoad, refused]);
|
|
1124
1501
|
return (
|
|
1125
1502
|
<>
|
|
1126
1503
|
<div className="sc-page-head">
|
|
1127
|
-
<h1 className="sc-page-title">{
|
|
1504
|
+
<h1 className="sc-page-title">{props.title}</h1>
|
|
1505
|
+
{props.aside}
|
|
1128
1506
|
</div>
|
|
1129
1507
|
{problem !== undefined && <Callout tone="danger">{problem}</Callout>}
|
|
1130
|
-
{
|
|
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 ? (
|
|
1131
1515
|
problem === undefined && (
|
|
1132
1516
|
<div className="sc-loading">
|
|
1133
|
-
<Spinner label={
|
|
1517
|
+
<Spinner label={props.loading} />
|
|
1134
1518
|
</div>
|
|
1135
1519
|
)
|
|
1136
|
-
) :
|
|
1137
|
-
<EmptyState title={
|
|
1520
|
+
) : plate.tasks.length === 0 && (plate.decisions?.length ?? 0) === 0 ? (
|
|
1521
|
+
<EmptyState title={props.empty.title} body={props.empty.body} />
|
|
1138
1522
|
) : (
|
|
1139
1523
|
<components.WorkList
|
|
1140
|
-
work={
|
|
1524
|
+
work={plate}
|
|
1141
1525
|
week={props.week}
|
|
1142
1526
|
taskHref={(projectKey, task) => taskHref(projectKey, task.number)}
|
|
1143
1527
|
decisionHref={(projectKey, decisionId) => decisionHref(projectKey, decisionId)}
|
|
1144
|
-
weekHref={
|
|
1528
|
+
weekHref={props.weekHref}
|
|
1145
1529
|
/>
|
|
1146
1530
|
)}
|
|
1147
1531
|
</>
|
|
1148
1532
|
);
|
|
1149
1533
|
}
|
|
1150
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
|
+
|
|
1151
1586
|
function WorkspacePage(props: {
|
|
1152
1587
|
readonly route: Route;
|
|
1153
1588
|
readonly data: ConsoleData;
|
|
@@ -1162,6 +1597,12 @@ function WorkspacePage(props: {
|
|
|
1162
1597
|
if (route.name === "work") {
|
|
1163
1598
|
return <WorkPage data={data} components={components} week={route.week} />;
|
|
1164
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} />;
|
|
1605
|
+
}
|
|
1165
1606
|
if (route.name === "categories") {
|
|
1166
1607
|
const me = data.me?.person ?? undefined;
|
|
1167
1608
|
const administrator = me?.role === "admin";
|
|
@@ -1197,17 +1638,42 @@ function WorkspacePage(props: {
|
|
|
1197
1638
|
);
|
|
1198
1639
|
}
|
|
1199
1640
|
|
|
1200
|
-
if (route.name === "people") {
|
|
1641
|
+
if (route.name === "people" || route.name === "new-listing") {
|
|
1201
1642
|
const me = data.me?.person ?? undefined;
|
|
1202
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");
|
|
1203
1647
|
return (
|
|
1204
1648
|
<>
|
|
1205
1649
|
<div className="sc-page-head">
|
|
1206
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
|
+
)}
|
|
1207
1656
|
</div>
|
|
1208
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
|
+
)}
|
|
1209
1673
|
<components.PeopleList
|
|
1210
1674
|
people={data.people}
|
|
1675
|
+
listed={data.listed}
|
|
1676
|
+
organization={data.organization}
|
|
1211
1677
|
me={me}
|
|
1212
1678
|
busy={busy}
|
|
1213
1679
|
onChangeRole={
|
|
@@ -1215,6 +1681,17 @@ function WorkspacePage(props: {
|
|
|
1215
1681
|
? (person, role) => void act(() => data.actions.updatePerson(person.id, { role }))
|
|
1216
1682
|
: undefined
|
|
1217
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}
|
|
1218
1695
|
agentsHref={administrator ? (person) => personAgentsHref(person.id) : undefined}
|
|
1219
1696
|
/>
|
|
1220
1697
|
</>
|
|
@@ -1362,7 +1839,10 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1362
1839
|
{
|
|
1363
1840
|
href: PATHS.people,
|
|
1364
1841
|
label: words.nav.people,
|
|
1365
|
-
current:
|
|
1842
|
+
current:
|
|
1843
|
+
route.name === "people" ||
|
|
1844
|
+
route.name === "new-listing" ||
|
|
1845
|
+
route.name === "person-agents",
|
|
1366
1846
|
},
|
|
1367
1847
|
...(viaToken
|
|
1368
1848
|
? []
|
|
@@ -1373,11 +1853,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1373
1853
|
current: route.name === "agents" || route.name === "new-token",
|
|
1374
1854
|
},
|
|
1375
1855
|
]),
|
|
1376
|
-
// 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.
|
|
1377
1858
|
{
|
|
1378
|
-
href: PATHS.
|
|
1379
|
-
label: words.nav.
|
|
1380
|
-
current: route.name === "categories",
|
|
1859
|
+
href: PATHS.settings,
|
|
1860
|
+
label: words.nav.settings,
|
|
1861
|
+
current: route.name === "workspace-settings" || route.name === "categories",
|
|
1381
1862
|
},
|
|
1382
1863
|
...pages
|
|
1383
1864
|
.filter((candidate) => candidate.level === "workspace")
|
|
@@ -1440,15 +1921,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1440
1921
|
label: words.nav.members,
|
|
1441
1922
|
current: route.name === "members",
|
|
1442
1923
|
},
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
},
|
|
1450
|
-
]
|
|
1451
|
-
: []),
|
|
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
|
+
},
|
|
1452
1930
|
...pages
|
|
1453
1931
|
.filter((candidate) => candidate.level === "project")
|
|
1454
1932
|
.map((candidate) => ({
|