@skillcdn/console 0.1.10 → 0.1.11
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 +4 -4
- package/dist/api.d.ts +6 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +49 -9
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +2 -0
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +7 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +10 -9
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +14 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -1
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +1 -4
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +5 -12
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +21 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +67 -7
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +11 -9
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +11 -7
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/paged.d.ts +14 -0
- package/dist/components/paged.d.ts.map +1 -0
- package/dist/components/paged.js +28 -0
- package/dist/components/paged.js.map +1 -0
- package/dist/components/projects.d.ts +22 -2
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +35 -7
- package/dist/components/projects.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +2 -2
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts +2 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +1 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/task-list.d.ts +1 -1
- package/dist/components/task-list.d.ts.map +1 -1
- package/dist/components/task-list.js +6 -4
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.js +1 -1
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +7 -3
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +149 -2
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +34 -18
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +5 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +15 -2
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +12 -6
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +12 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +12 -8
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/marks.d.ts +12 -0
- package/dist/marks.d.ts.map +1 -0
- package/dist/marks.js +31 -0
- package/dist/marks.js.map +1 -0
- package/dist/router.d.ts +4 -3
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +11 -5
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +7 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +8 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +123 -3
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +42 -10
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +10 -4
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +8 -3
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +7 -5
- package/docs/specs/rest.md +18 -12
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +1 -1
- package/src/api.ts +12 -0
- package/src/cli/cli.ts +51 -8
- package/src/cli/format.ts +8 -6
- package/src/cli/help.ts +10 -10
- package/src/client.ts +29 -6
- package/src/components/board.tsx +5 -38
- package/src/components/calendar.tsx +166 -54
- package/src/components/decision-list.tsx +38 -33
- package/src/components/event-feed.tsx +34 -27
- package/src/components/paged.tsx +39 -0
- package/src/components/projects.tsx +128 -10
- package/src/components/runs.tsx +7 -1
- package/src/components/shell.tsx +5 -2
- package/src/components/task-list.tsx +35 -32
- package/src/components/task-view.tsx +1 -1
- package/src/components/work.tsx +12 -2
- package/src/console.tsx +54 -20
- package/src/data.ts +20 -4
- package/src/i18n/en.ts +12 -8
- package/src/i18n/ko.ts +12 -8
- package/src/index.ts +4 -0
- package/src/limits.ts +6 -0
- package/src/marks.ts +40 -0
- package/src/router.ts +19 -7
- package/src/routes.ts +9 -0
- package/src/schemas.ts +48 -8
- package/src/styles/console.css +149 -2
- package/src/vocabulary.ts +14 -4
package/src/data.ts
CHANGED
|
@@ -58,10 +58,13 @@ const FEED_MAX_PAGES = 20;
|
|
|
58
58
|
const RELOAD_DELAY_MS = 150;
|
|
59
59
|
|
|
60
60
|
export interface ConsoleActions {
|
|
61
|
-
/** Makes a project, whose owner the person becomes. */
|
|
62
|
-
createProject(input: RestProjectInput): Promise<RestProject>;
|
|
61
|
+
/** Makes a project, whose owner the person becomes, with an image for its mark when one is given. */
|
|
62
|
+
createProject(input: RestProjectInput, icon?: FileUpload): Promise<RestProject>;
|
|
63
63
|
/** Changes the settings of the project the page is on; for an owner. */
|
|
64
64
|
updateProject(patch: RestProjectPatch): Promise<RestProject>;
|
|
65
|
+
/** Sets the image for the mark of the project the page is on, or takes it away; for an owner. */
|
|
66
|
+
setProjectIcon(input: FileUpload): Promise<RestProject>;
|
|
67
|
+
removeProjectIcon(): Promise<RestProject>;
|
|
65
68
|
addMember(input: RestMemberInput): Promise<RestMember>;
|
|
66
69
|
updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
|
|
67
70
|
removeMember(personId: string): Promise<void>;
|
|
@@ -387,10 +390,11 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
387
390
|
return client.project(projectKey);
|
|
388
391
|
};
|
|
389
392
|
return {
|
|
390
|
-
async createProject(input) {
|
|
393
|
+
async createProject(input, icon) {
|
|
391
394
|
const made = await client.createProject(input);
|
|
395
|
+
const marked = icon === undefined ? made : await client.project(made.key).setIcon(icon);
|
|
392
396
|
await refreshWorkspace();
|
|
393
|
-
return
|
|
397
|
+
return marked;
|
|
394
398
|
},
|
|
395
399
|
async updateProject(patch) {
|
|
396
400
|
const changed = await scope().update(patch);
|
|
@@ -398,6 +402,18 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
398
402
|
await refreshWorkspace();
|
|
399
403
|
return changed;
|
|
400
404
|
},
|
|
405
|
+
async setProjectIcon(input) {
|
|
406
|
+
const changed = await scope().setIcon(input);
|
|
407
|
+
setProject(changed);
|
|
408
|
+
await refreshWorkspace();
|
|
409
|
+
return changed;
|
|
410
|
+
},
|
|
411
|
+
async removeProjectIcon() {
|
|
412
|
+
const changed = await scope().removeIcon();
|
|
413
|
+
setProject(changed);
|
|
414
|
+
await refreshWorkspace();
|
|
415
|
+
return changed;
|
|
416
|
+
},
|
|
401
417
|
async addMember(input) {
|
|
402
418
|
const member = await scope().addMember(input);
|
|
403
419
|
await refreshProject();
|
package/src/i18n/en.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const en = {
|
|
|
43
43
|
private: "Only those listed",
|
|
44
44
|
} satisfies Record<ProjectVisibility, string>,
|
|
45
45
|
runStatus: {
|
|
46
|
-
running: "
|
|
46
|
+
running: "Open",
|
|
47
47
|
waiting: "Waiting for a decision",
|
|
48
48
|
finished: "Finished",
|
|
49
49
|
failed: "Failed",
|
|
@@ -95,6 +95,7 @@ export const en = {
|
|
|
95
95
|
nobody: "Nobody",
|
|
96
96
|
nothing: "Nothing",
|
|
97
97
|
language: "Language",
|
|
98
|
+
showMore: (n: number, of: number) => `Show ${n} more of ${of}`,
|
|
98
99
|
},
|
|
99
100
|
|
|
100
101
|
nav: {
|
|
@@ -150,7 +151,6 @@ export const en = {
|
|
|
150
151
|
list: "Projects",
|
|
151
152
|
private: "Private",
|
|
152
153
|
decisionsWaiting: (n: number) => `${count(n, "decision", "decisions")} waiting`,
|
|
153
|
-
agentsAtWork: (n: number) => `${count(n, "agent", "agents")} at work`,
|
|
154
154
|
form: {
|
|
155
155
|
aria: "New project",
|
|
156
156
|
name: "Name",
|
|
@@ -162,6 +162,11 @@ export const en = {
|
|
|
162
162
|
whoIsMember: "Who is a member",
|
|
163
163
|
skillsAddress: "Skills address",
|
|
164
164
|
skillsAddressPlaceholder: "/gh/<owner>/<repo>, or empty for the organization's",
|
|
165
|
+
symbol: "Mark",
|
|
166
|
+
image: "Image",
|
|
167
|
+
imageRemove: "Remove the image",
|
|
168
|
+
color: "Colour",
|
|
169
|
+
colorAuto: "Chosen from the key",
|
|
165
170
|
make: "Make the project",
|
|
166
171
|
},
|
|
167
172
|
members: {
|
|
@@ -207,9 +212,6 @@ export const en = {
|
|
|
207
212
|
moveTo: "Move to",
|
|
208
213
|
decisions: (n: number) => count(n, "decision", "decisions"),
|
|
209
214
|
decisionsTitle: "Decisions waiting for a person",
|
|
210
|
-
agentAtWork: "agent at work",
|
|
211
|
-
agentAtWorkTitle: "An agent is at work on it",
|
|
212
|
-
agentSince: (agent: string, since: string) => `${agent}, at work since ${since}`,
|
|
213
215
|
filters: {
|
|
214
216
|
aria: "Narrow the board",
|
|
215
217
|
everyone: "Everyone",
|
|
@@ -247,7 +249,6 @@ export const en = {
|
|
|
247
249
|
raiseDecision: "Raise a decision",
|
|
248
250
|
nothingMore: "Nothing more was written about it.",
|
|
249
251
|
links: "Links",
|
|
250
|
-
agentsAtWork: "Agents at work",
|
|
251
252
|
runs: "Runs",
|
|
252
253
|
decisions: "Decisions",
|
|
253
254
|
history: "What happened",
|
|
@@ -336,8 +337,10 @@ export const en = {
|
|
|
336
337
|
|
|
337
338
|
calendar: {
|
|
338
339
|
title: "Calendar",
|
|
339
|
-
lead: "The tasks
|
|
340
|
+
lead: "The tasks on their days: planned from a start to a due day, a task lies across them; due on a day alone, it sits on it. One with no day waits at the bottom to be given one.",
|
|
340
341
|
grid: "The month",
|
|
342
|
+
legend: { title: "Category colours" },
|
|
343
|
+
startsOnly: "Starts that day, with no due day yet",
|
|
341
344
|
previous: "The month before",
|
|
342
345
|
next: "The month after",
|
|
343
346
|
thisMonth: "This month",
|
|
@@ -346,7 +349,6 @@ export const en = {
|
|
|
346
349
|
nothingOverdue: "Nothing is overdue.",
|
|
347
350
|
unscheduled: "No day set",
|
|
348
351
|
allScheduled: "Every open task has a day.",
|
|
349
|
-
starts: "starts",
|
|
350
352
|
workload: {
|
|
351
353
|
title: "Who has what",
|
|
352
354
|
lead: "The open tasks of each person, and what of them is late or due within the week.",
|
|
@@ -509,6 +511,7 @@ export const en = {
|
|
|
509
511
|
runReported: (task: string, excerpt: string | undefined) =>
|
|
510
512
|
`reported on ${task}${excerpt === undefined ? "" : `: ${excerpt}`}`,
|
|
511
513
|
runHandedIn: (label: string, task: string) => `handed in ${label} on ${task}`,
|
|
514
|
+
runTakenOver: (task: string, agent: string) => `took over ${task} from ${agent}`,
|
|
512
515
|
runFinished: (task: string) => `finished ${task}`,
|
|
513
516
|
runFailed: (task: string) => `failed on ${task}`,
|
|
514
517
|
runAbandoned: (task: string) => `gave up on ${task}`,
|
|
@@ -524,6 +527,7 @@ export const en = {
|
|
|
524
527
|
for: "for",
|
|
525
528
|
since: "since",
|
|
526
529
|
until: "until",
|
|
530
|
+
lastHeard: "last heard",
|
|
527
531
|
markAbandoned: "Mark abandoned",
|
|
528
532
|
waitingForDecision: "Waiting for a decision",
|
|
529
533
|
answerIt: "answer it",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const ko: Messages = {
|
|
|
21
21
|
projectRole: { owner: "소유자", member: "멤버" },
|
|
22
22
|
visibility: { workspace: "워크스페이스의 모두", private: "목록에 있는 사람만" },
|
|
23
23
|
runStatus: {
|
|
24
|
-
running: "
|
|
24
|
+
running: "열림",
|
|
25
25
|
waiting: "결정 대기 중",
|
|
26
26
|
finished: "완료",
|
|
27
27
|
failed: "실패",
|
|
@@ -68,6 +68,7 @@ export const ko: Messages = {
|
|
|
68
68
|
nobody: "없음",
|
|
69
69
|
nothing: "없음",
|
|
70
70
|
language: "언어",
|
|
71
|
+
showMore: (n, of) => `${of}개 중 ${n}개 더 보기`,
|
|
71
72
|
},
|
|
72
73
|
|
|
73
74
|
nav: {
|
|
@@ -123,7 +124,6 @@ export const ko: Messages = {
|
|
|
123
124
|
list: "프로젝트",
|
|
124
125
|
private: "비공개",
|
|
125
126
|
decisionsWaiting: (n) => `결정 ${n}건 대기 중`,
|
|
126
|
-
agentsAtWork: (n) => `에이전트 ${n}개 작업 중`,
|
|
127
127
|
form: {
|
|
128
128
|
aria: "새 프로젝트",
|
|
129
129
|
name: "이름",
|
|
@@ -134,6 +134,11 @@ export const ko: Messages = {
|
|
|
134
134
|
whoIsMember: "멤버 범위",
|
|
135
135
|
skillsAddress: "스킬 주소",
|
|
136
136
|
skillsAddressPlaceholder: "/gh/<owner>/<repo>, 비우면 조직의 스킬",
|
|
137
|
+
symbol: "마크",
|
|
138
|
+
image: "이미지",
|
|
139
|
+
imageRemove: "이미지 지우기",
|
|
140
|
+
color: "색",
|
|
141
|
+
colorAuto: "키에서 정해진 색",
|
|
137
142
|
make: "프로젝트 만들기",
|
|
138
143
|
},
|
|
139
144
|
members: {
|
|
@@ -179,9 +184,6 @@ export const ko: Messages = {
|
|
|
179
184
|
moveTo: "이동",
|
|
180
185
|
decisions: (n) => `결정 ${n}건`,
|
|
181
186
|
decisionsTitle: "사람을 기다리는 결정",
|
|
182
|
-
agentAtWork: "에이전트 작업 중",
|
|
183
|
-
agentAtWorkTitle: "에이전트가 이 작업을 하고 있습니다",
|
|
184
|
-
agentSince: (agent, since) => `${agent}, ${since}부터 작업 중`,
|
|
185
187
|
filters: {
|
|
186
188
|
aria: "보드 좁혀 보기",
|
|
187
189
|
everyone: "모두",
|
|
@@ -219,7 +221,6 @@ export const ko: Messages = {
|
|
|
219
221
|
raiseDecision: "결정 요청",
|
|
220
222
|
nothingMore: "더 적힌 내용이 없습니다.",
|
|
221
223
|
links: "링크",
|
|
222
|
-
agentsAtWork: "작업 중인 에이전트",
|
|
223
224
|
runs: "실행",
|
|
224
225
|
decisions: "결정",
|
|
225
226
|
history: "일어난 일",
|
|
@@ -308,8 +309,10 @@ export const ko: Messages = {
|
|
|
308
309
|
|
|
309
310
|
calendar: {
|
|
310
311
|
title: "캘린더",
|
|
311
|
-
lead: "작업을
|
|
312
|
+
lead: "작업을 날짜 위에 놓고 봅니다. 시작일과 기한이 있는 작업은 그 기간에 걸쳐 놓이고, 기한만 있는 작업은 그날에 놓입니다. 날짜가 없는 작업은 아래에서 날짜를 기다립니다.",
|
|
312
313
|
grid: "이 달",
|
|
314
|
+
legend: { title: "카테고리 색" },
|
|
315
|
+
startsOnly: "그날 시작, 기한은 아직 없음",
|
|
313
316
|
previous: "이전 달",
|
|
314
317
|
next: "다음 달",
|
|
315
318
|
thisMonth: "이번 달",
|
|
@@ -318,7 +321,6 @@ export const ko: Messages = {
|
|
|
318
321
|
nothingOverdue: "기한이 지난 작업이 없습니다.",
|
|
319
322
|
unscheduled: "날짜 없음",
|
|
320
323
|
allScheduled: "열린 작업마다 날짜가 있습니다.",
|
|
321
|
-
starts: "시작",
|
|
322
324
|
workload: {
|
|
323
325
|
title: "누가 무엇을",
|
|
324
326
|
lead: "사람마다 열린 작업과, 그중 늦은 것과 일주일 안에 마감인 것.",
|
|
@@ -477,6 +479,7 @@ export const ko: Messages = {
|
|
|
477
479
|
runReported: (task, excerpt) =>
|
|
478
480
|
`${task}에 보고했습니다${excerpt === undefined ? "" : `: ${excerpt}`}`,
|
|
479
481
|
runHandedIn: (label, task) => `${task}에 ${label}을(를) 제출했습니다`,
|
|
482
|
+
runTakenOver: (task, agent) => `${agent}에게서 ${task}을(를) 넘겨받았습니다`,
|
|
480
483
|
runFinished: (task) => `${task}을(를) 끝냈습니다`,
|
|
481
484
|
runFailed: (task) => `${task}에 실패했습니다`,
|
|
482
485
|
runAbandoned: (task) => `${task}을(를) 포기했습니다`,
|
|
@@ -492,6 +495,7 @@ export const ko: Messages = {
|
|
|
492
495
|
for: "대신:",
|
|
493
496
|
since: "시작",
|
|
494
497
|
until: "종료",
|
|
498
|
+
lastHeard: "마지막 소식",
|
|
495
499
|
markAbandoned: "중단됨으로 표시",
|
|
496
500
|
waitingForDecision: "결정을 기다리는 중",
|
|
497
501
|
answerIt: "답변하기",
|
package/src/index.ts
CHANGED
|
@@ -16,9 +16,11 @@ export {
|
|
|
16
16
|
export {
|
|
17
17
|
Calendar,
|
|
18
18
|
type CalendarProps,
|
|
19
|
+
type WeekSegment,
|
|
19
20
|
Workload,
|
|
20
21
|
type WorkloadProps,
|
|
21
22
|
type WorkloadRow,
|
|
23
|
+
weekSegments,
|
|
22
24
|
workloadOf,
|
|
23
25
|
} from "./components/calendar.js";
|
|
24
26
|
export { CategoryList, type CategoryListProps } from "./components/categories.js";
|
|
@@ -64,6 +66,7 @@ export {
|
|
|
64
66
|
type GoalViewProps,
|
|
65
67
|
} from "./components/goals.js";
|
|
66
68
|
export { Markdown, type MarkdownProps } from "./components/markdown.js";
|
|
69
|
+
export { PAGE_SIZE, ShowMore, usePaged } from "./components/paged.js";
|
|
67
70
|
export { PeopleList, type PeopleListProps } from "./components/people.js";
|
|
68
71
|
export {
|
|
69
72
|
keyOf,
|
|
@@ -74,6 +77,7 @@ export {
|
|
|
74
77
|
type ProjectFormProps,
|
|
75
78
|
ProjectList,
|
|
76
79
|
type ProjectListProps,
|
|
80
|
+
ProjectMark,
|
|
77
81
|
ProjectRoleBadge,
|
|
78
82
|
VISIBILITY_LABELS,
|
|
79
83
|
} from "./components/projects.js";
|
package/src/limits.ts
CHANGED
|
@@ -64,3 +64,9 @@ export const MAX_CATEGORY_KEY_LENGTH = 40;
|
|
|
64
64
|
export const MAX_CATEGORY_NAME_LENGTH = 60;
|
|
65
65
|
/** How many categories one workspace may have: a vocabulary, not a tag cloud. */
|
|
66
66
|
export const MAX_CATEGORIES = 50;
|
|
67
|
+
/** A project's mark: a letter or two, or a symbol, shown on its colour; counted in code units. */
|
|
68
|
+
export const MAX_PROJECT_SYMBOL_LENGTH = 8;
|
|
69
|
+
/** How large a project's own image for its mark may be, in bytes. */
|
|
70
|
+
export const MAX_PROJECT_ICON_BYTES = 512 * 1024;
|
|
71
|
+
/** How many items a listing answers with when none is asked for, and the most one may ask for. */
|
|
72
|
+
export const DEFAULT_PAGE_LIMIT = 50;
|
package/src/marks.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { COLOR_NAMES, type ColorName } from "./vocabulary.js";
|
|
2
|
+
|
|
3
|
+
// A project's mark: a symbol on a colour, so that projects tell apart at a glance. A project
|
|
4
|
+
// that names neither gets a colour from its key, the same everywhere and every time, and the
|
|
5
|
+
// first letter of its name.
|
|
6
|
+
|
|
7
|
+
/** A small, well-spread hash of a string: enough to pick a colour, nothing more. */
|
|
8
|
+
function hashOf(value: string): number {
|
|
9
|
+
let hash = 2166136261;
|
|
10
|
+
for (const char of value) {
|
|
11
|
+
hash ^= char.codePointAt(0) ?? 0;
|
|
12
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
13
|
+
}
|
|
14
|
+
return hash;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The colour of a project's mark: the one it names, else one chosen from its key. */
|
|
18
|
+
export function projectColorOf(project: {
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly color: ColorName | null;
|
|
21
|
+
}): ColorName {
|
|
22
|
+
if (project.color !== null) {
|
|
23
|
+
return project.color;
|
|
24
|
+
}
|
|
25
|
+
// Gray is for what has no colour; a project chosen one gets one of the rest.
|
|
26
|
+
const choices = COLOR_NAMES.filter((name) => name !== "gray");
|
|
27
|
+
return choices[hashOf(project.key) % choices.length] ?? "gray";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The symbol of a project's mark: the one it names, else the first letter of its name. */
|
|
31
|
+
export function projectSymbolOf(project: {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly symbol: string | null;
|
|
34
|
+
}): string {
|
|
35
|
+
if (project.symbol !== null && project.symbol.length > 0) {
|
|
36
|
+
return project.symbol;
|
|
37
|
+
}
|
|
38
|
+
const first = [...project.name.trim()][0];
|
|
39
|
+
return first === undefined ? "?" : first.toUpperCase();
|
|
40
|
+
}
|
package/src/router.ts
CHANGED
|
@@ -52,8 +52,13 @@ export type Route =
|
|
|
52
52
|
| { readonly name: "decisions"; readonly project: string }
|
|
53
53
|
| { readonly name: "new-decision"; readonly project: string }
|
|
54
54
|
| { readonly name: "decision"; readonly project: string; readonly id: string }
|
|
55
|
-
/** The calendar of a project: the month shown, or this one when the address names none. */
|
|
56
|
-
| {
|
|
55
|
+
/** The calendar of a project: the month shown, or this one when the address names none, narrowed as the board is. */
|
|
56
|
+
| {
|
|
57
|
+
readonly name: "calendar";
|
|
58
|
+
readonly project: string;
|
|
59
|
+
readonly month: string | undefined;
|
|
60
|
+
readonly filter: BoardFilter;
|
|
61
|
+
}
|
|
57
62
|
| { readonly name: "roadmap"; readonly project: string }
|
|
58
63
|
| { readonly name: "new-goal"; readonly project: string }
|
|
59
64
|
| {
|
|
@@ -280,7 +285,7 @@ export function matchRoute(pathname: string, search = ""): Route {
|
|
|
280
285
|
if (month !== null && !isMonth(month)) {
|
|
281
286
|
return NOT_FOUND;
|
|
282
287
|
}
|
|
283
|
-
return { name: "calendar", project, month: month ?? undefined };
|
|
288
|
+
return { name: "calendar", project, month: month ?? undefined, filter: boardFilterOf(params) };
|
|
284
289
|
}
|
|
285
290
|
if (page === "roadmap") {
|
|
286
291
|
return { name: "roadmap", project };
|
|
@@ -307,6 +312,12 @@ export function projectHref(key: string, page?: ProjectPage): string {
|
|
|
307
312
|
|
|
308
313
|
/** Where a project's board is, narrowed as the filter says; the plain board for an empty one. */
|
|
309
314
|
export function boardHref(key: string, filter: BoardFilter = {}): string {
|
|
315
|
+
const parts = filterParts(filter);
|
|
316
|
+
return parts.length === 0 ? projectHref(key) : `${projectHref(key)}?${parts.join("&")}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The filter as the query carries it, a part per narrowing; nothing for the plain board. */
|
|
320
|
+
function filterParts(filter: BoardFilter): string[] {
|
|
310
321
|
const parts: string[] = [];
|
|
311
322
|
// An empty value means "none": it keeps its sign; "mine" is a flag and has no value.
|
|
312
323
|
if (filter.category !== undefined) {
|
|
@@ -321,7 +332,7 @@ export function boardHref(key: string, filter: BoardFilter = {}): string {
|
|
|
321
332
|
if (filter.mine === true) {
|
|
322
333
|
parts.push("mine");
|
|
323
334
|
}
|
|
324
|
-
return parts
|
|
335
|
+
return parts;
|
|
325
336
|
}
|
|
326
337
|
|
|
327
338
|
/** Where a task is written: the form. */
|
|
@@ -345,10 +356,11 @@ export function decisionHref(key: string, id: string): string {
|
|
|
345
356
|
return `${PATHS.project}/${key}/decisions/${id}`;
|
|
346
357
|
}
|
|
347
358
|
|
|
348
|
-
/** Where a project's calendar is, on a month, or on this one when none is named. */
|
|
349
|
-
export function calendarHref(key: string, month?: string): string {
|
|
359
|
+
/** Where a project's calendar is, on a month, or on this one when none is named, narrowed as the filter says. */
|
|
360
|
+
export function calendarHref(key: string, month?: string, filter: BoardFilter = {}): string {
|
|
350
361
|
const base = projectHref(key, "calendar");
|
|
351
|
-
|
|
362
|
+
const parts = [...(month === undefined ? [] : [`month=${month}`]), ...filterParts(filter)];
|
|
363
|
+
return parts.length === 0 ? base : `${base}?${parts.join("&")}`;
|
|
352
364
|
}
|
|
353
365
|
|
|
354
366
|
/** Where a goal is set: the form. */
|
package/src/routes.ts
CHANGED
|
@@ -114,3 +114,12 @@ export function projectPath(key: string, collection?: ProjectCollection, id?: st
|
|
|
114
114
|
? `${base}/${collection}`
|
|
115
115
|
: `${base}/${collection}/${encodeURIComponent(id)}`;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Where a project's own image for its mark is read, with the hash as a version where the image
|
|
120
|
+
* is known, so that a browser's copy is asked for again only when the image changes.
|
|
121
|
+
*/
|
|
122
|
+
export function projectIconPath(key: string, icon?: { readonly sha256: string } | null): string {
|
|
123
|
+
const base = `${projectPath(key)}/icon`;
|
|
124
|
+
return icon === undefined || icon === null ? base : `${base}?v=${icon.sha256.slice(0, 16)}`;
|
|
125
|
+
}
|
package/src/schemas.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
MAX_PROJECT_DESCRIPTION_LENGTH,
|
|
22
22
|
MAX_PROJECT_KEY_LENGTH,
|
|
23
23
|
MAX_PROJECT_NAME_LENGTH,
|
|
24
|
+
MAX_PROJECT_SYMBOL_LENGTH,
|
|
24
25
|
MAX_QUESTION_LENGTH,
|
|
25
26
|
MAX_SECRET_LENGTH,
|
|
26
27
|
MAX_SKILLS_ADDRESS_LENGTH,
|
|
@@ -35,10 +36,12 @@ import { hasForbiddenCodePoint } from "./text.js";
|
|
|
35
36
|
import {
|
|
36
37
|
ARTIFACT_KINDS,
|
|
37
38
|
CATEGORY_COLORS,
|
|
39
|
+
COLOR_NAMES,
|
|
38
40
|
EVENT_KINDS,
|
|
39
41
|
GOAL_STATES,
|
|
40
42
|
LINK_SOURCES,
|
|
41
43
|
PERSON_ROLES,
|
|
44
|
+
PROJECT_ICON_TYPES,
|
|
42
45
|
PROJECT_ROLES,
|
|
43
46
|
PROJECT_VISIBILITIES,
|
|
44
47
|
PROVIDER_KEYS,
|
|
@@ -160,7 +163,19 @@ const projectKey = z.string().check(
|
|
|
160
163
|
/** The address of a project's skills, as the standard spells one; the console checks it as one. */
|
|
161
164
|
const skillsAddress = line(MAX_SKILLS_ADDRESS_LENGTH);
|
|
162
165
|
|
|
166
|
+
/** A project's mark: a letter or two, or a symbol, with no space in it; shown on the project's colour. */
|
|
167
|
+
const projectSymbol = line(MAX_PROJECT_SYMBOL_LENGTH).check(
|
|
168
|
+
z.refine((value) => !/\s/.test(value), "must have no space in it"),
|
|
169
|
+
);
|
|
170
|
+
|
|
163
171
|
/** A project of the workspace, as the asker sees it: with what they are in it. */
|
|
172
|
+
/** A project's own image for its mark: the hash it is kept by, which `projectIconPath` reads it with, and its kind. */
|
|
173
|
+
export const restProjectIconSchema = z.object({
|
|
174
|
+
sha256: z.string().check(z.regex(/^[0-9a-f]{64}$/)),
|
|
175
|
+
contentType: z.enum(PROJECT_ICON_TYPES),
|
|
176
|
+
});
|
|
177
|
+
export type RestProjectIcon = z.infer<typeof restProjectIconSchema>;
|
|
178
|
+
|
|
164
179
|
export const restProjectSchema = z.object({
|
|
165
180
|
id: uuid,
|
|
166
181
|
key: projectKey,
|
|
@@ -170,6 +185,12 @@ export const restProjectSchema = z.object({
|
|
|
170
185
|
visibility: z.enum(PROJECT_VISIBILITIES),
|
|
171
186
|
/** The address of the project's skills at the SkillCDN deployment, canonical, or `null` for the organization's. */
|
|
172
187
|
skillsAddress: z.nullable(z.string()),
|
|
188
|
+
/** The colour of the project's mark, one of the names, or `null` for one chosen from its key. */
|
|
189
|
+
color: z.nullable(z.enum(COLOR_NAMES)),
|
|
190
|
+
/** The project's mark, a letter or two or a symbol, or `null` for the first letter of its name. */
|
|
191
|
+
symbol: z.nullable(z.string()),
|
|
192
|
+
/** The project's own image for its mark, shown in place of the symbol, or `null` for none. */
|
|
193
|
+
icon: z.nullable(restProjectIconSchema),
|
|
173
194
|
/** What the asker is in it: an owner configures the project, a member works on it. */
|
|
174
195
|
role: z.enum(PROJECT_ROLES),
|
|
175
196
|
/** How many decisions in it wait for a person. */
|
|
@@ -194,6 +215,9 @@ export const restProjectInputSchema = z.object({
|
|
|
194
215
|
visibility: z.optional(z.enum(PROJECT_VISIBILITIES)),
|
|
195
216
|
/** Left out or `null`: the organization's skills. */
|
|
196
217
|
skillsAddress: z.optional(z.nullable(skillsAddress)),
|
|
218
|
+
/** Left out or `null`: a colour chosen from the key, and the first letter of the name. */
|
|
219
|
+
color: z.optional(z.nullable(z.enum(COLOR_NAMES))),
|
|
220
|
+
symbol: z.optional(z.nullable(projectSymbol)),
|
|
197
221
|
});
|
|
198
222
|
export type RestProjectInput = z.infer<typeof restProjectInputSchema>;
|
|
199
223
|
|
|
@@ -203,6 +227,8 @@ export const restProjectPatchSchema = z.object({
|
|
|
203
227
|
description: z.optional(paragraphs(MAX_PROJECT_DESCRIPTION_LENGTH)),
|
|
204
228
|
visibility: z.optional(z.enum(PROJECT_VISIBILITIES)),
|
|
205
229
|
skillsAddress: z.optional(z.nullable(skillsAddress)),
|
|
230
|
+
color: z.optional(z.nullable(z.enum(COLOR_NAMES))),
|
|
231
|
+
symbol: z.optional(z.nullable(projectSymbol)),
|
|
206
232
|
});
|
|
207
233
|
export type RestProjectPatch = z.infer<typeof restProjectPatchSchema>;
|
|
208
234
|
|
|
@@ -313,8 +339,8 @@ export const restTaskSchema = z.object({
|
|
|
313
339
|
});
|
|
314
340
|
export type RestTask = z.infer<typeof restTaskSchema>;
|
|
315
341
|
|
|
316
|
-
/** `GET /api/v1/projects/<key>/tasks`: the board, newest first. */
|
|
317
|
-
export const restTasksSchema = z.object({ items: z.array(restTaskSchema) });
|
|
342
|
+
/** `GET /api/v1/projects/<key>/tasks`: a page of the board, newest first, and whether there is more after it. */
|
|
343
|
+
export const restTasksSchema = z.object({ items: z.array(restTaskSchema), more: z.boolean() });
|
|
318
344
|
export type RestTasks = z.infer<typeof restTasksSchema>;
|
|
319
345
|
|
|
320
346
|
const taskLinkInput = z.object({
|
|
@@ -449,12 +475,21 @@ export const restDecisionSchema = z.object({
|
|
|
449
475
|
});
|
|
450
476
|
export type RestDecision = z.infer<typeof restDecisionSchema>;
|
|
451
477
|
|
|
452
|
-
/** `GET /api/v1/projects/<key>/decisions`: the ones that wait first, newest first within each. */
|
|
453
|
-
export const restDecisionsSchema = z.object({
|
|
478
|
+
/** `GET /api/v1/projects/<key>/decisions`: a page, the ones that wait first, newest first within each, and whether there is more. */
|
|
479
|
+
export const restDecisionsSchema = z.object({
|
|
480
|
+
items: z.array(restDecisionSchema),
|
|
481
|
+
more: z.boolean(),
|
|
482
|
+
});
|
|
454
483
|
export type RestDecisions = z.infer<typeof restDecisionsSchema>;
|
|
455
484
|
|
|
456
|
-
/** The project a thing on a person's plate belongs to, named for the page to say and
|
|
457
|
-
const projectRef = z.object({
|
|
485
|
+
/** The project a thing on a person's plate belongs to, named and marked for the page to say, link and show it. */
|
|
486
|
+
const projectRef = z.object({
|
|
487
|
+
key: projectKey,
|
|
488
|
+
name: z.string(),
|
|
489
|
+
color: z.nullable(z.enum(COLOR_NAMES)),
|
|
490
|
+
symbol: z.nullable(z.string()),
|
|
491
|
+
icon: z.nullable(restProjectIconSchema),
|
|
492
|
+
});
|
|
458
493
|
|
|
459
494
|
/**
|
|
460
495
|
* `GET /api/v1/work`: what is on the asker's plate across the projects they may see (ADR-0019):
|
|
@@ -598,6 +633,11 @@ export const restRunSchema = z.object({
|
|
|
598
633
|
status: z.enum(RUN_STATUSES),
|
|
599
634
|
startedAt: instant,
|
|
600
635
|
endedAt: z.nullable(instant),
|
|
636
|
+
/**
|
|
637
|
+
* When the agent was last heard from on this run: when it began, reported, handed in or
|
|
638
|
+
* asked. The board cannot know whether an agent still runs; this is what it knows.
|
|
639
|
+
*/
|
|
640
|
+
lastHeardAt: instant,
|
|
601
641
|
/** What the agent said when the run ended, in Markdown, or `null`. */
|
|
602
642
|
summary: z.nullable(z.string()),
|
|
603
643
|
/** Oldest first. */
|
|
@@ -608,8 +648,8 @@ export const restRunSchema = z.object({
|
|
|
608
648
|
});
|
|
609
649
|
export type RestRun = z.infer<typeof restRunSchema>;
|
|
610
650
|
|
|
611
|
-
/** `GET /api/v1/projects/<key>/runs`: the runs, newest first. */
|
|
612
|
-
export const restRunsSchema = z.object({ items: z.array(restRunSchema) });
|
|
651
|
+
/** `GET /api/v1/projects/<key>/runs`: a page of the runs, newest first, and whether there is more. */
|
|
652
|
+
export const restRunsSchema = z.object({ items: z.array(restRunSchema), more: z.boolean() });
|
|
613
653
|
export type RestRuns = z.infer<typeof restRunsSchema>;
|
|
614
654
|
|
|
615
655
|
/** `GET .../events?after=`: what happened after that number, oldest first. */
|