@skillcdn/console 0.1.11 → 0.1.13
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 +8 -5
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +260 -6
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +7 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +22 -0
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +69 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +2 -2
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.js +2 -2
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/people.js +2 -2
- package/dist/components/people.js.map +1 -1
- 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/runs.js +2 -2
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.js +2 -2
- package/dist/components/shell.js.map +1 -1
- package/dist/components/task-form.js +2 -2
- package/dist/components/task-form.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 +9 -4
- package/dist/components/ui.js.map +1 -1
- package/dist/console.d.ts +41 -6
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +61 -50
- package/dist/console.js.map +1 -1
- package/dist/i18n/en.d.ts +5 -5
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +8 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +8 -8
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +15 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +17 -0
- package/dist/router.js.map +1 -1
- package/docs/specs/cli.md +15 -2
- package/docs/specs/rest.md +13 -13
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +4 -4
- package/skills/working-the-board/SKILL.md +5 -1
- package/src/cli/cli.ts +333 -5
- package/src/cli/format.ts +27 -0
- package/src/cli/help.ts +86 -1
- package/src/components/board.tsx +11 -2
- package/src/components/calendar.tsx +2 -2
- package/src/components/people.tsx +5 -5
- package/src/components/projects.tsx +5 -3
- package/src/components/runs.tsx +2 -2
- package/src/components/shell.tsx +3 -3
- package/src/components/task-form.tsx +2 -2
- package/src/components/ui.tsx +14 -5
- package/src/console.tsx +115 -54
- package/src/i18n/en.ts +9 -8
- package/src/i18n/ko.ts +9 -8
- package/src/index.ts +5 -0
- package/src/router.ts +26 -0
package/src/components/runs.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import type { RestArtifact, RestRun, RunStatus } from "../api.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
4
|
import { Markdown } from "./markdown.js";
|
|
5
|
-
import { Badge, Button, formatBytes, PersonChip, Time } from "./ui.js";
|
|
5
|
+
import { Badge, Button, formatBytes, PersonChip, personName, Time } from "./ui.js";
|
|
6
6
|
|
|
7
7
|
// A run: one agent at work on one task for one person, with what it reported, what it handed
|
|
8
8
|
// in, and what it waits for. Takes its data as props and nothing from the network.
|
|
@@ -49,7 +49,7 @@ export function RunCard(props: RunCardProps) {
|
|
|
49
49
|
const { run } = props;
|
|
50
50
|
const open = run.status === "running" || run.status === "waiting";
|
|
51
51
|
return (
|
|
52
|
-
<article className="sc-run" aria-label={words.label(run.agent, run.person
|
|
52
|
+
<article className="sc-run" aria-label={words.label(run.agent, personName(run.person))}>
|
|
53
53
|
<header className="sc-run-header">
|
|
54
54
|
<span className="sc-run-agent">{run.agent}</span>
|
|
55
55
|
<RunStatusBadge status={run.status} />
|
package/src/components/shell.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ReactNode, useEffect, useRef } from "react";
|
|
2
2
|
import type { RestPerson } from "../api.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
|
-
import { Avatar, cx } from "./ui.js";
|
|
4
|
+
import { Avatar, cx, personName } from "./ui.js";
|
|
5
5
|
|
|
6
6
|
// The frame every page sits in (ADR-0010): a header that names the workspace and the project the
|
|
7
7
|
// page is on, with the person's menu at its end; a row of tabs below it with the pages of the
|
|
@@ -109,9 +109,9 @@ function PersonMenu(props: {
|
|
|
109
109
|
const { person } = props;
|
|
110
110
|
return (
|
|
111
111
|
<details className="sc-menu" ref={details}>
|
|
112
|
-
<summary className="sc-menu-summary" aria-label={words.shell.menu(person
|
|
112
|
+
<summary className="sc-menu-summary" aria-label={words.shell.menu(personName(person))}>
|
|
113
113
|
<Avatar person={person} size="sm" />
|
|
114
|
-
<span className="sc-person-login">{person
|
|
114
|
+
<span className="sc-person-login">{personName(person)}</span>
|
|
115
115
|
<svg className="sc-menu-caret" viewBox="0 0 16 16" aria-hidden="true">
|
|
116
116
|
<path
|
|
117
117
|
d="M4 6l4 4 4-4"
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
TASK_STATES,
|
|
15
15
|
} from "../api.js";
|
|
16
16
|
import { useWords } from "../i18n/index.js";
|
|
17
|
-
import { Button } from "./ui.js";
|
|
17
|
+
import { Button, personName } from "./ui.js";
|
|
18
18
|
|
|
19
19
|
// Writing a task, or changing one. The bounds are the package's, said before a request is made.
|
|
20
20
|
// The plan is two days (ADR-0019); what happened to the task is the board's to keep, not a
|
|
@@ -151,7 +151,7 @@ export function TaskForm(props: TaskFormProps) {
|
|
|
151
151
|
<option value="">{words.common.nobody}</option>
|
|
152
152
|
{props.people.map((person) => (
|
|
153
153
|
<option key={person.id} value={person.id}>
|
|
154
|
-
{person
|
|
154
|
+
{personName(person)}
|
|
155
155
|
</option>
|
|
156
156
|
))}
|
|
157
157
|
</select>
|
package/src/components/ui.tsx
CHANGED
|
@@ -74,13 +74,22 @@ export function Callout(props: {
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** What a person is called on the pages: the name the host gives, or the login where there is none. */
|
|
78
|
+
export function personName(person: {
|
|
79
|
+
readonly login: string;
|
|
80
|
+
readonly name: string | null;
|
|
81
|
+
}): string {
|
|
82
|
+
const name = person.name?.trim() ?? "";
|
|
83
|
+
return name === "" ? person.login : name;
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
/**
|
|
78
87
|
* A person's picture as the git host serves it: loaded lazily, without a referrer, and gone as
|
|
79
|
-
* a whole when there is none or it cannot be loaded, leaving the initial of the
|
|
88
|
+
* a whole when there is none or it cannot be loaded, leaving the initial of the name instead.
|
|
80
89
|
*/
|
|
81
90
|
export function Avatar(props: { readonly person: RestPerson; readonly size?: "sm" | "md" | "lg" }) {
|
|
82
91
|
const size = props.size ?? "md";
|
|
83
|
-
const initial = props.person.
|
|
92
|
+
const initial = personName(props.person).slice(0, 1).toUpperCase();
|
|
84
93
|
if (props.person.avatar === null) {
|
|
85
94
|
return (
|
|
86
95
|
<span
|
|
@@ -103,12 +112,12 @@ export function Avatar(props: { readonly person: RestPerson; readonly size?: "sm
|
|
|
103
112
|
);
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
/** A person, as a picture and
|
|
115
|
+
/** A person, as a picture and their name, wherever one is named; the login is there on hover. */
|
|
107
116
|
export function PersonChip(props: { readonly person: RestPerson; readonly size?: "sm" | "md" }) {
|
|
108
117
|
return (
|
|
109
|
-
<span className="sc-person" title={props.person.
|
|
118
|
+
<span className="sc-person" title={props.person.login}>
|
|
110
119
|
<Avatar person={props.person} size={props.size ?? "sm"} />
|
|
111
|
-
<span className="sc-person-login">{props.person
|
|
120
|
+
<span className="sc-person-login">{personName(props.person)}</span>
|
|
112
121
|
</span>
|
|
113
122
|
);
|
|
114
123
|
}
|
package/src/console.tsx
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ComponentType,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
StrictMode,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
2
10
|
import { createRoot, type Root } from "react-dom/client";
|
|
3
11
|
import {
|
|
4
12
|
type ConsoleClient,
|
|
@@ -50,7 +58,7 @@ import { TaskForm } from "./components/task-form.js";
|
|
|
50
58
|
import { TaskList } from "./components/task-list.js";
|
|
51
59
|
import { TaskView, type TaskViewProps } from "./components/task-view.js";
|
|
52
60
|
import { NewToken, TokenForm, TokenList, type TokenListProps } from "./components/tokens.js";
|
|
53
|
-
import { Button, Callout, EmptyState, Spinner } from "./components/ui.js";
|
|
61
|
+
import { Button, Callout, EmptyState, personName, Spinner } from "./components/ui.js";
|
|
54
62
|
import { WorkList, type WorkListProps } from "./components/work.js";
|
|
55
63
|
import { ConnectPage } from "./connect-page.js";
|
|
56
64
|
import { type ConsoleData, useConsoleData } from "./data.js";
|
|
@@ -61,11 +69,13 @@ import {
|
|
|
61
69
|
DEFAULT_LANGUAGES,
|
|
62
70
|
LanguageContext,
|
|
63
71
|
type LanguagePack,
|
|
72
|
+
type Messages,
|
|
64
73
|
useWords,
|
|
65
74
|
} from "./i18n/index.js";
|
|
66
75
|
import {
|
|
67
76
|
boardHref,
|
|
68
77
|
calendarHref,
|
|
78
|
+
customPageHref,
|
|
69
79
|
decisionHref,
|
|
70
80
|
docHref,
|
|
71
81
|
goalHref,
|
|
@@ -128,6 +138,52 @@ export const DEFAULT_COMPONENTS: ConsoleComponents = {
|
|
|
128
138
|
CategoryList,
|
|
129
139
|
};
|
|
130
140
|
|
|
141
|
+
/**
|
|
142
|
+
* A page a console adds of its own, shown as a tab beside the console's: on the workspace, or
|
|
143
|
+
* on every project. The address is `/pages/<key>`, or `/projects/<project>/pages/<key>`.
|
|
144
|
+
*/
|
|
145
|
+
export interface CustomPage {
|
|
146
|
+
/** What the address says, and what tells the page apart: lowercase letters, digits and hyphens. */
|
|
147
|
+
readonly key: string;
|
|
148
|
+
/** What the tab says: one word for every language, or a word by language tag, `en` standing in for a tag without one. */
|
|
149
|
+
readonly label: string | Readonly<Record<string, string>>;
|
|
150
|
+
/** Where the tab is: among the workspace's pages, or among each project's. */
|
|
151
|
+
readonly level: "workspace" | "project";
|
|
152
|
+
/** Whether the page takes the whole width of the window, as the board does. */
|
|
153
|
+
readonly wide?: boolean | undefined;
|
|
154
|
+
/** What the page shows, given what the default pages are given. */
|
|
155
|
+
readonly render: (context: CustomPageContext) => ReactNode;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** What a page of a console's own is given: the data the default pages read, and the means they use. */
|
|
159
|
+
export interface CustomPageContext {
|
|
160
|
+
/** The workspace and, on a project, the project the page is on, kept current; with the actions. */
|
|
161
|
+
readonly data: ConsoleData;
|
|
162
|
+
/** The project the page is on, or nothing on the workspace. */
|
|
163
|
+
readonly project: RestProject | undefined;
|
|
164
|
+
/** The client, for what the data does not carry. */
|
|
165
|
+
readonly client: ConsoleClient;
|
|
166
|
+
readonly navigation: Navigation;
|
|
167
|
+
/** The words of the language the pages speak, and its tag. */
|
|
168
|
+
readonly words: Messages;
|
|
169
|
+
readonly language: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** What the tab of a page of one's own says in a language: its word for the tag, for the language alone, for English, or the first it has. */
|
|
173
|
+
export function customPageLabel(page: CustomPage, tag: string): string {
|
|
174
|
+
if (typeof page.label === "string") {
|
|
175
|
+
return page.label;
|
|
176
|
+
}
|
|
177
|
+
const language = tag.split("-")[0] ?? tag;
|
|
178
|
+
return (
|
|
179
|
+
page.label[tag] ??
|
|
180
|
+
page.label[language] ??
|
|
181
|
+
page.label.en ??
|
|
182
|
+
Object.values(page.label)[0] ??
|
|
183
|
+
page.key
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
131
187
|
export interface ConsoleConfig {
|
|
132
188
|
/** The origin of the console's API. Left out, the page's own origin. */
|
|
133
189
|
readonly baseUrl?: string | undefined;
|
|
@@ -141,6 +197,8 @@ export interface ConsoleConfig {
|
|
|
141
197
|
readonly brand?: Brand | undefined;
|
|
142
198
|
/** The client to talk to the server with; made from `baseUrl` when left out. For tests. */
|
|
143
199
|
readonly client?: ConsoleClient | undefined;
|
|
200
|
+
/** Pages of the console's own, as tabs beside the default ones. */
|
|
201
|
+
readonly pages?: readonly CustomPage[] | undefined;
|
|
144
202
|
}
|
|
145
203
|
|
|
146
204
|
export interface ConsoleApp {
|
|
@@ -274,13 +332,12 @@ function holdsToken(data: ConsoleData): boolean {
|
|
|
274
332
|
}
|
|
275
333
|
|
|
276
334
|
/**
|
|
277
|
-
* The pages a console that holds a token does not offer: the tokens themselves and
|
|
278
|
-
*
|
|
335
|
+
* The pages a console that holds a token does not offer: the tokens themselves, and the
|
|
336
|
+
* approval of an agent's connection, which makes one; a person's own doing on the console's
|
|
337
|
+
* own pages, signed in. Everything else a person may do, their agent may (ADR-0021).
|
|
279
338
|
*/
|
|
280
339
|
export function onlyOnOwnPages(route: Route): boolean {
|
|
281
340
|
return (
|
|
282
|
-
route.name === "new-project" ||
|
|
283
|
-
route.name === "settings" ||
|
|
284
341
|
route.name === "agents" ||
|
|
285
342
|
route.name === "new-token" ||
|
|
286
343
|
route.name === "person-agents" ||
|
|
@@ -293,28 +350,6 @@ function NotWithToken() {
|
|
|
293
350
|
return <EmptyState title={words.title} body={words.body} />;
|
|
294
351
|
}
|
|
295
352
|
|
|
296
|
-
/**
|
|
297
|
-
* Where a console that holds a token keeps the person's choice of language: the browser's own
|
|
298
|
-
* storage, which may be absent or refuse, and then the browser's languages decide.
|
|
299
|
-
*/
|
|
300
|
-
const LANGUAGE_KEY = "sc-language";
|
|
301
|
-
|
|
302
|
-
function storedLanguage(): string | undefined {
|
|
303
|
-
try {
|
|
304
|
-
return globalThis.localStorage?.getItem(LANGUAGE_KEY) ?? undefined;
|
|
305
|
-
} catch {
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function storeLanguage(tag: string): void {
|
|
311
|
-
try {
|
|
312
|
-
globalThis.localStorage?.setItem(LANGUAGE_KEY, tag);
|
|
313
|
-
} catch {
|
|
314
|
-
// A browser that keeps nothing: the choice holds for this page.
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
353
|
/** A link that reads as a button: where a form is, for instance. */
|
|
319
354
|
function LinkButton(props: {
|
|
320
355
|
readonly href: string;
|
|
@@ -339,12 +374,11 @@ function ProjectsPage(props: {
|
|
|
339
374
|
const { data, components, navigation, making } = props;
|
|
340
375
|
const { busy, error, act } = useAction();
|
|
341
376
|
const href = (project: RestProject) => projectHref(project.key);
|
|
342
|
-
const configures = !holdsToken(data);
|
|
343
377
|
return (
|
|
344
378
|
<>
|
|
345
379
|
<div className="sc-page-head">
|
|
346
380
|
<h1 className="sc-page-title">{words.title}</h1>
|
|
347
|
-
{!making &&
|
|
381
|
+
{!making && (
|
|
348
382
|
<LinkButton href={PATHS.newProject} variant="primary">
|
|
349
383
|
{words.newProject}
|
|
350
384
|
</LinkButton>
|
|
@@ -377,11 +411,7 @@ function ProjectsPage(props: {
|
|
|
377
411
|
<EmptyState
|
|
378
412
|
title={words.empty.title}
|
|
379
413
|
body={words.empty.body}
|
|
380
|
-
action={
|
|
381
|
-
configures ? (
|
|
382
|
-
<LinkButton href={PATHS.newProject}>{words.newProject}</LinkButton>
|
|
383
|
-
) : undefined
|
|
384
|
-
}
|
|
414
|
+
action={<LinkButton href={PATHS.newProject}>{words.newProject}</LinkButton>}
|
|
385
415
|
/>
|
|
386
416
|
}
|
|
387
417
|
/>
|
|
@@ -908,7 +938,7 @@ function ProjectPage(props: {
|
|
|
908
938
|
}
|
|
909
939
|
if (route.name === "members") {
|
|
910
940
|
const me = data.me?.person ?? undefined;
|
|
911
|
-
const configures = owner
|
|
941
|
+
const configures = owner;
|
|
912
942
|
const members = words.projects.members;
|
|
913
943
|
return (
|
|
914
944
|
<>
|
|
@@ -1047,7 +1077,9 @@ function PersonAgentsPage(props: {
|
|
|
1047
1077
|
return (
|
|
1048
1078
|
<>
|
|
1049
1079
|
<div className="sc-page-head">
|
|
1050
|
-
<h1 className="sc-page-title">
|
|
1080
|
+
<h1 className="sc-page-title">
|
|
1081
|
+
{words.people.agentsOf(person === undefined ? "?" : personName(person))}
|
|
1082
|
+
</h1>
|
|
1051
1083
|
<LinkButton href={PATHS.people}>{words.nav.people}</LinkButton>
|
|
1052
1084
|
</div>
|
|
1053
1085
|
<p className="sc-lead">
|
|
@@ -1140,7 +1172,7 @@ function WorkspacePage(props: {
|
|
|
1140
1172
|
}
|
|
1141
1173
|
if (route.name === "categories") {
|
|
1142
1174
|
const me = data.me?.person ?? undefined;
|
|
1143
|
-
const administrator = me?.role === "admin"
|
|
1175
|
+
const administrator = me?.role === "admin";
|
|
1144
1176
|
const categories = words.categories;
|
|
1145
1177
|
return (
|
|
1146
1178
|
<>
|
|
@@ -1179,7 +1211,7 @@ function WorkspacePage(props: {
|
|
|
1179
1211
|
|
|
1180
1212
|
if (route.name === "people") {
|
|
1181
1213
|
const me = data.me?.person ?? undefined;
|
|
1182
|
-
const administrator = me?.role === "admin"
|
|
1214
|
+
const administrator = me?.role === "admin";
|
|
1183
1215
|
return (
|
|
1184
1216
|
<>
|
|
1185
1217
|
<div className="sc-page-head">
|
|
@@ -1259,6 +1291,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1259
1291
|
const client = config.client ?? createClient({ baseUrl: config.baseUrl ?? "" });
|
|
1260
1292
|
const fallbackTitle = config.title ?? "Console";
|
|
1261
1293
|
const languages = config.languages ?? DEFAULT_LANGUAGES;
|
|
1294
|
+
const pages = config.pages ?? [];
|
|
1262
1295
|
|
|
1263
1296
|
function App(props: { readonly initialPath?: string | undefined }) {
|
|
1264
1297
|
const { location, navigation } = useNavigation(props.initialPath);
|
|
@@ -1273,10 +1306,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1273
1306
|
const title = data.me?.workspace.name ?? fallbackTitle;
|
|
1274
1307
|
const viaToken = holdsToken(data);
|
|
1275
1308
|
// The language: the person's choice, else the browser's, else the first the console speaks.
|
|
1276
|
-
|
|
1277
|
-
// browser instead.
|
|
1278
|
-
const [ownChoice, setOwnChoice] = useState<string | undefined>(storedLanguage);
|
|
1279
|
-
const chosen = viaToken ? (ownChoice ?? data.me?.language) : data.me?.language;
|
|
1309
|
+
const chosen = data.me?.language;
|
|
1280
1310
|
const pack = useMemo(
|
|
1281
1311
|
() =>
|
|
1282
1312
|
chooseLanguage(
|
|
@@ -1363,6 +1393,16 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1363
1393
|
label: words.nav.categories,
|
|
1364
1394
|
current: route.name === "categories",
|
|
1365
1395
|
},
|
|
1396
|
+
...pages
|
|
1397
|
+
.filter((candidate) => candidate.level === "workspace")
|
|
1398
|
+
.map((candidate) => ({
|
|
1399
|
+
href: customPageHref(candidate.key),
|
|
1400
|
+
label: customPageLabel(candidate, pack.tag),
|
|
1401
|
+
current:
|
|
1402
|
+
route.name === "custom" &&
|
|
1403
|
+
route.level === "workspace" &&
|
|
1404
|
+
route.page === candidate.key,
|
|
1405
|
+
})),
|
|
1366
1406
|
];
|
|
1367
1407
|
const nav: NavItem[] =
|
|
1368
1408
|
projectKey === undefined
|
|
@@ -1414,7 +1454,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1414
1454
|
label: words.nav.members,
|
|
1415
1455
|
current: route.name === "members",
|
|
1416
1456
|
},
|
|
1417
|
-
...(project?.role === "owner"
|
|
1457
|
+
...(project?.role === "owner"
|
|
1418
1458
|
? [
|
|
1419
1459
|
{
|
|
1420
1460
|
href: projectHref(projectKey, "settings"),
|
|
@@ -1423,8 +1463,27 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1423
1463
|
},
|
|
1424
1464
|
]
|
|
1425
1465
|
: []),
|
|
1466
|
+
...pages
|
|
1467
|
+
.filter((candidate) => candidate.level === "project")
|
|
1468
|
+
.map((candidate) => ({
|
|
1469
|
+
href: customPageHref(candidate.key, projectKey),
|
|
1470
|
+
label: customPageLabel(candidate, pack.tag),
|
|
1471
|
+
current:
|
|
1472
|
+
route.name === "custom" &&
|
|
1473
|
+
route.level === "project" &&
|
|
1474
|
+
route.page === candidate.key,
|
|
1475
|
+
})),
|
|
1426
1476
|
];
|
|
1427
|
-
|
|
1477
|
+
/** A page of the console's own at the address, or nothing there. */
|
|
1478
|
+
const customPageOf = (key: string, level: CustomPage["level"]): ReactNode => {
|
|
1479
|
+
const found = pages.find((candidate) => candidate.key === key && candidate.level === level);
|
|
1480
|
+
return found === undefined ? (
|
|
1481
|
+
<NothingHere />
|
|
1482
|
+
) : (
|
|
1483
|
+
found.render({ data, project, client, navigation, words, language: pack.tag })
|
|
1484
|
+
);
|
|
1485
|
+
};
|
|
1486
|
+
let page: ReactNode;
|
|
1428
1487
|
if (!data.loaded || route.name === "moved") {
|
|
1429
1488
|
page = (
|
|
1430
1489
|
<div className="sc-loading">
|
|
@@ -1442,6 +1501,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1442
1501
|
making={route.name === "new-project"}
|
|
1443
1502
|
/>
|
|
1444
1503
|
);
|
|
1504
|
+
} else if (route.name === "custom" && route.level === "workspace") {
|
|
1505
|
+
page = customPageOf(route.page, "workspace");
|
|
1445
1506
|
} else if (projectKey === undefined) {
|
|
1446
1507
|
page = (
|
|
1447
1508
|
<WorkspacePage
|
|
@@ -1465,6 +1526,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1465
1526
|
<Spinner label={words.common.loadingProject} />
|
|
1466
1527
|
</div>
|
|
1467
1528
|
);
|
|
1529
|
+
} else if (route.name === "custom") {
|
|
1530
|
+
page = customPageOf(route.page, "project");
|
|
1468
1531
|
} else {
|
|
1469
1532
|
page = (
|
|
1470
1533
|
<ProjectPage
|
|
@@ -1497,17 +1560,15 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
1497
1560
|
label: candidate.label,
|
|
1498
1561
|
current: candidate.tag === pack.tag,
|
|
1499
1562
|
}))}
|
|
1500
|
-
onLanguage={(tag) => {
|
|
1501
|
-
if (viaToken) {
|
|
1502
|
-
storeLanguage(tag);
|
|
1503
|
-
setOwnChoice(tag);
|
|
1504
|
-
} else {
|
|
1505
|
-
void data.actions.updateMe({ language: tag }).catch(() => undefined);
|
|
1506
|
-
}
|
|
1507
|
-
}}
|
|
1563
|
+
onLanguage={(tag) => void data.actions.updateMe({ language: tag }).catch(() => undefined)}
|
|
1508
1564
|
person={data.me.person}
|
|
1509
1565
|
live={projectKey === undefined ? undefined : data.live}
|
|
1510
|
-
wide={
|
|
1566
|
+
wide={
|
|
1567
|
+
route.name === "board" ||
|
|
1568
|
+
route.name === "calendar" ||
|
|
1569
|
+
(route.name === "custom" &&
|
|
1570
|
+
pages.some((candidate) => candidate.key === route.page && candidate.wide === true))
|
|
1571
|
+
}
|
|
1511
1572
|
onNavigate={(href) => navigation.go(href)}
|
|
1512
1573
|
onSignOut={viaToken ? undefined : () => void data.actions.signOut()}
|
|
1513
1574
|
>
|
package/src/i18n/en.ts
CHANGED
|
@@ -89,7 +89,7 @@ export const en = {
|
|
|
89
89
|
},
|
|
90
90
|
notWithToken: {
|
|
91
91
|
title: "Done on the console's own pages",
|
|
92
|
-
body: "This console holds a token, which acts as you on the board
|
|
92
|
+
body: "This console holds a token, which acts as you on the board, configuring included. Agents are connected, and tokens made and removed, on the organization's console, signed in.",
|
|
93
93
|
},
|
|
94
94
|
you: "(you)",
|
|
95
95
|
nobody: "Nobody",
|
|
@@ -122,7 +122,7 @@ export const en = {
|
|
|
122
122
|
live: "Live",
|
|
123
123
|
reconnecting: "Reconnecting",
|
|
124
124
|
signOut: "Sign out",
|
|
125
|
-
menu: (
|
|
125
|
+
menu: (name: string) => `${name}: menu`,
|
|
126
126
|
},
|
|
127
127
|
|
|
128
128
|
signIn: {
|
|
@@ -178,7 +178,7 @@ export const en = {
|
|
|
178
178
|
leadRoles:
|
|
179
179
|
"An owner configures the project; a member works on it. A workspace administrator is an owner of every project.",
|
|
180
180
|
since: "since",
|
|
181
|
-
roleOf: (
|
|
181
|
+
roleOf: (name: string) => `Role of ${name}`,
|
|
182
182
|
addAria: "Add a member",
|
|
183
183
|
person: "Person",
|
|
184
184
|
chooseSomeone: "Choose someone",
|
|
@@ -533,7 +533,7 @@ export const en = {
|
|
|
533
533
|
answerIt: "answer it",
|
|
534
534
|
reports: "Reports",
|
|
535
535
|
handedIn: "Handed in",
|
|
536
|
-
label: (agent: string,
|
|
536
|
+
label: (agent: string, name: string) => `${agent} for ${name}`,
|
|
537
537
|
},
|
|
538
538
|
|
|
539
539
|
skills: {
|
|
@@ -625,9 +625,9 @@ export const en = {
|
|
|
625
625
|
title: "People",
|
|
626
626
|
list: "People",
|
|
627
627
|
lead: "Everyone who has signed in. An administrator configures the workspace and says what each person is; a member works in the projects they are in. What a person may do, their agents may do.",
|
|
628
|
-
roleOf: (
|
|
628
|
+
roleOf: (name: string) => `Role of ${name}`,
|
|
629
629
|
agents: "Agents",
|
|
630
|
-
agentsOf: (
|
|
630
|
+
agentsOf: (name: string) => `Agents of ${name}`,
|
|
631
631
|
agentsLead: (name: string) =>
|
|
632
632
|
`What works here as ${name}, each with a token of its own. Disconnecting one takes its token away at once.`,
|
|
633
633
|
loadingAgents: "Loading the agents",
|
|
@@ -684,7 +684,7 @@ export const en = {
|
|
|
684
684
|
lookingUp: "Looking the code up",
|
|
685
685
|
notACode: "That is not a code: eight letters and digits, in two groups.",
|
|
686
686
|
noSuchCode:
|
|
687
|
-
"No agent asks to connect with this code. A code is good for
|
|
687
|
+
"No agent asks to connect with this code. A code is good for thirty minutes and for one approval; ask your agent for a new one.",
|
|
688
688
|
couldNotAsk: "The console could not be asked.",
|
|
689
689
|
asks: {
|
|
690
690
|
title: "An agent asks to connect as you",
|
|
@@ -711,7 +711,8 @@ export const en = {
|
|
|
711
711
|
words: {
|
|
712
712
|
title: "Connecting an agent",
|
|
713
713
|
tellYourAgent: "Tell your agent, in its own chat:",
|
|
714
|
-
quote: (origin: string) =>
|
|
714
|
+
quote: (origin: string) =>
|
|
715
|
+
`Install @skillcdn/console from npm, run console login --url ${origin}, and tell me the address and the code it shows.`,
|
|
715
716
|
doesTheRest:
|
|
716
717
|
"An agent that can run commands does the rest: it installs the command, connects, and shows you an address with a code.",
|
|
717
718
|
thenApprove:
|
package/src/i18n/ko.ts
CHANGED
|
@@ -62,7 +62,7 @@ export const ko: Messages = {
|
|
|
62
62
|
},
|
|
63
63
|
notWithToken: {
|
|
64
64
|
title: "콘솔 자체 페이지에서 하는 일입니다",
|
|
65
|
-
body: "이 콘솔은 토큰으로 당신을 대신해 보드를
|
|
65
|
+
body: "이 콘솔은 토큰으로 당신을 대신해 보드를 쓰며, 설정도 그렇게 합니다. 에이전트 연결과 토큰 관리는 조직 콘솔에 로그인해서 합니다.",
|
|
66
66
|
},
|
|
67
67
|
you: "(나)",
|
|
68
68
|
nobody: "없음",
|
|
@@ -95,7 +95,7 @@ export const ko: Messages = {
|
|
|
95
95
|
live: "실시간",
|
|
96
96
|
reconnecting: "다시 연결하는 중",
|
|
97
97
|
signOut: "로그아웃",
|
|
98
|
-
menu: (
|
|
98
|
+
menu: (name) => `${name}: 메뉴`,
|
|
99
99
|
},
|
|
100
100
|
|
|
101
101
|
signIn: {
|
|
@@ -150,7 +150,7 @@ export const ko: Messages = {
|
|
|
150
150
|
leadRoles:
|
|
151
151
|
"소유자는 프로젝트를 설정하고, 멤버는 프로젝트에서 일합니다. 워크스페이스 관리자는 모든 프로젝트의 소유자입니다.",
|
|
152
152
|
since: "부터",
|
|
153
|
-
roleOf: (
|
|
153
|
+
roleOf: (name) => `${name}의 역할`,
|
|
154
154
|
addAria: "멤버 추가",
|
|
155
155
|
person: "사람",
|
|
156
156
|
chooseSomeone: "선택하세요",
|
|
@@ -501,7 +501,7 @@ export const ko: Messages = {
|
|
|
501
501
|
answerIt: "답변하기",
|
|
502
502
|
reports: "보고",
|
|
503
503
|
handedIn: "제출물",
|
|
504
|
-
label: (agent,
|
|
504
|
+
label: (agent, name) => `${name}을(를) 위한 ${agent}`,
|
|
505
505
|
},
|
|
506
506
|
|
|
507
507
|
skills: {
|
|
@@ -593,9 +593,9 @@ export const ko: Messages = {
|
|
|
593
593
|
title: "사람들",
|
|
594
594
|
list: "사람들",
|
|
595
595
|
lead: "로그인한 모든 사람. 관리자는 워크스페이스를 설정하고 각 사람의 역할을 정합니다. 멤버는 속한 프로젝트에서 일합니다. 사람이 할 수 있는 일은 그 사람의 에이전트도 할 수 있습니다.",
|
|
596
|
-
roleOf: (
|
|
596
|
+
roleOf: (name) => `${name}의 역할`,
|
|
597
597
|
agents: "에이전트",
|
|
598
|
-
agentsOf: (
|
|
598
|
+
agentsOf: (name) => `${name}의 에이전트`,
|
|
599
599
|
agentsLead: (name) =>
|
|
600
600
|
`${name}(으)로 여기서 일하는 것들입니다. 각각 고유한 토큰을 가집니다. 연결을 끊으면 토큰이 즉시 사라집니다.`,
|
|
601
601
|
loadingAgents: "에이전트를 불러오는 중",
|
|
@@ -652,7 +652,7 @@ export const ko: Messages = {
|
|
|
652
652
|
lookingUp: "코드를 찾는 중",
|
|
653
653
|
notACode: "코드가 아닙니다. 영문자와 숫자 여덟 자, 두 묶음입니다.",
|
|
654
654
|
noSuchCode:
|
|
655
|
-
"이 코드로 연결을 요청한 에이전트가 없습니다. 코드는
|
|
655
|
+
"이 코드로 연결을 요청한 에이전트가 없습니다. 코드는 30분 동안, 한 번의 승인에만 유효합니다. 에이전트에게 새 코드를 요청하세요.",
|
|
656
656
|
couldNotAsk: "콘솔에 물어볼 수 없습니다.",
|
|
657
657
|
asks: {
|
|
658
658
|
title: "에이전트가 나로서 연결하기를 요청합니다",
|
|
@@ -678,7 +678,8 @@ export const ko: Messages = {
|
|
|
678
678
|
words: {
|
|
679
679
|
title: "에이전트 연결하기",
|
|
680
680
|
tellYourAgent: "에이전트의 대화창에서 이렇게 말하세요:",
|
|
681
|
-
quote: (origin) =>
|
|
681
|
+
quote: (origin) =>
|
|
682
|
+
`npm으로 @skillcdn/console을 설치하고, console login --url ${origin} 을 실행한 뒤 나오는 주소와 코드를 알려줘.`,
|
|
682
683
|
doesTheRest:
|
|
683
684
|
"명령을 실행할 수 있는 에이전트는 나머지를 알아서 합니다. 명령을 설치하고, 연결하고, 코드가 있는 주소를 보여 줍니다.",
|
|
684
685
|
thenApprove:
|
package/src/index.ts
CHANGED
|
@@ -127,6 +127,7 @@ export {
|
|
|
127
127
|
PRIORITY_LABELS,
|
|
128
128
|
PriorityBadge,
|
|
129
129
|
ProgressBar,
|
|
130
|
+
personName,
|
|
130
131
|
ROLE_LABELS,
|
|
131
132
|
RoleBadge,
|
|
132
133
|
Spinner,
|
|
@@ -140,7 +141,10 @@ export {
|
|
|
140
141
|
type ConsoleApp,
|
|
141
142
|
type ConsoleComponents,
|
|
142
143
|
type ConsoleConfig,
|
|
144
|
+
type CustomPage,
|
|
145
|
+
type CustomPageContext,
|
|
143
146
|
createConsole,
|
|
147
|
+
customPageLabel,
|
|
144
148
|
DEFAULT_COMPONENTS,
|
|
145
149
|
} from "./console.js";
|
|
146
150
|
export { type ConsoleActions, type ConsoleData, useConsoleData } from "./data.js";
|
|
@@ -180,6 +184,7 @@ export {
|
|
|
180
184
|
boardHref,
|
|
181
185
|
calendarHref,
|
|
182
186
|
connectHref,
|
|
187
|
+
customPageHref,
|
|
183
188
|
type DocsView,
|
|
184
189
|
decisionHref,
|
|
185
190
|
docHref,
|
package/src/router.ts
CHANGED
|
@@ -84,6 +84,14 @@ export type Route =
|
|
|
84
84
|
| { readonly name: "connect"; readonly code: string | undefined }
|
|
85
85
|
/** An address that moved: the page takes the person to `to` in its place. */
|
|
86
86
|
| { readonly name: "moved"; readonly to: string }
|
|
87
|
+
/** A page a console adds of its own (`createConsole({ pages })`): on the workspace, or on a project. */
|
|
88
|
+
| { readonly name: "custom"; readonly level: "workspace"; readonly page: string }
|
|
89
|
+
| {
|
|
90
|
+
readonly name: "custom";
|
|
91
|
+
readonly level: "project";
|
|
92
|
+
readonly project: string;
|
|
93
|
+
readonly page: string;
|
|
94
|
+
}
|
|
87
95
|
| { readonly name: "not-found" };
|
|
88
96
|
|
|
89
97
|
/** The pages of a project, after its key. */
|
|
@@ -112,6 +120,8 @@ export const PATHS = {
|
|
|
112
120
|
newToken: "/agents/new",
|
|
113
121
|
/** The categories tasks are sorted into, kept by an administrator. */
|
|
114
122
|
categories: "/categories",
|
|
123
|
+
/** Where the pages a console adds of its own are: `/pages/<key>`, and `/projects/<key>/pages/<page>` on a project. */
|
|
124
|
+
pages: "/pages",
|
|
115
125
|
/** Where an agent's connection is approved (ADR-0011): `/connect/<code>`, or the code typed. */
|
|
116
126
|
connect: "/connect",
|
|
117
127
|
} as const;
|
|
@@ -223,6 +233,12 @@ export function matchRoute(pathname: string, search = ""): Route {
|
|
|
223
233
|
? { name: "person-agents", id }
|
|
224
234
|
: NOT_FOUND;
|
|
225
235
|
}
|
|
236
|
+
if (path.startsWith(`${PATHS.pages}/`)) {
|
|
237
|
+
const [page, ...more] = path.slice(PATHS.pages.length + 1).split("/");
|
|
238
|
+
return page !== undefined && PROJECT_KEY.test(page) && more.length === 0
|
|
239
|
+
? { name: "custom", level: "workspace", page }
|
|
240
|
+
: NOT_FOUND;
|
|
241
|
+
}
|
|
226
242
|
if (!path.startsWith(`${PATHS.project}/`)) {
|
|
227
243
|
return NOT_FOUND;
|
|
228
244
|
}
|
|
@@ -242,6 +258,11 @@ export function matchRoute(pathname: string, search = ""): Route {
|
|
|
242
258
|
const view = docsViewOf(params, docPath);
|
|
243
259
|
return view === undefined ? NOT_FOUND : { name: "docs", project, path: docPath, view };
|
|
244
260
|
}
|
|
261
|
+
if (page === "pages") {
|
|
262
|
+
return third !== undefined && PROJECT_KEY.test(third) && rest.length === 0
|
|
263
|
+
? { name: "custom", level: "project", project, page: third }
|
|
264
|
+
: NOT_FOUND;
|
|
265
|
+
}
|
|
245
266
|
if (rest.length > 0) {
|
|
246
267
|
return NOT_FOUND;
|
|
247
268
|
}
|
|
@@ -363,6 +384,11 @@ export function calendarHref(key: string, month?: string, filter: BoardFilter =
|
|
|
363
384
|
return parts.length === 0 ? base : `${base}?${parts.join("&")}`;
|
|
364
385
|
}
|
|
365
386
|
|
|
387
|
+
/** Where a page a console adds of its own is: on the workspace, or on a project when its key is given. */
|
|
388
|
+
export function customPageHref(page: string, project?: string): string {
|
|
389
|
+
return project === undefined ? `${PATHS.pages}/${page}` : `${projectHref(project)}/pages/${page}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
366
392
|
/** Where a goal is set: the form. */
|
|
367
393
|
export function newGoalHref(key: string): string {
|
|
368
394
|
return `${PATHS.project}/${key}/roadmap/new`;
|