@skillcdn/console 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/SKILLCDN.md +2 -0
- package/dist/agent-name.d.ts +3 -0
- package/dist/agent-name.d.ts.map +1 -0
- package/dist/agent-name.js +9 -0
- package/dist/agent-name.js.map +1 -0
- package/dist/api.d.ts +6 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +215 -22
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +14 -4
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +59 -10
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +27 -11
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +5 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +20 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/client.js.map +1 -1
- package/dist/components/calendar.d.ts +15 -0
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +78 -41
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/connect.d.ts +4 -1
- package/dist/components/connect.d.ts.map +1 -1
- package/dist/components/connect.js +7 -3
- package/dist/components/connect.js.map +1 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +3 -3
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/documents.d.ts +5 -0
- package/dist/components/documents.d.ts.map +1 -1
- package/dist/components/documents.js +3 -2
- package/dist/components/documents.js.map +1 -1
- package/dist/components/event-feed.d.ts +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +10 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/people.d.ts +24 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +35 -3
- package/dist/components/people.js.map +1 -1
- package/dist/components/projects.d.ts +6 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -3
- package/dist/components/projects.js.map +1 -1
- package/dist/components/rules.d.ts +40 -0
- package/dist/components/rules.d.ts.map +1 -0
- package/dist/components/rules.js +46 -0
- package/dist/components/rules.js.map +1 -0
- package/dist/components/runs.d.ts +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +2 -1
- package/dist/components/runs.js.map +1 -1
- package/dist/components/skills.d.ts.map +1 -1
- package/dist/components/skills.js +5 -1
- package/dist/components/skills.js.map +1 -1
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +2 -9
- package/dist/components/tokens.js.map +1 -1
- package/dist/components/ui.d.ts +7 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +13 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +9 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +8 -3
- package/dist/components/work.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +1 -1
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +253 -13
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +203 -49
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +19 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -1
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +4 -0
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +26 -0
- package/dist/dates.js.map +1 -1
- package/dist/i18n/en.d.ts +90 -28
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +89 -31
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +89 -31
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +4 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +4 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +34 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +55 -7
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +14 -4
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +17 -2
- package/dist/routes.js.map +1 -1
- package/dist/rules.d.ts +28 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +102 -0
- package/dist/rules.js.map +1 -0
- package/dist/schemas.d.ts +652 -4
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +96 -6
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +21 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +21 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +13 -9
- package/docs/specs/rest.md +32 -8
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +1 -1
- package/skills/deploying-a-console/SKILL.md +5 -5
- package/skills/deploying-a-console/assets/secrets.example.json +2 -1
- package/skills/deploying-a-console/references/access.md +10 -6
- package/skills/deploying-a-console/references/resources.md +10 -7
- package/skills/working-the-board/SKILL.md +10 -9
- package/skills/working-the-board/references/project-rules.md +36 -0
- package/src/agent-name.ts +9 -0
- package/src/api.ts +37 -0
- package/src/cli/cli.ts +253 -24
- package/src/cli/format.ts +74 -10
- package/src/cli/help.ts +30 -10
- package/src/cli/main.ts +5 -0
- package/src/client.ts +47 -1
- package/src/components/calendar.tsx +242 -101
- package/src/components/connect.tsx +12 -6
- package/src/components/decision-list.tsx +2 -2
- package/src/components/documents.tsx +7 -3
- package/src/components/event-feed.tsx +16 -3
- package/src/components/people.tsx +244 -35
- package/src/components/projects.tsx +31 -8
- package/src/components/rules.tsx +167 -0
- package/src/components/runs.tsx +4 -2
- package/src/components/skills.tsx +22 -11
- package/src/components/tokens.tsx +2 -14
- package/src/components/ui.tsx +25 -2
- package/src/components/work.tsx +69 -43
- package/src/connect-page.tsx +0 -1
- package/src/console.tsx +531 -67
- package/src/data.ts +52 -2
- package/src/dates.ts +28 -0
- package/src/i18n/en.ts +99 -38
- package/src/i18n/ko.ts +95 -38
- package/src/index.ts +21 -1
- package/src/limits.ts +4 -0
- package/src/router.ts +77 -12
- package/src/routes.ts +23 -3
- package/src/rules.ts +109 -0
- package/src/schemas.ts +121 -4
- package/src/styles/console.css +253 -13
- package/src/vocabulary.ts +30 -0
package/src/client.ts
CHANGED
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
categoryPath,
|
|
5
5
|
connectPath,
|
|
6
6
|
personTokensPath,
|
|
7
|
+
personWorkPath,
|
|
7
8
|
projectPath,
|
|
8
9
|
REST_ROUTES,
|
|
9
10
|
restPath,
|
|
11
|
+
rulesPath,
|
|
10
12
|
} from "./routes.js";
|
|
11
13
|
import {
|
|
12
14
|
type RestAnswerInput,
|
|
@@ -31,6 +33,8 @@ import {
|
|
|
31
33
|
type RestGoalInput,
|
|
32
34
|
type RestGoalPatch,
|
|
33
35
|
type RestGoals,
|
|
36
|
+
type RestListing,
|
|
37
|
+
type RestListingInput,
|
|
34
38
|
type RestMe,
|
|
35
39
|
type RestMember,
|
|
36
40
|
type RestMemberInput,
|
|
@@ -40,11 +44,15 @@ import {
|
|
|
40
44
|
type RestPeople,
|
|
41
45
|
type RestPerson,
|
|
42
46
|
type RestPersonPatch,
|
|
47
|
+
type RestPersonWork,
|
|
43
48
|
type RestProject,
|
|
44
49
|
type RestProjectInput,
|
|
45
50
|
type RestProjectPatch,
|
|
51
|
+
type RestProjectRules,
|
|
46
52
|
type RestProjects,
|
|
47
53
|
type RestReportInput,
|
|
54
|
+
type RestRules,
|
|
55
|
+
type RestRulesInput,
|
|
48
56
|
type RestRun,
|
|
49
57
|
type RestRunEndInput,
|
|
50
58
|
type RestRunInput,
|
|
@@ -73,13 +81,17 @@ import {
|
|
|
73
81
|
restEventsSchema,
|
|
74
82
|
restGoalSchema,
|
|
75
83
|
restGoalsSchema,
|
|
84
|
+
restListingSchema,
|
|
76
85
|
restMemberSchema,
|
|
77
86
|
restMembersSchema,
|
|
78
87
|
restMeSchema,
|
|
79
88
|
restPeopleSchema,
|
|
80
89
|
restPersonSchema,
|
|
90
|
+
restPersonWorkSchema,
|
|
91
|
+
restProjectRulesSchema,
|
|
81
92
|
restProjectSchema,
|
|
82
93
|
restProjectsSchema,
|
|
94
|
+
restRulesSchema,
|
|
83
95
|
restRunSchema,
|
|
84
96
|
restRunsSchema,
|
|
85
97
|
restSkillsSchema,
|
|
@@ -243,6 +255,8 @@ export interface ProjectClient {
|
|
|
243
255
|
eventStreamUrl(after: number): string;
|
|
244
256
|
/** The project's skills, as SkillCDN serves them at its address, or the organization's. */
|
|
245
257
|
skills(signal?: AbortSignal): Promise<RestSkills>;
|
|
258
|
+
/** The rules that hold in the project (ADR-0023): the workspace's, and what the project adds. */
|
|
259
|
+
rules(signal?: AbortSignal): Promise<RestProjectRules>;
|
|
246
260
|
/** A folder's pages and folders, or the pages a search finds. */
|
|
247
261
|
documents(filter?: DocumentFilter, signal?: AbortSignal): Promise<RestDocuments>;
|
|
248
262
|
/** One document, by its path: its latest version, its links both ways, its files. */
|
|
@@ -271,11 +285,17 @@ export interface ProjectClient {
|
|
|
271
285
|
|
|
272
286
|
export interface ConsoleClient {
|
|
273
287
|
me(signal?: AbortSignal): Promise<RestMe>;
|
|
288
|
+
/** Gives up the token this client acts with, so that it is good for nothing from now on: what `console logout` does. For a token; a session has none. */
|
|
289
|
+
giveUpToken(): Promise<void>;
|
|
274
290
|
/** Keeps the person's choice of language, or none; for a person signed in. */
|
|
275
291
|
updateMe(patch: RestMePatch): Promise<RestMe>;
|
|
276
292
|
people(signal?: AbortSignal): Promise<RestPeople>;
|
|
277
|
-
/** Changes what a person is; for an administrator. */
|
|
293
|
+
/** Changes what a person is, or whether they are in; for an administrator. */
|
|
278
294
|
updatePerson(id: string, patch: RestPersonPatch): Promise<RestPerson>;
|
|
295
|
+
/** Lets a login in (ADR-0022): whoever signs in with that account becomes a person; for an administrator. */
|
|
296
|
+
createListing(input: RestListingInput): Promise<RestListing>;
|
|
297
|
+
/** Takes a listing away before anyone signed in with it; for an administrator. */
|
|
298
|
+
removeListing(id: string): Promise<void>;
|
|
279
299
|
/** The projects the asker may see, with what they are in each. */
|
|
280
300
|
projects(signal?: AbortSignal): Promise<RestProjects>;
|
|
281
301
|
/** Makes a project, whose owner the asker becomes. */
|
|
@@ -286,6 +306,14 @@ export interface ConsoleClient {
|
|
|
286
306
|
events(after: number, signal?: AbortSignal): Promise<RestEvents>;
|
|
287
307
|
/** The categories tasks are sorted into (ADR-0019), by name. */
|
|
288
308
|
categories(signal?: AbortSignal): Promise<RestCategories>;
|
|
309
|
+
/** The workspace's rules (ADR-0023): the console's own until an administrator writes the organization's. */
|
|
310
|
+
rules(signal?: AbortSignal): Promise<RestRules>;
|
|
311
|
+
/** Writes the organization's rules; for an administrator. The console's own, sent as they are, write nothing. */
|
|
312
|
+
writeRules(input: RestRulesInput): Promise<RestRules>;
|
|
313
|
+
/** Puts the organization's rules away, so that the console's own hold again; for an administrator. */
|
|
314
|
+
archiveRules(): Promise<RestRules>;
|
|
315
|
+
/** The versions of the organization's rules, newest first; none while the console's own hold. */
|
|
316
|
+
rulesVersions(signal?: AbortSignal): Promise<RestVersions>;
|
|
289
317
|
/** Makes a category; for an administrator signed in. */
|
|
290
318
|
createCategory(input: RestCategoryInput): Promise<RestCategory>;
|
|
291
319
|
updateCategory(key: string, patch: RestCategoryPatch): Promise<RestCategory>;
|
|
@@ -293,6 +321,8 @@ export interface ConsoleClient {
|
|
|
293
321
|
removeCategory(key: string): Promise<void>;
|
|
294
322
|
/** What is on the asker's plate across the projects they may see: their open tasks, and the decisions waiting. */
|
|
295
323
|
work(signal?: AbortSignal): Promise<RestWork>;
|
|
324
|
+
/** Another person's plate, within what the asker may see: for an administrator, or an owner of a project they are in. */
|
|
325
|
+
personWork(personId: string, signal?: AbortSignal): Promise<RestPersonWork>;
|
|
296
326
|
/** The tokens of whoever asks. */
|
|
297
327
|
tokens(signal?: AbortSignal): Promise<RestTokens>;
|
|
298
328
|
/** The tokens of another person: for an administrator. */
|
|
@@ -520,6 +550,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
520
550
|
eventStreamUrl: (after) => withQuery(`${at("events")}/stream`, { after: String(after) }),
|
|
521
551
|
skills: (signal) =>
|
|
522
552
|
request("GET", `${base}${at("skills")}`, undefined, restSkillsSchema, signal),
|
|
553
|
+
rules: (signal) =>
|
|
554
|
+
request("GET", `${base}${at("rules")}`, undefined, restProjectRulesSchema, signal),
|
|
523
555
|
documents: (filter = {}, signal) =>
|
|
524
556
|
request(
|
|
525
557
|
"GET",
|
|
@@ -572,10 +604,15 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
572
604
|
|
|
573
605
|
return {
|
|
574
606
|
me: (signal) => request("GET", `${base}${REST_ROUTES.me}`, undefined, restMeSchema, signal),
|
|
607
|
+
giveUpToken: () => request("DELETE", `${base}${REST_ROUTES.meToken}`, undefined, undefined),
|
|
575
608
|
people: (signal) =>
|
|
576
609
|
request("GET", `${base}${REST_ROUTES.people}`, undefined, restPeopleSchema, signal),
|
|
577
610
|
updatePerson: (id, patch) =>
|
|
578
611
|
request("PATCH", `${base}${restPath("people", id)}`, patch, restPersonSchema),
|
|
612
|
+
createListing: (input) =>
|
|
613
|
+
request("POST", `${base}${REST_ROUTES.listings}`, input, restListingSchema),
|
|
614
|
+
removeListing: (id) =>
|
|
615
|
+
request("DELETE", `${base}${restPath("listings", id)}`, undefined, undefined),
|
|
579
616
|
projects: (signal) =>
|
|
580
617
|
request("GET", `${base}${REST_ROUTES.projects}`, undefined, restProjectsSchema, signal),
|
|
581
618
|
createProject: (input) =>
|
|
@@ -591,6 +628,13 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
591
628
|
),
|
|
592
629
|
categories: (signal) =>
|
|
593
630
|
request("GET", `${base}${REST_ROUTES.categories}`, undefined, restCategoriesSchema, signal),
|
|
631
|
+
rules: (signal) =>
|
|
632
|
+
request("GET", `${base}${REST_ROUTES.rules}`, undefined, restRulesSchema, signal),
|
|
633
|
+
writeRules: (input) => request("PUT", `${base}${REST_ROUTES.rules}`, input, restRulesSchema),
|
|
634
|
+
archiveRules: () =>
|
|
635
|
+
request("POST", `${base}${rulesPath("archive")}`, undefined, restRulesSchema),
|
|
636
|
+
rulesVersions: (signal) =>
|
|
637
|
+
request("GET", `${base}${rulesPath("versions")}`, undefined, restVersionsSchema, signal),
|
|
594
638
|
createCategory: (input) =>
|
|
595
639
|
request("POST", `${base}${REST_ROUTES.categories}`, input, restCategorySchema),
|
|
596
640
|
updateCategory: (key, patch) =>
|
|
@@ -598,6 +642,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
598
642
|
removeCategory: (key) => request("DELETE", `${base}${categoryPath(key)}`, undefined, undefined),
|
|
599
643
|
work: (signal) =>
|
|
600
644
|
request("GET", `${base}${REST_ROUTES.work}`, undefined, restWorkSchema, signal),
|
|
645
|
+
personWork: (personId, signal) =>
|
|
646
|
+
request("GET", `${base}${personWorkPath(personId)}`, undefined, restPersonWorkSchema, signal),
|
|
601
647
|
tokens: (signal) =>
|
|
602
648
|
request("GET", `${base}${REST_ROUTES.tokens}`, undefined, restTokensSchema, signal),
|
|
603
649
|
createToken: (input) =>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import type { ReactNode, SyntheticEvent } from "react";
|
|
1
2
|
import { OPEN_TASK_STATES, type RestCategory, type RestPerson, type RestTask } from "../api.js";
|
|
2
3
|
import {
|
|
4
|
+
addDays,
|
|
3
5
|
addMonths,
|
|
4
6
|
daysBetween,
|
|
5
7
|
firstDayOfWeek,
|
|
8
|
+
formatDays,
|
|
6
9
|
formatMonth,
|
|
7
10
|
formatWeekday,
|
|
8
11
|
type GridDay,
|
|
@@ -10,15 +13,18 @@ import {
|
|
|
10
13
|
monthOf,
|
|
11
14
|
today as todayOf,
|
|
12
15
|
weekdays,
|
|
16
|
+
weekOf,
|
|
13
17
|
} from "../dates.js";
|
|
14
18
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
15
|
-
import { cx, PersonChip, personName } from "./ui.js";
|
|
19
|
+
import { Avatar, cx, PersonChip, personName } from "./ui.js";
|
|
16
20
|
|
|
17
|
-
// The calendar of a project (ADR-0019): the month as a grid
|
|
18
|
-
// due day lies across its days as a bar, in a lane of its own within
|
|
19
|
-
// of one week never cross; a task with
|
|
20
|
-
// start
|
|
21
|
-
// its
|
|
21
|
+
// The calendar of a project (ADR-0019): the month as a grid, or one week on its own. A task
|
|
22
|
+
// planned from a start to a due day lies across its days as a bar, in a lane of its own within
|
|
23
|
+
// each week so that the bars of one week never cross; a task with one day alone, a due day or
|
|
24
|
+
// a start, sits on that day as a dot. On a project's calendar the person who has a task stands
|
|
25
|
+
// before its title, since the days hold several people's work. Who has what says whether the
|
|
26
|
+
// work is on its days. Takes its data as props and nothing from the network; the month or the
|
|
27
|
+
// week shown is the address's (ADR-0010).
|
|
22
28
|
|
|
23
29
|
const isOpen = (task: RestTask): boolean => OPEN_TASK_STATES.includes(task.state);
|
|
24
30
|
|
|
@@ -99,40 +105,165 @@ function push(map: Map<string, RestTask[]>, day: string, task: RestTask): void {
|
|
|
99
105
|
}
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const firstDay = firstDayOfWeek(tag);
|
|
107
|
-
const weeks = monthGrid(props.month, firstDay);
|
|
108
|
-
const due = new Map<string, RestTask[]>();
|
|
109
|
-
const starting = new Map<string, RestTask[]>();
|
|
110
|
-
for (const task of props.tasks) {
|
|
108
|
+
/** The tasks that sit on one day each, by day: due on it, or beginning on it with no due day yet. */
|
|
109
|
+
function onDays(tasks: readonly RestTask[]): ReadonlyMap<string, readonly RestTask[]> {
|
|
110
|
+
const on = new Map<string, RestTask[]>();
|
|
111
|
+
for (const task of tasks) {
|
|
111
112
|
if (spans(task)) {
|
|
112
113
|
continue;
|
|
113
114
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
push(starting, task.startsOn, task);
|
|
115
|
+
const day = task.dueOn ?? task.startsOn;
|
|
116
|
+
if (day !== null) {
|
|
117
|
+
push(on, day, task);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
// Within a day, what is still open comes first; the rest reads as what was.
|
|
121
|
-
for (const list of
|
|
121
|
+
for (const list of on.values()) {
|
|
122
122
|
list.sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || a.number - b.number);
|
|
123
123
|
}
|
|
124
|
+
return on;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** How a task reads: struck through once it is over, and marked once its due day is past. */
|
|
128
|
+
const tone = (task: RestTask, now: string): string =>
|
|
129
|
+
cx(
|
|
130
|
+
!isOpen(task) && "sc-calendar-over",
|
|
131
|
+
isOpen(task) && task.dueOn !== null && task.dueOn < now && "sc-calendar-late",
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
interface WeekRowProps {
|
|
135
|
+
readonly week: readonly GridDay[];
|
|
136
|
+
readonly segments: readonly WeekSegment[];
|
|
137
|
+
/** The tasks on one day each, by day. */
|
|
138
|
+
readonly on: ReadonlyMap<string, readonly RestTask[]>;
|
|
139
|
+
readonly now: string;
|
|
140
|
+
readonly taskHref: (task: RestTask) => string;
|
|
141
|
+
/** What stands before a task's title where the tasks are of several projects: the project's mark. */
|
|
142
|
+
readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
|
|
143
|
+
/** Whether the person who has a task stands before its title, where the days hold several people's work. */
|
|
144
|
+
readonly people?: boolean | undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Marks an item whose title its line cut short, or which shows none (a bar on its later days),
|
|
149
|
+
* so that the tip it carries in `data-tip` is shown at once while the pointer is on it. Measured
|
|
150
|
+
* as the pointer arrives, since only the browser knows where the line ended.
|
|
151
|
+
*/
|
|
152
|
+
function reveal(event: SyntheticEvent<HTMLElement>): void {
|
|
153
|
+
const item = event.currentTarget;
|
|
154
|
+
const title = item.querySelector(".sc-calendar-task-title");
|
|
155
|
+
item.toggleAttribute("data-clipped", title === null || title.scrollWidth > title.clientWidth);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** One week as a row of cells: the bars across the days in their lanes, then the tasks on each day. */
|
|
159
|
+
function WeekRow(props: WeekRowProps) {
|
|
160
|
+
const { week, segments, now } = props;
|
|
161
|
+
const tip = (task: RestTask): string =>
|
|
162
|
+
props.people === true && task.assignee !== null
|
|
163
|
+
? `#${task.number} ${task.title} \u00B7 ${personName(task.assignee)}`
|
|
164
|
+
: `#${task.number} ${task.title}`;
|
|
165
|
+
const who = (task: RestTask): ReactNode =>
|
|
166
|
+
props.people === true && task.assignee !== null ? (
|
|
167
|
+
<Avatar person={task.assignee} size="xs" />
|
|
168
|
+
) : null;
|
|
169
|
+
const lanes = segments.reduce((most, segment) => Math.max(most, segment.lane + 1), 0);
|
|
170
|
+
return (
|
|
171
|
+
<tr>
|
|
172
|
+
{week.map((cell, column) => (
|
|
173
|
+
<td
|
|
174
|
+
key={cell.day}
|
|
175
|
+
className={cx(
|
|
176
|
+
"sc-calendar-cell",
|
|
177
|
+
!cell.inMonth && "sc-calendar-cell-outside",
|
|
178
|
+
cell.day === now && "sc-calendar-cell-today",
|
|
179
|
+
)}
|
|
180
|
+
>
|
|
181
|
+
<span className="sc-calendar-day">{Number(cell.day.slice(8))}</span>
|
|
182
|
+
{Array.from({ length: lanes }, (_, lane) => {
|
|
183
|
+
const segment = segments.find(
|
|
184
|
+
(candidate) =>
|
|
185
|
+
candidate.lane === lane && candidate.from <= column && column <= candidate.to,
|
|
186
|
+
);
|
|
187
|
+
if (segment === undefined) {
|
|
188
|
+
// The lane is someone else's on this day: the bars below keep their height.
|
|
189
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: an empty lane is nothing but its position, and the lanes of a week never reorder
|
|
190
|
+
return <span key={lane} className="sc-calendar-lane" aria-hidden="true" />;
|
|
191
|
+
}
|
|
192
|
+
const { task } = segment;
|
|
193
|
+
return (
|
|
194
|
+
<a
|
|
195
|
+
key={task.id}
|
|
196
|
+
className={cx(
|
|
197
|
+
"sc-calendar-bar",
|
|
198
|
+
`sc-category-${task.category?.color ?? "gray"}`,
|
|
199
|
+
segment.from === column && segment.begins && "sc-calendar-bar-begins",
|
|
200
|
+
segment.to === column && segment.ends && "sc-calendar-bar-ends",
|
|
201
|
+
tone(task, now),
|
|
202
|
+
"sc-tip",
|
|
203
|
+
)}
|
|
204
|
+
href={props.taskHref(task)}
|
|
205
|
+
data-tip={tip(task)}
|
|
206
|
+
onMouseEnter={reveal}
|
|
207
|
+
onFocus={reveal}
|
|
208
|
+
>
|
|
209
|
+
{segment.from === column && (
|
|
210
|
+
<>
|
|
211
|
+
{props.markOf?.(task)}
|
|
212
|
+
{who(task)}
|
|
213
|
+
<span className="sc-calendar-task-title">{task.title}</span>
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
</a>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
219
|
+
{(props.on.get(cell.day) ?? []).map((task) => {
|
|
220
|
+
const mark = props.markOf?.(task);
|
|
221
|
+
return (
|
|
222
|
+
<a
|
|
223
|
+
key={task.id}
|
|
224
|
+
className={cx("sc-calendar-task", tone(task, now), "sc-tip")}
|
|
225
|
+
href={props.taskHref(task)}
|
|
226
|
+
data-tip={tip(task)}
|
|
227
|
+
onMouseEnter={reveal}
|
|
228
|
+
onFocus={reveal}
|
|
229
|
+
>
|
|
230
|
+
{mark === undefined || mark === null ? (
|
|
231
|
+
<span
|
|
232
|
+
className={cx(
|
|
233
|
+
"sc-calendar-dot",
|
|
234
|
+
`sc-category-${task.category?.color ?? "gray"}`,
|
|
235
|
+
)}
|
|
236
|
+
/>
|
|
237
|
+
) : (
|
|
238
|
+
mark
|
|
239
|
+
)}
|
|
240
|
+
{who(task)}
|
|
241
|
+
<span className="sc-calendar-task-title">{task.title}</span>
|
|
242
|
+
</a>
|
|
243
|
+
);
|
|
244
|
+
})}
|
|
245
|
+
</td>
|
|
246
|
+
))}
|
|
247
|
+
</tr>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function Calendar(props: CalendarProps) {
|
|
252
|
+
const words = useWords().calendar;
|
|
253
|
+
const { tag } = useLanguage();
|
|
254
|
+
const now = props.today ?? todayOf();
|
|
255
|
+
const firstDay = firstDayOfWeek(tag);
|
|
256
|
+
const weeks = monthGrid(props.month, firstDay);
|
|
257
|
+
const on = onDays(props.tasks);
|
|
124
258
|
const inMonth = (day: string) => monthOf(day) === props.month;
|
|
125
259
|
const monthWeeks = weeks.map((week) => ({ week, segments: weekSegments(week, props.tasks) }));
|
|
126
260
|
const nothing =
|
|
127
|
-
[...
|
|
128
|
-
[...starting.keys()].every((day) => !inMonth(day)) &&
|
|
261
|
+
[...on.keys()].every((day) => !inMonth(day)) &&
|
|
129
262
|
monthWeeks.every(({ week, segments }) =>
|
|
130
263
|
segments.every(
|
|
131
264
|
(segment) => !week.slice(segment.from, segment.to + 1).some((cell) => cell.inMonth),
|
|
132
265
|
),
|
|
133
266
|
);
|
|
134
|
-
const tone = (task: RestTask, day: string) =>
|
|
135
|
-
cx(!isOpen(task) && "sc-calendar-over", isOpen(task) && day < now && "sc-calendar-late");
|
|
136
267
|
return (
|
|
137
268
|
<div className="sc-calendar">
|
|
138
269
|
<div className="sc-calendar-head">
|
|
@@ -171,83 +302,17 @@ export function Calendar(props: CalendarProps) {
|
|
|
171
302
|
</tr>
|
|
172
303
|
</thead>
|
|
173
304
|
<tbody>
|
|
174
|
-
{monthWeeks.map(({ week, segments }) =>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)}
|
|
186
|
-
>
|
|
187
|
-
<span className="sc-calendar-day">{Number(cell.day.slice(8))}</span>
|
|
188
|
-
{Array.from({ length: lanes }, (_, lane) => {
|
|
189
|
-
const segment = segments.find(
|
|
190
|
-
(candidate) =>
|
|
191
|
-
candidate.lane === lane &&
|
|
192
|
-
candidate.from <= column &&
|
|
193
|
-
column <= candidate.to,
|
|
194
|
-
);
|
|
195
|
-
if (segment === undefined) {
|
|
196
|
-
// The lane is someone else's on this day: the bars below keep their height.
|
|
197
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: an empty lane is nothing but its position, and the lanes of a week never reorder
|
|
198
|
-
return <span key={lane} className="sc-calendar-lane" aria-hidden="true" />;
|
|
199
|
-
}
|
|
200
|
-
const { task } = segment;
|
|
201
|
-
return (
|
|
202
|
-
<a
|
|
203
|
-
key={task.id}
|
|
204
|
-
className={cx(
|
|
205
|
-
"sc-calendar-bar",
|
|
206
|
-
`sc-category-${task.category?.color ?? "gray"}`,
|
|
207
|
-
segment.from === column && segment.begins && "sc-calendar-bar-begins",
|
|
208
|
-
segment.to === column && segment.ends && "sc-calendar-bar-ends",
|
|
209
|
-
tone(task, task.dueOn ?? cell.day),
|
|
210
|
-
)}
|
|
211
|
-
href={props.taskHref(task)}
|
|
212
|
-
title={`#${task.number} ${task.title}`}
|
|
213
|
-
>
|
|
214
|
-
{segment.from === column && (
|
|
215
|
-
<span className="sc-calendar-task-title">{task.title}</span>
|
|
216
|
-
)}
|
|
217
|
-
</a>
|
|
218
|
-
);
|
|
219
|
-
})}
|
|
220
|
-
{(due.get(cell.day) ?? []).map((task) => (
|
|
221
|
-
<a
|
|
222
|
-
key={task.id}
|
|
223
|
-
className={cx("sc-calendar-task", tone(task, cell.day))}
|
|
224
|
-
href={props.taskHref(task)}
|
|
225
|
-
title={`#${task.number} ${task.title}`}
|
|
226
|
-
>
|
|
227
|
-
<span
|
|
228
|
-
className={cx(
|
|
229
|
-
"sc-calendar-dot",
|
|
230
|
-
`sc-category-${task.category?.color ?? "gray"}`,
|
|
231
|
-
)}
|
|
232
|
-
/>
|
|
233
|
-
<span className="sc-calendar-task-title">{task.title}</span>
|
|
234
|
-
</a>
|
|
235
|
-
))}
|
|
236
|
-
{(starting.get(cell.day) ?? []).map((task) => (
|
|
237
|
-
<a
|
|
238
|
-
key={task.id}
|
|
239
|
-
className="sc-calendar-task sc-calendar-task-start"
|
|
240
|
-
href={props.taskHref(task)}
|
|
241
|
-
title={`#${task.number} ${task.title}: ${words.startsOnly}`}
|
|
242
|
-
>
|
|
243
|
-
<span className="sc-calendar-task-title">{task.title}</span>
|
|
244
|
-
</a>
|
|
245
|
-
))}
|
|
246
|
-
</td>
|
|
247
|
-
))}
|
|
248
|
-
</tr>
|
|
249
|
-
);
|
|
250
|
-
})}
|
|
305
|
+
{monthWeeks.map(({ week, segments }) => (
|
|
306
|
+
<WeekRow
|
|
307
|
+
key={week[0]?.day}
|
|
308
|
+
week={week}
|
|
309
|
+
segments={segments}
|
|
310
|
+
on={on}
|
|
311
|
+
now={now}
|
|
312
|
+
taskHref={props.taskHref}
|
|
313
|
+
people
|
|
314
|
+
/>
|
|
315
|
+
))}
|
|
251
316
|
</tbody>
|
|
252
317
|
</table>
|
|
253
318
|
{(props.categories ?? []).length > 0 && (
|
|
@@ -267,6 +332,82 @@ export function Calendar(props: CalendarProps) {
|
|
|
267
332
|
);
|
|
268
333
|
}
|
|
269
334
|
|
|
335
|
+
export interface WeekProps {
|
|
336
|
+
readonly tasks: readonly RestTask[];
|
|
337
|
+
/** A day of the week shown; today's week when left out. */
|
|
338
|
+
readonly day?: string | undefined;
|
|
339
|
+
/** The day to count from; the browser's today when left out. */
|
|
340
|
+
readonly today?: string | undefined;
|
|
341
|
+
readonly taskHref: (task: RestTask) => string;
|
|
342
|
+
/** Where the week of a day is shown. */
|
|
343
|
+
readonly weekHref: (day: string) => string;
|
|
344
|
+
/** What stands before a task's title where the tasks are of several projects: the project's mark. */
|
|
345
|
+
readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** One week on its own: the days as columns, the tasks on them, and the weeks around a step away. */
|
|
349
|
+
export function Week(props: WeekProps) {
|
|
350
|
+
const words = useWords().calendar.week;
|
|
351
|
+
const { tag } = useLanguage();
|
|
352
|
+
const now = props.today ?? todayOf();
|
|
353
|
+
const firstDay = firstDayOfWeek(tag);
|
|
354
|
+
const days = weekOf(props.day ?? now, firstDay);
|
|
355
|
+
const first = days[0] ?? now;
|
|
356
|
+
const last = days[days.length - 1] ?? now;
|
|
357
|
+
const week: GridDay[] = days.map((day) => ({ day, inMonth: true }));
|
|
358
|
+
const on = onDays(props.tasks);
|
|
359
|
+
const segments = weekSegments(week, props.tasks);
|
|
360
|
+
const nothing = segments.length === 0 && days.every((day) => !on.has(day));
|
|
361
|
+
return (
|
|
362
|
+
<div className="sc-calendar sc-week">
|
|
363
|
+
<div className="sc-calendar-head">
|
|
364
|
+
<h2 className="sc-calendar-month">{formatDays(first, last, tag)}</h2>
|
|
365
|
+
<nav className="sc-calendar-nav" aria-label={words.title}>
|
|
366
|
+
<a
|
|
367
|
+
className="sc-button sc-button-ghost sc-button-sm"
|
|
368
|
+
href={props.weekHref(addDays(first, -7))}
|
|
369
|
+
aria-label={words.previous}
|
|
370
|
+
>
|
|
371
|
+
{"<"}
|
|
372
|
+
</a>
|
|
373
|
+
<a className="sc-button sc-button-ghost sc-button-sm" href={props.weekHref(now)}>
|
|
374
|
+
{words.thisWeek}
|
|
375
|
+
</a>
|
|
376
|
+
<a
|
|
377
|
+
className="sc-button sc-button-ghost sc-button-sm"
|
|
378
|
+
href={props.weekHref(addDays(first, 7))}
|
|
379
|
+
aria-label={words.next}
|
|
380
|
+
>
|
|
381
|
+
{">"}
|
|
382
|
+
</a>
|
|
383
|
+
</nav>
|
|
384
|
+
</div>
|
|
385
|
+
<table className="sc-calendar-grid" aria-label={words.title}>
|
|
386
|
+
<thead>
|
|
387
|
+
<tr>
|
|
388
|
+
{weekdays(firstDay).map((weekday) => (
|
|
389
|
+
<th key={weekday} scope="col">
|
|
390
|
+
{formatWeekday(weekday, tag)}
|
|
391
|
+
</th>
|
|
392
|
+
))}
|
|
393
|
+
</tr>
|
|
394
|
+
</thead>
|
|
395
|
+
<tbody>
|
|
396
|
+
<WeekRow
|
|
397
|
+
week={week}
|
|
398
|
+
segments={segments}
|
|
399
|
+
on={on}
|
|
400
|
+
now={now}
|
|
401
|
+
taskHref={props.taskHref}
|
|
402
|
+
markOf={props.markOf}
|
|
403
|
+
/>
|
|
404
|
+
</tbody>
|
|
405
|
+
</table>
|
|
406
|
+
{nothing && <p className="sc-muted">{words.nothing}</p>}
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
270
411
|
/** One person's share of the open work, as the table of who has what says it. */
|
|
271
412
|
export interface WorkloadRow {
|
|
272
413
|
/** The person, or nobody for the tasks assigned to no one. */
|
|
@@ -2,7 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
import { normalizeConnectCode, type RestConnectRequest, type RestTokenInput } from "../api.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
4
|
import { TokenForm } from "./tokens.js";
|
|
5
|
-
import { Button, Callout, Time } from "./ui.js";
|
|
5
|
+
import { Button, Callout, CopyButton, Time } from "./ui.js";
|
|
6
6
|
|
|
7
7
|
// Connecting an agent (ADR-0011): where a person types the code their agent showed, what asks
|
|
8
8
|
// to connect under a code with the way to approve it, and the words that tell a person what to
|
|
@@ -106,16 +106,22 @@ export interface ConnectWordsProps {
|
|
|
106
106
|
readonly origin: string;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* What a person says to their agent to connect it: the words to paste into its chat, as they
|
|
111
|
+
* are, with the way to copy them; and the commands for a person who runs them by hand.
|
|
112
|
+
*/
|
|
110
113
|
export function ConnectWords(props: ConnectWordsProps) {
|
|
111
114
|
const words = useWords().connect.words;
|
|
115
|
+
const quote = words.quote(props.origin);
|
|
112
116
|
return (
|
|
113
117
|
<section className="sc-panel sc-connect" aria-label={words.title}>
|
|
114
118
|
<h2 className="sc-section-title">{words.title}</h2>
|
|
115
|
-
<p>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
<p>{words.tellYourAgent}</p>
|
|
120
|
+
<div className="sc-copyable">
|
|
121
|
+
<pre className="sc-code">{quote}</pre>
|
|
122
|
+
<CopyButton text={quote} size="sm" />
|
|
123
|
+
</div>
|
|
124
|
+
<p>{words.doesTheRest}</p>
|
|
119
125
|
<p>{words.byHand}</p>
|
|
120
126
|
<pre className="sc-code">
|
|
121
127
|
npm install -g @skillcdn/console{"\n"}console login --url {props.origin}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
agentLabel,
|
|
3
4
|
MAX_BODY_LENGTH,
|
|
4
5
|
MAX_NOTE_LENGTH,
|
|
5
6
|
type RestAnswerInput,
|
|
@@ -128,7 +129,7 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
128
129
|
<PersonChip person={decision.raisedBy} />{" "}
|
|
129
130
|
{decision.run === null
|
|
130
131
|
? words.decisions.asked
|
|
131
|
-
: words.decisions.askedThrough(decision.run.agent)}{" "}
|
|
132
|
+
: words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
|
|
132
133
|
<Time iso={decision.createdAt} />
|
|
133
134
|
{task !== undefined && (
|
|
134
135
|
<>
|
|
@@ -326,7 +327,6 @@ export function DecisionList(props: DecisionListProps) {
|
|
|
326
327
|
<h2 className="sc-section-title">
|
|
327
328
|
{words.decisions.record} <span className="sc-column-count">{answered.length}</span>
|
|
328
329
|
</h2>
|
|
329
|
-
<p className="sc-lead">{words.decisions.recordLead}</p>
|
|
330
330
|
<DecisionLog
|
|
331
331
|
decisions={answered}
|
|
332
332
|
tasks={props.tasks}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
2
|
+
import { agentLabel } from "../agent-name.js";
|
|
2
3
|
import {
|
|
3
4
|
foldersAbove,
|
|
4
5
|
isDocumentPath,
|
|
@@ -59,7 +60,7 @@ export function DocumentCrumbs(props: {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/** Who wrote a version, and through which agent, as a chip and a word. */
|
|
62
|
-
function Writer(props: {
|
|
63
|
+
export function Writer(props: {
|
|
63
64
|
readonly by: RestDocumentSummary["updatedBy"];
|
|
64
65
|
readonly agent: string | null;
|
|
65
66
|
}) {
|
|
@@ -68,8 +69,11 @@ function Writer(props: {
|
|
|
68
69
|
<>
|
|
69
70
|
<PersonChip person={props.by} />
|
|
70
71
|
{props.agent !== null && (
|
|
71
|
-
<span
|
|
72
|
-
|
|
72
|
+
<span
|
|
73
|
+
className="sc-feed-agent"
|
|
74
|
+
title={agentLabel(props.agent) === props.agent ? words.docs.asAgentTitle : props.agent}
|
|
75
|
+
>
|
|
76
|
+
{words.feed.as(agentLabel(props.agent))}
|
|
73
77
|
</span>
|
|
74
78
|
)}
|
|
75
79
|
</>
|