@skillcdn/console 0.1.14 → 0.1.16
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 +225 -24
- 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 +28 -12
- 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 +35 -2
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +80 -31
- 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 +10 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +8 -5
- 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/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +1 -1
- package/dist/components/task-form.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 +263 -17
- 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 +84 -9
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +84 -13
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +84 -13
- 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 +33 -9
- 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 +264 -26
- package/src/cli/format.ts +74 -10
- package/src/cli/help.ts +31 -11
- package/src/cli/main.ts +5 -0
- package/src/client.ts +47 -1
- package/src/components/calendar.tsx +252 -51
- 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 +37 -14
- package/src/components/rules.tsx +167 -0
- package/src/components/runs.tsx +4 -2
- package/src/components/skills.tsx +22 -10
- package/src/components/task-form.tsx +1 -0
- 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 +524 -45
- package/src/data.ts +52 -2
- package/src/i18n/en.ts +94 -14
- package/src/i18n/ko.ts +90 -14
- 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 +263 -17
- 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}
|
|
@@ -657,6 +677,7 @@ function ProjectPage(props: {
|
|
|
657
677
|
month={month}
|
|
658
678
|
today={now}
|
|
659
679
|
categories={data.categories}
|
|
680
|
+
goals={data.goals}
|
|
660
681
|
taskHref={href}
|
|
661
682
|
monthHref={(wanted) => calendarHref(key, wanted, route.filter)}
|
|
662
683
|
/>
|
|
@@ -916,6 +937,10 @@ function ProjectPage(props: {
|
|
|
916
937
|
if (event.goalId !== null) {
|
|
917
938
|
return goalHref(key, event.goalId);
|
|
918
939
|
}
|
|
940
|
+
// The rules are a page shown on the settings, not among the documents (ADR-0023).
|
|
941
|
+
if (event.data.path === RULES_PATH) {
|
|
942
|
+
return projectSettingsHref(key);
|
|
943
|
+
}
|
|
919
944
|
return event.data.path === undefined ? undefined : toDoc(event.data.path);
|
|
920
945
|
}}
|
|
921
946
|
empty={<EmptyState title={words.feed.empty} />}
|
|
@@ -946,7 +971,7 @@ function ProjectPage(props: {
|
|
|
946
971
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
947
972
|
<components.MemberList
|
|
948
973
|
members={data.members}
|
|
949
|
-
people={data.people}
|
|
974
|
+
people={data.people.filter(inWorkspace)}
|
|
950
975
|
me={me}
|
|
951
976
|
busy={busy}
|
|
952
977
|
onAdd={configures ? (input) => void act(() => data.actions.addMember(input)) : undefined}
|
|
@@ -961,6 +986,7 @@ function ProjectPage(props: {
|
|
|
961
986
|
? (member) => void act(() => data.actions.removeMember(member.person.id))
|
|
962
987
|
: undefined
|
|
963
988
|
}
|
|
989
|
+
workHref={owner ? (member) => personWorkHref(member.person.id) : undefined}
|
|
964
990
|
empty={
|
|
965
991
|
<EmptyState
|
|
966
992
|
title={members.empty.title}
|
|
@@ -974,20 +1000,71 @@ function ProjectPage(props: {
|
|
|
974
1000
|
);
|
|
975
1001
|
}
|
|
976
1002
|
if (route.name === "settings") {
|
|
977
|
-
|
|
1003
|
+
return (
|
|
1004
|
+
<ProjectSettingsPage route={route} project={project} data={data} navigation={navigation} />
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return <NothingHere />;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* A project's settings (ADR-0023): read by every member, so that everyone sees what the project
|
|
1012
|
+
* is and what its agents are asked; changed by an owner, on the forms at their own addresses.
|
|
1013
|
+
*/
|
|
1014
|
+
function ProjectSettingsPage(props: {
|
|
1015
|
+
readonly route: Extract<Route, { name: "settings" }>;
|
|
1016
|
+
readonly project: RestProject;
|
|
1017
|
+
readonly data: ConsoleData;
|
|
1018
|
+
readonly navigation: Navigation;
|
|
1019
|
+
}) {
|
|
1020
|
+
const words = useWords();
|
|
1021
|
+
const { route, project, data, navigation } = props;
|
|
1022
|
+
const { busy, error, act } = useAction();
|
|
1023
|
+
const settings = words.projects.settings;
|
|
1024
|
+
const form = words.projects.form;
|
|
1025
|
+
const owner = project.role === "owner";
|
|
1026
|
+
const key = project.key;
|
|
1027
|
+
const here = projectSettingsHref(key);
|
|
1028
|
+
const skills = data.me?.workspace.skills;
|
|
1029
|
+
const [rules, setRules] = useState<RestProjectRules | undefined>(undefined);
|
|
1030
|
+
const feedLength = data.events.length;
|
|
1031
|
+
// The rules, asked for again whenever the project's feed grows.
|
|
1032
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: the feed's length is the trigger
|
|
1033
|
+
useEffect(() => {
|
|
1034
|
+
const controller = new AbortController();
|
|
1035
|
+
data.actions
|
|
1036
|
+
.projectRules(controller.signal)
|
|
1037
|
+
.then((found) => {
|
|
1038
|
+
if (!controller.signal.aborted) {
|
|
1039
|
+
setRules(found);
|
|
1040
|
+
}
|
|
1041
|
+
})
|
|
1042
|
+
.catch(() => undefined);
|
|
1043
|
+
return () => controller.abort();
|
|
1044
|
+
}, [data.actions, feedLength]);
|
|
1045
|
+
|
|
1046
|
+
const head = (
|
|
1047
|
+
<div className="sc-page-head">
|
|
1048
|
+
<h1 className="sc-page-title">{settings.title}</h1>
|
|
1049
|
+
{owner && route.form === undefined && (
|
|
1050
|
+
<LinkButton href={projectSettingsHref(key, "edit")}>{words.common.edit}</LinkButton>
|
|
1051
|
+
)}
|
|
1052
|
+
</div>
|
|
1053
|
+
);
|
|
1054
|
+
if (route.form === "edit") {
|
|
978
1055
|
if (!owner) {
|
|
979
|
-
return <
|
|
1056
|
+
return <NothingHere />;
|
|
980
1057
|
}
|
|
981
1058
|
return (
|
|
982
1059
|
<>
|
|
983
|
-
|
|
984
|
-
<h1 className="sc-page-title">{settings.title}</h1>
|
|
985
|
-
</div>
|
|
1060
|
+
{head}
|
|
986
1061
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
987
1062
|
<section className="sc-panel sc-panel-form" aria-label={settings.aria}>
|
|
988
1063
|
<ProjectForm
|
|
989
1064
|
project={project}
|
|
990
1065
|
busy={busy}
|
|
1066
|
+
skillsSource={skills?.source}
|
|
1067
|
+
organizationAddress={skills?.address ?? null}
|
|
991
1068
|
onSubmit={(input, icon) =>
|
|
992
1069
|
void act(async () => {
|
|
993
1070
|
await data.actions.updateProject(input as RestProjectPatch);
|
|
@@ -996,17 +1073,288 @@ function ProjectPage(props: {
|
|
|
996
1073
|
} else if (icon !== undefined) {
|
|
997
1074
|
await data.actions.setProjectIcon(uploadOf(icon));
|
|
998
1075
|
}
|
|
1076
|
+
navigation.replace(here);
|
|
999
1077
|
})
|
|
1000
1078
|
}
|
|
1079
|
+
onCancel={() => navigation.back(here)}
|
|
1001
1080
|
/>
|
|
1002
1081
|
</section>
|
|
1003
1082
|
</>
|
|
1004
1083
|
);
|
|
1005
1084
|
}
|
|
1006
|
-
|
|
1085
|
+
if (route.form === "rules") {
|
|
1086
|
+
if (!owner) {
|
|
1087
|
+
return <NothingHere />;
|
|
1088
|
+
}
|
|
1089
|
+
return (
|
|
1090
|
+
<>
|
|
1091
|
+
{head}
|
|
1092
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1093
|
+
<section className="sc-panel sc-panel-form" aria-label={words.rules.projectRules}>
|
|
1094
|
+
{rules === undefined ? (
|
|
1095
|
+
<Spinner label={words.rules.loading} />
|
|
1096
|
+
) : (
|
|
1097
|
+
<RulesForm
|
|
1098
|
+
rules={rules.project}
|
|
1099
|
+
scope="project"
|
|
1100
|
+
template={PROJECT_RULES_TEMPLATE}
|
|
1101
|
+
busy={busy}
|
|
1102
|
+
onSubmit={(input) =>
|
|
1103
|
+
void act(async () => {
|
|
1104
|
+
await data.actions.writeDocument(RULES_PATH, {
|
|
1105
|
+
title: RULES_TITLE,
|
|
1106
|
+
body: input.body,
|
|
1107
|
+
...(input.baseVersion === undefined ? {} : { baseVersion: input.baseVersion }),
|
|
1108
|
+
});
|
|
1109
|
+
navigation.replace(here);
|
|
1110
|
+
})
|
|
1111
|
+
}
|
|
1112
|
+
onCancel={() => navigation.back(here)}
|
|
1113
|
+
/>
|
|
1114
|
+
)}
|
|
1115
|
+
</section>
|
|
1116
|
+
</>
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
return (
|
|
1120
|
+
<>
|
|
1121
|
+
{head}
|
|
1122
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1123
|
+
<section className="sc-panel" aria-label={settings.aria}>
|
|
1124
|
+
<dl className="sc-facts sc-facts-plain">
|
|
1125
|
+
<div>
|
|
1126
|
+
<dt>{form.name}</dt>
|
|
1127
|
+
<dd>{project.name}</dd>
|
|
1128
|
+
</div>
|
|
1129
|
+
<div>
|
|
1130
|
+
<dt>{form.key}</dt>
|
|
1131
|
+
<dd>
|
|
1132
|
+
<code className="sc-project-key">{project.key}</code>
|
|
1133
|
+
</dd>
|
|
1134
|
+
</div>
|
|
1135
|
+
<div>
|
|
1136
|
+
<dt>{form.whoIsMember}</dt>
|
|
1137
|
+
<dd>{words.vocabulary.visibility[project.visibility]}</dd>
|
|
1138
|
+
</div>
|
|
1139
|
+
<div>
|
|
1140
|
+
<dt>{form.skillsAddress}</dt>
|
|
1141
|
+
<dd>
|
|
1142
|
+
{project.skillsAddress !== null ? (
|
|
1143
|
+
<code>
|
|
1144
|
+
{skills?.source ?? ""}
|
|
1145
|
+
{project.skillsAddress}
|
|
1146
|
+
</code>
|
|
1147
|
+
) : skills?.address != null ? (
|
|
1148
|
+
settings.organizationsSkills(skills.address)
|
|
1149
|
+
) : (
|
|
1150
|
+
settings.noSkills
|
|
1151
|
+
)}
|
|
1152
|
+
</dd>
|
|
1153
|
+
</div>
|
|
1154
|
+
<div>
|
|
1155
|
+
<dt>{form.symbol}</dt>
|
|
1156
|
+
<dd>
|
|
1157
|
+
<ProjectMark project={project} size="sm" />
|
|
1158
|
+
</dd>
|
|
1159
|
+
</div>
|
|
1160
|
+
{project.description.length > 0 && (
|
|
1161
|
+
<div className="sc-facts-wide">
|
|
1162
|
+
<dt>{form.description}</dt>
|
|
1163
|
+
<dd>{project.description}</dd>
|
|
1164
|
+
</div>
|
|
1165
|
+
)}
|
|
1166
|
+
</dl>
|
|
1167
|
+
</section>
|
|
1168
|
+
<section className="sc-panel" aria-label={words.rules.projectRules}>
|
|
1169
|
+
<h2 className="sc-section-title">{words.rules.projectRules}</h2>
|
|
1170
|
+
{rules === undefined ? (
|
|
1171
|
+
<Spinner label={words.rules.loading} />
|
|
1172
|
+
) : (
|
|
1173
|
+
<RulesView
|
|
1174
|
+
rules={rules.project}
|
|
1175
|
+
scope="project"
|
|
1176
|
+
docHref={(path) => docHref(key, path)}
|
|
1177
|
+
editHref={owner ? projectSettingsHref(key, "rules") : undefined}
|
|
1178
|
+
onArchive={
|
|
1179
|
+
owner
|
|
1180
|
+
? () =>
|
|
1181
|
+
void act(async () => {
|
|
1182
|
+
await data.actions.archiveDocument(RULES_PATH);
|
|
1183
|
+
setRules(await data.actions.projectRules());
|
|
1184
|
+
})
|
|
1185
|
+
: undefined
|
|
1186
|
+
}
|
|
1187
|
+
busy={busy}
|
|
1188
|
+
/>
|
|
1189
|
+
)}
|
|
1190
|
+
</section>
|
|
1191
|
+
<section className="sc-panel" aria-label={words.rules.workspaceRules}>
|
|
1192
|
+
<div className="sc-section-head">
|
|
1193
|
+
<h2 className="sc-section-title">{words.rules.workspaceRules}</h2>
|
|
1194
|
+
<a className="sc-muted" href={settingsHref()}>
|
|
1195
|
+
{words.rules.onWorkspaceSettings}
|
|
1196
|
+
</a>
|
|
1197
|
+
</div>
|
|
1198
|
+
{rules !== undefined && <RulesView rules={rules.workspace} scope="workspace" folded />}
|
|
1199
|
+
</section>
|
|
1200
|
+
</>
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* The workspace's settings (ADR-0023): its rules, the console's own until an administrator
|
|
1206
|
+
* writes the organization's; its categories, kept on their own page; and where its skills come
|
|
1207
|
+
* from, which whoever runs the console set. Read by everyone; the rules written by an administrator.
|
|
1208
|
+
*/
|
|
1209
|
+
function WorkspaceSettingsPage(props: {
|
|
1210
|
+
readonly form: "rules" | undefined;
|
|
1211
|
+
readonly data: ConsoleData;
|
|
1212
|
+
readonly navigation: Navigation;
|
|
1213
|
+
}) {
|
|
1214
|
+
const words = useWords();
|
|
1215
|
+
const { form, data, navigation } = props;
|
|
1216
|
+
const { busy, error, act } = useAction();
|
|
1217
|
+
const administrator = data.me?.person?.role === "admin";
|
|
1218
|
+
const skills = data.me?.workspace.skills;
|
|
1219
|
+
const [rules, setRules] = useState<RestRules | undefined>(undefined);
|
|
1220
|
+
useEffect(() => {
|
|
1221
|
+
const controller = new AbortController();
|
|
1222
|
+
data.actions
|
|
1223
|
+
.rules(controller.signal)
|
|
1224
|
+
.then((found) => {
|
|
1225
|
+
if (!controller.signal.aborted) {
|
|
1226
|
+
setRules(found);
|
|
1227
|
+
}
|
|
1228
|
+
})
|
|
1229
|
+
.catch(() => undefined);
|
|
1230
|
+
return () => controller.abort();
|
|
1231
|
+
}, [data.actions]);
|
|
1232
|
+
|
|
1233
|
+
const head = (
|
|
1234
|
+
<div className="sc-page-head">
|
|
1235
|
+
<h1 className="sc-page-title">{words.settings.title}</h1>
|
|
1236
|
+
</div>
|
|
1237
|
+
);
|
|
1238
|
+
if (form === "rules") {
|
|
1239
|
+
if (!administrator) {
|
|
1240
|
+
return <NothingHere />;
|
|
1241
|
+
}
|
|
1242
|
+
return (
|
|
1243
|
+
<>
|
|
1244
|
+
{head}
|
|
1245
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1246
|
+
<section className="sc-panel sc-panel-form" aria-label={words.rules.title}>
|
|
1247
|
+
{rules === undefined ? (
|
|
1248
|
+
<Spinner label={words.rules.loading} />
|
|
1249
|
+
) : (
|
|
1250
|
+
<RulesForm
|
|
1251
|
+
rules={rules}
|
|
1252
|
+
scope="workspace"
|
|
1253
|
+
template={REFERENCE_RULES}
|
|
1254
|
+
busy={busy}
|
|
1255
|
+
onSubmit={(input) =>
|
|
1256
|
+
void act(async () => {
|
|
1257
|
+
setRules(
|
|
1258
|
+
await data.actions.writeRules({
|
|
1259
|
+
body: input.body,
|
|
1260
|
+
...(input.baseVersion === undefined
|
|
1261
|
+
? {}
|
|
1262
|
+
: { baseVersion: input.baseVersion }),
|
|
1263
|
+
}),
|
|
1264
|
+
);
|
|
1265
|
+
navigation.replace(PATHS.settings);
|
|
1266
|
+
})
|
|
1267
|
+
}
|
|
1268
|
+
onCancel={() => navigation.back(PATHS.settings)}
|
|
1269
|
+
onReset={() =>
|
|
1270
|
+
void act(async () => {
|
|
1271
|
+
setRules(await data.actions.archiveRules());
|
|
1272
|
+
navigation.replace(PATHS.settings);
|
|
1273
|
+
})
|
|
1274
|
+
}
|
|
1275
|
+
/>
|
|
1276
|
+
)}
|
|
1277
|
+
</section>
|
|
1278
|
+
</>
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
return (
|
|
1282
|
+
<>
|
|
1283
|
+
{head}
|
|
1284
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1285
|
+
<section className="sc-panel" aria-label={words.rules.title}>
|
|
1286
|
+
<h2 className="sc-section-title">{words.rules.title}</h2>
|
|
1287
|
+
{rules === undefined ? (
|
|
1288
|
+
<Spinner label={words.rules.loading} />
|
|
1289
|
+
) : (
|
|
1290
|
+
<RulesView
|
|
1291
|
+
rules={rules}
|
|
1292
|
+
scope="workspace"
|
|
1293
|
+
editHref={administrator ? settingsHref("rules") : undefined}
|
|
1294
|
+
onArchive={
|
|
1295
|
+
administrator
|
|
1296
|
+
? () =>
|
|
1297
|
+
void act(async () => {
|
|
1298
|
+
setRules(await data.actions.archiveRules());
|
|
1299
|
+
})
|
|
1300
|
+
: undefined
|
|
1301
|
+
}
|
|
1302
|
+
busy={busy}
|
|
1303
|
+
/>
|
|
1304
|
+
)}
|
|
1305
|
+
</section>
|
|
1306
|
+
<section className="sc-panel" aria-label={words.settings.categories}>
|
|
1307
|
+
<h2 className="sc-section-title">{words.settings.categories}</h2>
|
|
1308
|
+
{data.categories.length > 0 && (
|
|
1309
|
+
<p className="sc-category-chips">
|
|
1310
|
+
{data.categories.map((category) => (
|
|
1311
|
+
<CategoryBadge key={category.key} category={category} />
|
|
1312
|
+
))}
|
|
1313
|
+
</p>
|
|
1314
|
+
)}
|
|
1315
|
+
<LinkButton href={PATHS.categories}>{words.settings.manageCategories}</LinkButton>
|
|
1316
|
+
</section>
|
|
1317
|
+
{skills !== undefined && (
|
|
1318
|
+
<section className="sc-panel" aria-label={words.settings.skills}>
|
|
1319
|
+
<h2 className="sc-section-title">{words.settings.skills}</h2>
|
|
1320
|
+
<dl className="sc-facts sc-facts-plain">
|
|
1321
|
+
<div>
|
|
1322
|
+
<dt>{words.settings.skillsSource}</dt>
|
|
1323
|
+
<dd>
|
|
1324
|
+
<a href={skills.source} target="_blank" rel="noopener noreferrer">
|
|
1325
|
+
{skills.source}
|
|
1326
|
+
</a>
|
|
1327
|
+
</dd>
|
|
1328
|
+
</div>
|
|
1329
|
+
<div>
|
|
1330
|
+
<dt>{words.settings.skillsAddress}</dt>
|
|
1331
|
+
<dd>
|
|
1332
|
+
{skills.address === null ? (
|
|
1333
|
+
words.settings.noSkillsAddress
|
|
1334
|
+
) : (
|
|
1335
|
+
<a
|
|
1336
|
+
href={`${skills.source}${skills.address}`}
|
|
1337
|
+
target="_blank"
|
|
1338
|
+
rel="noopener noreferrer"
|
|
1339
|
+
>
|
|
1340
|
+
<code>{skills.address}</code>
|
|
1341
|
+
</a>
|
|
1342
|
+
)}
|
|
1343
|
+
</dd>
|
|
1344
|
+
</div>
|
|
1345
|
+
</dl>
|
|
1346
|
+
<p className="sc-muted">{words.settings.skillsFromConfiguration}</p>
|
|
1347
|
+
</section>
|
|
1348
|
+
)}
|
|
1349
|
+
</>
|
|
1350
|
+
);
|
|
1007
1351
|
}
|
|
1008
1352
|
|
|
1009
1353
|
/** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
|
|
1354
|
+
/** Whether a person is in: who a picker of assignees and of members offers (ADR-0022). */
|
|
1355
|
+
const inWorkspace = (person: { readonly status: "active" | "deactivated" }): boolean =>
|
|
1356
|
+
person.status === "active";
|
|
1357
|
+
|
|
1010
1358
|
const uploadOf = (file: File): FileUpload => ({
|
|
1011
1359
|
name: file.name,
|
|
1012
1360
|
bytes: file,
|
|
@@ -1093,61 +1441,149 @@ function PersonAgentsPage(props: {
|
|
|
1093
1441
|
);
|
|
1094
1442
|
}
|
|
1095
1443
|
|
|
1096
|
-
/** What
|
|
1097
|
-
|
|
1098
|
-
readonly
|
|
1444
|
+
/** What a plate's page shows: the tasks, and the decisions waiting on the person's own. */
|
|
1445
|
+
interface Plate {
|
|
1446
|
+
readonly tasks: RestWork["tasks"];
|
|
1447
|
+
readonly decisions?: RestWork["decisions"] | undefined;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* A plate, read when the page is opened: the person's own, or another person's for whoever
|
|
1452
|
+
* oversees them. The week at the top is the address's.
|
|
1453
|
+
*/
|
|
1454
|
+
function PlatePage(props: {
|
|
1099
1455
|
readonly components: ConsoleComponents;
|
|
1456
|
+
/** Reads the plate; stable across renders, since the page reads again whenever it changes. */
|
|
1457
|
+
readonly load: (signal: AbortSignal) => Promise<Plate>;
|
|
1458
|
+
readonly title: string;
|
|
1459
|
+
/** What stands beside the title: the way back, on another person's plate. */
|
|
1460
|
+
readonly aside?: ReactNode;
|
|
1100
1461
|
/** A day of the week shown at the top, from the address; today's week when it names none. */
|
|
1101
1462
|
readonly week: string | undefined;
|
|
1463
|
+
readonly weekHref: (day: string) => string;
|
|
1464
|
+
readonly loading: string;
|
|
1465
|
+
readonly couldNotLoad: string;
|
|
1466
|
+
readonly empty: { readonly title: string; readonly body: string };
|
|
1467
|
+
/** What a refusal says where the plate may not be the asker's to see; the server's words otherwise. */
|
|
1468
|
+
readonly refused?: { readonly title: string; readonly body: string } | undefined;
|
|
1102
1469
|
}) {
|
|
1103
1470
|
const words = useWords();
|
|
1104
|
-
const {
|
|
1105
|
-
const [
|
|
1471
|
+
const { components, load, couldNotLoad, refused } = props;
|
|
1472
|
+
const [plate, setPlate] = useState<Plate | undefined>(undefined);
|
|
1106
1473
|
const [problem, setProblem] = useState<string | undefined>(undefined);
|
|
1107
|
-
const
|
|
1108
|
-
const couldNotLoad = words.work.couldNotLoad;
|
|
1474
|
+
const [forbidden, setForbidden] = useState(false);
|
|
1109
1475
|
useEffect(() => {
|
|
1110
1476
|
const controller = new AbortController();
|
|
1477
|
+
setPlate(undefined);
|
|
1478
|
+
setProblem(undefined);
|
|
1479
|
+
setForbidden(false);
|
|
1111
1480
|
load(controller.signal)
|
|
1112
1481
|
.then((answer) => {
|
|
1113
1482
|
if (!controller.signal.aborted) {
|
|
1114
|
-
|
|
1483
|
+
setPlate(answer);
|
|
1115
1484
|
}
|
|
1116
1485
|
})
|
|
1117
1486
|
.catch((failure: unknown) => {
|
|
1118
|
-
if (
|
|
1487
|
+
if (controller.signal.aborted) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (
|
|
1491
|
+
refused !== undefined &&
|
|
1492
|
+
failure instanceof ApiError &&
|
|
1493
|
+
failure.code === "auth.forbidden"
|
|
1494
|
+
) {
|
|
1495
|
+
setForbidden(true);
|
|
1496
|
+
} else {
|
|
1119
1497
|
setProblem(errorWords(failure, couldNotLoad));
|
|
1120
1498
|
}
|
|
1121
1499
|
});
|
|
1122
1500
|
return () => controller.abort();
|
|
1123
|
-
}, [load, couldNotLoad]);
|
|
1501
|
+
}, [load, couldNotLoad, refused]);
|
|
1124
1502
|
return (
|
|
1125
1503
|
<>
|
|
1126
1504
|
<div className="sc-page-head">
|
|
1127
|
-
<h1 className="sc-page-title">{
|
|
1505
|
+
<h1 className="sc-page-title">{props.title}</h1>
|
|
1506
|
+
{props.aside}
|
|
1128
1507
|
</div>
|
|
1129
1508
|
{problem !== undefined && <Callout tone="danger">{problem}</Callout>}
|
|
1130
|
-
{
|
|
1509
|
+
{forbidden && refused !== undefined ? (
|
|
1510
|
+
<EmptyState
|
|
1511
|
+
title={refused.title}
|
|
1512
|
+
body={refused.body}
|
|
1513
|
+
action={<LinkButton href={PATHS.work}>{words.nav.work}</LinkButton>}
|
|
1514
|
+
/>
|
|
1515
|
+
) : plate === undefined ? (
|
|
1131
1516
|
problem === undefined && (
|
|
1132
1517
|
<div className="sc-loading">
|
|
1133
|
-
<Spinner label={
|
|
1518
|
+
<Spinner label={props.loading} />
|
|
1134
1519
|
</div>
|
|
1135
1520
|
)
|
|
1136
|
-
) :
|
|
1137
|
-
<EmptyState title={
|
|
1521
|
+
) : plate.tasks.length === 0 && (plate.decisions?.length ?? 0) === 0 ? (
|
|
1522
|
+
<EmptyState title={props.empty.title} body={props.empty.body} />
|
|
1138
1523
|
) : (
|
|
1139
1524
|
<components.WorkList
|
|
1140
|
-
work={
|
|
1525
|
+
work={plate}
|
|
1141
1526
|
week={props.week}
|
|
1142
1527
|
taskHref={(projectKey, task) => taskHref(projectKey, task.number)}
|
|
1143
1528
|
decisionHref={(projectKey, decisionId) => decisionHref(projectKey, decisionId)}
|
|
1144
|
-
weekHref={
|
|
1529
|
+
weekHref={props.weekHref}
|
|
1145
1530
|
/>
|
|
1146
1531
|
)}
|
|
1147
1532
|
</>
|
|
1148
1533
|
);
|
|
1149
1534
|
}
|
|
1150
1535
|
|
|
1536
|
+
/** What is on the person's own plate, across their projects. */
|
|
1537
|
+
function WorkPage(props: {
|
|
1538
|
+
readonly data: ConsoleData;
|
|
1539
|
+
readonly components: ConsoleComponents;
|
|
1540
|
+
readonly week: string | undefined;
|
|
1541
|
+
}) {
|
|
1542
|
+
const words = useWords();
|
|
1543
|
+
const { work } = props.data.actions;
|
|
1544
|
+
return (
|
|
1545
|
+
<PlatePage
|
|
1546
|
+
components={props.components}
|
|
1547
|
+
load={work}
|
|
1548
|
+
title={words.work.title}
|
|
1549
|
+
week={props.week}
|
|
1550
|
+
weekHref={workHref}
|
|
1551
|
+
loading={words.work.loading}
|
|
1552
|
+
couldNotLoad={words.work.couldNotLoad}
|
|
1553
|
+
empty={words.work.empty}
|
|
1554
|
+
/>
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/** Another person's plate, for whoever oversees them: an administrator, or an owner of a project they are in. */
|
|
1559
|
+
function PersonWorkPage(props: {
|
|
1560
|
+
readonly id: string;
|
|
1561
|
+
readonly data: ConsoleData;
|
|
1562
|
+
readonly components: ConsoleComponents;
|
|
1563
|
+
readonly week: string | undefined;
|
|
1564
|
+
}) {
|
|
1565
|
+
const words = useWords();
|
|
1566
|
+
const { id, data } = props;
|
|
1567
|
+
const person = data.people.find((candidate) => candidate.id === id);
|
|
1568
|
+
const name = person === undefined ? "?" : personName(person);
|
|
1569
|
+
const { personWork } = data.actions;
|
|
1570
|
+
const load = useCallback((signal: AbortSignal) => personWork(id, signal), [personWork, id]);
|
|
1571
|
+
return (
|
|
1572
|
+
<PlatePage
|
|
1573
|
+
components={props.components}
|
|
1574
|
+
load={load}
|
|
1575
|
+
title={words.people.workOf(name)}
|
|
1576
|
+
aside={<LinkButton href={PATHS.people}>{words.nav.people}</LinkButton>}
|
|
1577
|
+
week={props.week}
|
|
1578
|
+
weekHref={(day) => personWorkHref(id, day)}
|
|
1579
|
+
loading={words.people.loadingWork}
|
|
1580
|
+
couldNotLoad={words.people.couldNotLoadWork}
|
|
1581
|
+
empty={{ title: words.people.noWork.title(name), body: words.people.noWork.body }}
|
|
1582
|
+
refused={words.people.notOverseen}
|
|
1583
|
+
/>
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1151
1587
|
function WorkspacePage(props: {
|
|
1152
1588
|
readonly route: Route;
|
|
1153
1589
|
readonly data: ConsoleData;
|
|
@@ -1162,6 +1598,12 @@ function WorkspacePage(props: {
|
|
|
1162
1598
|
if (route.name === "work") {
|
|
1163
1599
|
return <WorkPage data={data} components={components} week={route.week} />;
|
|
1164
1600
|
}
|
|
1601
|
+
if (route.name === "person-work") {
|
|
1602
|
+
return <PersonWorkPage id={route.id} data={data} components={components} week={route.week} />;
|
|
1603
|
+
}
|
|
1604
|
+
if (route.name === "workspace-settings") {
|
|
1605
|
+
return <WorkspaceSettingsPage form={route.form} data={data} navigation={navigation} />;
|
|
1606
|
+
}
|
|
1165
1607
|
if (route.name === "categories") {
|
|
1166
1608
|
const me = data.me?.person ?? undefined;
|
|
1167
1609
|
const administrator = me?.role === "admin";
|
|
@@ -1197,17 +1639,42 @@ function WorkspacePage(props: {
|
|
|
1197
1639
|
);
|
|
1198
1640
|
}
|
|
1199
1641
|
|
|
1200
|
-
if (route.name === "people") {
|
|
1642
|
+
if (route.name === "people" || route.name === "new-listing") {
|
|
1201
1643
|
const me = data.me?.person ?? undefined;
|
|
1202
1644
|
const administrator = me?.role === "admin";
|
|
1645
|
+
const making = route.name === "new-listing" && administrator;
|
|
1646
|
+
// An owner sees the work of the people in their projects; the console refuses the rest.
|
|
1647
|
+
const oversees = administrator || data.projects.some((project) => project.role === "owner");
|
|
1203
1648
|
return (
|
|
1204
1649
|
<>
|
|
1205
1650
|
<div className="sc-page-head">
|
|
1206
1651
|
<h1 className="sc-page-title">{words.people.title}</h1>
|
|
1652
|
+
{administrator && !making && (
|
|
1653
|
+
<LinkButton href={PATHS.newListing} variant="primary">
|
|
1654
|
+
{words.people.letIn}
|
|
1655
|
+
</LinkButton>
|
|
1656
|
+
)}
|
|
1207
1657
|
</div>
|
|
1208
1658
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1659
|
+
{making && (
|
|
1660
|
+
<section className="sc-panel sc-panel-form" aria-label={words.people.letInAria}>
|
|
1661
|
+
<ListingForm
|
|
1662
|
+
providers={data.me?.signIn ?? []}
|
|
1663
|
+
busy={busy}
|
|
1664
|
+
onSubmit={(input) =>
|
|
1665
|
+
void act(async () => {
|
|
1666
|
+
await data.actions.createListing(input);
|
|
1667
|
+
navigation.replace(PATHS.people);
|
|
1668
|
+
})
|
|
1669
|
+
}
|
|
1670
|
+
onCancel={() => navigation.back(PATHS.people)}
|
|
1671
|
+
/>
|
|
1672
|
+
</section>
|
|
1673
|
+
)}
|
|
1209
1674
|
<components.PeopleList
|
|
1210
1675
|
people={data.people}
|
|
1676
|
+
listed={data.listed}
|
|
1677
|
+
organization={data.organization}
|
|
1211
1678
|
me={me}
|
|
1212
1679
|
busy={busy}
|
|
1213
1680
|
onChangeRole={
|
|
@@ -1215,6 +1682,17 @@ function WorkspacePage(props: {
|
|
|
1215
1682
|
? (person, role) => void act(() => data.actions.updatePerson(person.id, { role }))
|
|
1216
1683
|
: undefined
|
|
1217
1684
|
}
|
|
1685
|
+
onChangeStatus={
|
|
1686
|
+
administrator
|
|
1687
|
+
? (person, status) => void act(() => data.actions.updatePerson(person.id, { status }))
|
|
1688
|
+
: undefined
|
|
1689
|
+
}
|
|
1690
|
+
onUnlist={
|
|
1691
|
+
administrator
|
|
1692
|
+
? (listing) => void act(() => data.actions.removeListing(listing.id))
|
|
1693
|
+
: undefined
|
|
1694
|
+
}
|
|
1695
|
+
workHref={oversees ? (person) => personWorkHref(person.id) : undefined}
|
|
1218
1696
|
agentsHref={administrator ? (person) => personAgentsHref(person.id) : undefined}
|
|
1219
1697
|
/>
|
|
1220
1698
|
</>
|
|
@@ -1362,7 +1840,10 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1362
1840
|
{
|
|
1363
1841
|
href: PATHS.people,
|
|
1364
1842
|
label: words.nav.people,
|
|
1365
|
-
current:
|
|
1843
|
+
current:
|
|
1844
|
+
route.name === "people" ||
|
|
1845
|
+
route.name === "new-listing" ||
|
|
1846
|
+
route.name === "person-agents",
|
|
1366
1847
|
},
|
|
1367
1848
|
...(viaToken
|
|
1368
1849
|
? []
|
|
@@ -1373,11 +1854,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1373
1854
|
current: route.name === "agents" || route.name === "new-token",
|
|
1374
1855
|
},
|
|
1375
1856
|
]),
|
|
1376
|
-
// The
|
|
1857
|
+
// The settings are read by everyone: the rules every agent follows, the categories and
|
|
1858
|
+
// what the colours mean, where the skills come from; kept by an administrator.
|
|
1377
1859
|
{
|
|
1378
|
-
href: PATHS.
|
|
1379
|
-
label: words.nav.
|
|
1380
|
-
current: route.name === "categories",
|
|
1860
|
+
href: PATHS.settings,
|
|
1861
|
+
label: words.nav.settings,
|
|
1862
|
+
current: route.name === "workspace-settings" || route.name === "categories",
|
|
1381
1863
|
},
|
|
1382
1864
|
...pages
|
|
1383
1865
|
.filter((candidate) => candidate.level === "workspace")
|
|
@@ -1440,15 +1922,12 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1440
1922
|
label: words.nav.members,
|
|
1441
1923
|
current: route.name === "members",
|
|
1442
1924
|
},
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
},
|
|
1450
|
-
]
|
|
1451
|
-
: []),
|
|
1925
|
+
// Read by every member: what the project is, and what its agents are asked (ADR-0023).
|
|
1926
|
+
{
|
|
1927
|
+
href: projectHref(projectKey, "settings"),
|
|
1928
|
+
label: words.nav.settings,
|
|
1929
|
+
current: route.name === "settings",
|
|
1930
|
+
},
|
|
1452
1931
|
...pages
|
|
1453
1932
|
.filter((candidate) => candidate.level === "project")
|
|
1454
1933
|
.map((candidate) => ({
|