@skillcdn/console 0.1.9 → 0.1.10
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 -5
- package/dist/api.d.ts +5 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +255 -10
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +9 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +80 -0
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +62 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +31 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -3
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +27 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +75 -7
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +34 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +96 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/categories.d.ts +13 -0
- package/dist/components/categories.d.ts.map +1 -0
- package/dist/components/categories.js +52 -0
- package/dist/components/categories.js.map +1 -0
- package/dist/components/decision-list.d.ts +10 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +14 -1
- 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 +12 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +35 -0
- package/dist/components/goals.d.ts.map +1 -0
- package/dist/components/goals.js +45 -0
- package/dist/components/goals.js.map +1 -0
- package/dist/components/task-form.d.ts +5 -1
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +13 -1
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.d.ts +16 -0
- package/dist/components/task-list.d.ts.map +1 -0
- package/dist/components/task-list.js +14 -0
- package/dist/components/task-list.js.map +1 -0
- package/dist/components/task-view.d.ts +9 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +10 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +30 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +60 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +10 -0
- package/dist/components/work.d.ts.map +1 -0
- package/dist/components/work.js +29 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +511 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +124 -8
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +36 -2
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +39 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/dates.js +120 -0
- package/dist/dates.js.map +1 -0
- package/dist/i18n/en.d.ts +162 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +174 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +169 -1
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +5 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +5 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +50 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +74 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +7 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +9 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +366 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +112 -3
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +15 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +26 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +11 -4
- package/docs/specs/rest.md +33 -8
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +30 -0
- package/src/cli/cli.ts +311 -10
- package/src/cli/format.ts +88 -0
- package/src/cli/help.ts +69 -7
- package/src/client.ts +70 -2
- package/src/components/board.tsx +205 -12
- package/src/components/calendar.tsx +255 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +62 -5
- package/src/components/event-feed.tsx +12 -0
- package/src/components/goals.tsx +264 -0
- package/src/components/task-form.tsx +78 -1
- package/src/components/task-list.tsx +61 -0
- package/src/components/task-view.tsx +74 -3
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +83 -0
- package/src/console.tsx +337 -4
- package/src/data.ts +63 -8
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +184 -1
- package/src/i18n/ko.ts +175 -1
- package/src/index.ts +58 -1
- package/src/limits.ts +5 -0
- package/src/router.ts +119 -6
- package/src/routes.ts +10 -0
- package/src/schemas.ts +141 -1
- package/src/styles/console.css +511 -0
- package/src/vocabulary.ts +33 -0
package/src/index.ts
CHANGED
|
@@ -4,7 +4,24 @@
|
|
|
4
4
|
// a script that wants none of the pages. The styles are `@skillcdn/console/console.css`.
|
|
5
5
|
|
|
6
6
|
export * from "./api.js";
|
|
7
|
-
export {
|
|
7
|
+
export {
|
|
8
|
+
Board,
|
|
9
|
+
BoardFilters,
|
|
10
|
+
type BoardFiltersProps,
|
|
11
|
+
type BoardProps,
|
|
12
|
+
byDue,
|
|
13
|
+
filterTasks,
|
|
14
|
+
TaskCard,
|
|
15
|
+
} from "./components/board.js";
|
|
16
|
+
export {
|
|
17
|
+
Calendar,
|
|
18
|
+
type CalendarProps,
|
|
19
|
+
Workload,
|
|
20
|
+
type WorkloadProps,
|
|
21
|
+
type WorkloadRow,
|
|
22
|
+
workloadOf,
|
|
23
|
+
} from "./components/calendar.js";
|
|
24
|
+
export { CategoryList, type CategoryListProps } from "./components/categories.js";
|
|
8
25
|
export {
|
|
9
26
|
ConnectApproval,
|
|
10
27
|
type ConnectApprovalProps,
|
|
@@ -19,6 +36,8 @@ export {
|
|
|
19
36
|
type DecisionCardProps,
|
|
20
37
|
DecisionList,
|
|
21
38
|
type DecisionListProps,
|
|
39
|
+
DecisionLog,
|
|
40
|
+
type DecisionLogProps,
|
|
22
41
|
} from "./components/decision-list.js";
|
|
23
42
|
export {
|
|
24
43
|
DocumentCrumbs,
|
|
@@ -36,6 +55,14 @@ export {
|
|
|
36
55
|
type FolderViewProps,
|
|
37
56
|
} from "./components/documents.js";
|
|
38
57
|
export { describeEvent, EventFeed, type EventFeedProps } from "./components/event-feed.js";
|
|
58
|
+
export {
|
|
59
|
+
GoalForm,
|
|
60
|
+
type GoalFormProps,
|
|
61
|
+
GoalList,
|
|
62
|
+
type GoalListProps,
|
|
63
|
+
GoalView,
|
|
64
|
+
type GoalViewProps,
|
|
65
|
+
} from "./components/goals.js";
|
|
39
66
|
export { Markdown, type MarkdownProps } from "./components/markdown.js";
|
|
40
67
|
export { PeopleList, type PeopleListProps } from "./components/people.js";
|
|
41
68
|
export {
|
|
@@ -69,6 +96,7 @@ export {
|
|
|
69
96
|
export { SignIn, type SignInProps } from "./components/sign-in.js";
|
|
70
97
|
export { SkillList, type SkillListProps } from "./components/skills.js";
|
|
71
98
|
export { TaskForm, type TaskFormProps } from "./components/task-form.js";
|
|
99
|
+
export { TaskList, type TaskListProps } from "./components/task-list.js";
|
|
72
100
|
export { TaskView, type TaskViewProps } from "./components/task-view.js";
|
|
73
101
|
export {
|
|
74
102
|
NewToken,
|
|
@@ -83,13 +111,18 @@ export {
|
|
|
83
111
|
Badge,
|
|
84
112
|
Button,
|
|
85
113
|
Callout,
|
|
114
|
+
CategoryBadge,
|
|
86
115
|
cx,
|
|
116
|
+
Day,
|
|
117
|
+
DueBadge,
|
|
87
118
|
EmptyState,
|
|
88
119
|
formatBytes,
|
|
89
120
|
formatInstant,
|
|
121
|
+
GoalStateBadge,
|
|
90
122
|
PersonChip,
|
|
91
123
|
PRIORITY_LABELS,
|
|
92
124
|
PriorityBadge,
|
|
125
|
+
ProgressBar,
|
|
93
126
|
ROLE_LABELS,
|
|
94
127
|
RoleBadge,
|
|
95
128
|
Spinner,
|
|
@@ -97,6 +130,7 @@ export {
|
|
|
97
130
|
StateBadge,
|
|
98
131
|
Time,
|
|
99
132
|
} from "./components/ui.js";
|
|
133
|
+
export { WorkList, type WorkListProps } from "./components/work.js";
|
|
100
134
|
export { ConnectPage, type ConnectPageProps } from "./connect-page.js";
|
|
101
135
|
export {
|
|
102
136
|
type ConsoleApp,
|
|
@@ -106,6 +140,24 @@ export {
|
|
|
106
140
|
DEFAULT_COMPONENTS,
|
|
107
141
|
} from "./console.js";
|
|
108
142
|
export { type ConsoleActions, type ConsoleData, useConsoleData } from "./data.js";
|
|
143
|
+
export {
|
|
144
|
+
addDays,
|
|
145
|
+
addMonths,
|
|
146
|
+
dateOf,
|
|
147
|
+
dayOf,
|
|
148
|
+
daysBetween,
|
|
149
|
+
firstDayOfWeek,
|
|
150
|
+
formatDay,
|
|
151
|
+
formatMonth,
|
|
152
|
+
formatWeekday,
|
|
153
|
+
type GridDay,
|
|
154
|
+
isDay,
|
|
155
|
+
isMonth,
|
|
156
|
+
monthGrid,
|
|
157
|
+
monthOf,
|
|
158
|
+
today,
|
|
159
|
+
weekdays,
|
|
160
|
+
} from "./dates.js";
|
|
109
161
|
export { DocsPage, type DocsPageProps } from "./docs-page.js";
|
|
110
162
|
export {
|
|
111
163
|
chooseLanguage,
|
|
@@ -120,14 +172,19 @@ export {
|
|
|
120
172
|
useWords,
|
|
121
173
|
} from "./i18n/index.js";
|
|
122
174
|
export {
|
|
175
|
+
type BoardFilter,
|
|
176
|
+
boardHref,
|
|
177
|
+
calendarHref,
|
|
123
178
|
connectHref,
|
|
124
179
|
type DocsView,
|
|
125
180
|
decisionHref,
|
|
126
181
|
docHref,
|
|
182
|
+
goalHref,
|
|
127
183
|
matchRoute,
|
|
128
184
|
movedFrom,
|
|
129
185
|
type Navigation,
|
|
130
186
|
newDecisionHref,
|
|
187
|
+
newGoalHref,
|
|
131
188
|
newTaskHref,
|
|
132
189
|
PATHS,
|
|
133
190
|
type ProjectPage,
|
package/src/limits.ts
CHANGED
|
@@ -59,3 +59,8 @@ export const MAX_VERSIONS_PER_DOCUMENT = 500;
|
|
|
59
59
|
export const MAX_FILES_PER_DOCUMENT = 50;
|
|
60
60
|
/** The words a search of the documents takes. */
|
|
61
61
|
export const MAX_SEARCH_LENGTH = 200;
|
|
62
|
+
/** A category of tasks (ADR-0019): its key, what the command and filters say, and its name. */
|
|
63
|
+
export const MAX_CATEGORY_KEY_LENGTH = 40;
|
|
64
|
+
export const MAX_CATEGORY_NAME_LENGTH = 60;
|
|
65
|
+
/** How many categories one workspace may have: a vocabulary, not a tag cloud. */
|
|
66
|
+
export const MAX_CATEGORIES = 50;
|
package/src/router.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { isFolderPath, isSignInFailure, SIGN_IN_PARAM, type SignInFailure } from "./api.js";
|
|
2
|
+
import { isMonth } from "./dates.js";
|
|
2
3
|
|
|
3
4
|
// The addresses of the default console (ADR-0010): the router is a function from a URL to a
|
|
4
5
|
// route, and the hrefs below are its inverse. A project's pages live under `/projects/<key>`,
|
|
5
6
|
// the word the REST API uses; an address names what it shows (a task by its number, a decision
|
|
6
|
-
// by its id, a document by its path), every form has one, and the query says
|
|
7
|
-
// shown (`?edit`, `?ask`, `?new`, `?version=`, `?q
|
|
7
|
+
// by its id, a document by its path, a goal by its id), every form has one, and the query says
|
|
8
|
+
// how a page is shown (`?edit`, `?ask`, `?new`, `?version=`, `?q=`, `?month=`, and the board's
|
|
9
|
+
// filters). The old addresses are taken to the new.
|
|
8
10
|
|
|
9
11
|
/** How a task's page is shown: as it is, or with one of its forms open. */
|
|
10
12
|
export type TaskFormKind = "edit" | "ask";
|
|
@@ -16,10 +18,22 @@ export type DocsView =
|
|
|
16
18
|
| { readonly kind: "edit" }
|
|
17
19
|
| { readonly kind: "search"; readonly q: string };
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* What the board is narrowed to (ADR-0019), from the query: a category by its key (`""` for
|
|
23
|
+
* the tasks with none), a goal by its id (`""` for the tasks with none), a person by their id,
|
|
24
|
+
* or the person's own.
|
|
25
|
+
*/
|
|
26
|
+
export interface BoardFilter {
|
|
27
|
+
readonly category?: string | undefined;
|
|
28
|
+
readonly goal?: string | undefined;
|
|
29
|
+
readonly person?: string | undefined;
|
|
30
|
+
readonly mine?: boolean | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
export type Route =
|
|
20
34
|
| { readonly name: "projects" }
|
|
21
35
|
| { readonly name: "new-project" }
|
|
22
|
-
| { readonly name: "board"; readonly project: string }
|
|
36
|
+
| { readonly name: "board"; readonly project: string; readonly filter: BoardFilter }
|
|
23
37
|
| { readonly name: "new-task"; readonly project: string }
|
|
24
38
|
| {
|
|
25
39
|
readonly name: "task";
|
|
@@ -38,6 +52,16 @@ export type Route =
|
|
|
38
52
|
| { readonly name: "decisions"; readonly project: string }
|
|
39
53
|
| { readonly name: "new-decision"; readonly project: string }
|
|
40
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
|
+
| { readonly name: "calendar"; readonly project: string; readonly month: string | undefined }
|
|
57
|
+
| { readonly name: "roadmap"; readonly project: string }
|
|
58
|
+
| { readonly name: "new-goal"; readonly project: string }
|
|
59
|
+
| {
|
|
60
|
+
readonly name: "goal";
|
|
61
|
+
readonly project: string;
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly form: "edit" | undefined;
|
|
64
|
+
}
|
|
41
65
|
| { readonly name: "feed"; readonly project: string }
|
|
42
66
|
| { readonly name: "skills"; readonly project: string }
|
|
43
67
|
| { readonly name: "members"; readonly project: string }
|
|
@@ -47,6 +71,10 @@ export type Route =
|
|
|
47
71
|
| { readonly name: "person-agents"; readonly id: string }
|
|
48
72
|
| { readonly name: "agents" }
|
|
49
73
|
| { readonly name: "new-token" }
|
|
74
|
+
/** What is on the person's plate across their projects. */
|
|
75
|
+
| { readonly name: "work" }
|
|
76
|
+
/** The categories tasks are sorted into; an administrator keeps them here. */
|
|
77
|
+
| { readonly name: "categories" }
|
|
50
78
|
/** Where a person approves an agent's connection, by the code it showed, or types one. */
|
|
51
79
|
| { readonly name: "connect"; readonly code: string | undefined }
|
|
52
80
|
/** An address that moved: the page takes the person to `to` in its place. */
|
|
@@ -54,7 +82,15 @@ export type Route =
|
|
|
54
82
|
| { readonly name: "not-found" };
|
|
55
83
|
|
|
56
84
|
/** The pages of a project, after its key. */
|
|
57
|
-
export type ProjectPage =
|
|
85
|
+
export type ProjectPage =
|
|
86
|
+
| "docs"
|
|
87
|
+
| "decisions"
|
|
88
|
+
| "calendar"
|
|
89
|
+
| "roadmap"
|
|
90
|
+
| "feed"
|
|
91
|
+
| "skills"
|
|
92
|
+
| "members"
|
|
93
|
+
| "settings";
|
|
58
94
|
|
|
59
95
|
export const PATHS = {
|
|
60
96
|
/** The projects the person may see: the front page. */
|
|
@@ -62,11 +98,15 @@ export const PATHS = {
|
|
|
62
98
|
/** Where a project's pages are: `/projects/<key>`, then the page. */
|
|
63
99
|
project: "/projects",
|
|
64
100
|
newProject: "/projects/new",
|
|
101
|
+
/** What is on the person's plate, across the projects they may see. */
|
|
102
|
+
work: "/work",
|
|
65
103
|
people: "/people",
|
|
66
104
|
/** The agents of whoever is signed in, which hold their tokens: their own page. */
|
|
67
105
|
agents: "/agents",
|
|
68
106
|
/** The form for a token made by hand, for a script or a console of one's own. */
|
|
69
107
|
newToken: "/agents/new",
|
|
108
|
+
/** The categories tasks are sorted into, kept by an administrator. */
|
|
109
|
+
categories: "/categories",
|
|
70
110
|
/** Where an agent's connection is approved (ADR-0011): `/connect/<code>`, or the code typed. */
|
|
71
111
|
connect: "/connect",
|
|
72
112
|
} as const;
|
|
@@ -77,7 +117,7 @@ const NUMBER = /^[1-9][0-9]{0,8}$/;
|
|
|
77
117
|
/** A code as it may be typed into an address: checked for what it is by the page. */
|
|
78
118
|
const CODE_TEXT = /^[A-Za-z0-9-]{1,16}$/;
|
|
79
119
|
/** The pages of a project that take nothing after their name. */
|
|
80
|
-
const PLAIN_PAGES: readonly Exclude<ProjectPage, "docs">[] = [
|
|
120
|
+
const PLAIN_PAGES: readonly Exclude<ProjectPage, "docs" | "calendar" | "roadmap">[] = [
|
|
81
121
|
"decisions",
|
|
82
122
|
"feed",
|
|
83
123
|
"skills",
|
|
@@ -103,6 +143,19 @@ export function movedFrom(pathname: string): string | undefined {
|
|
|
103
143
|
const taskFormOf = (params: URLSearchParams): TaskFormKind | undefined =>
|
|
104
144
|
params.has("edit") ? "edit" : params.has("ask") ? "ask" : undefined;
|
|
105
145
|
|
|
146
|
+
/** The board's filters, as the query names them; a value that is not one is left out. */
|
|
147
|
+
const boardFilterOf = (params: URLSearchParams): BoardFilter => {
|
|
148
|
+
const category = params.get("category");
|
|
149
|
+
const goal = params.get("goal");
|
|
150
|
+
const person = params.get("person");
|
|
151
|
+
return {
|
|
152
|
+
...(category !== null && (category === "" || PROJECT_KEY.test(category)) ? { category } : {}),
|
|
153
|
+
...(goal !== null && (goal === "" || UUID.test(goal)) ? { goal } : {}),
|
|
154
|
+
...(person !== null && UUID.test(person) ? { person } : {}),
|
|
155
|
+
...(params.has("mine") ? { mine: true } : {}),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
106
159
|
/** The view the query asks for at a path, or nothing for one the path cannot be shown as. */
|
|
107
160
|
const docsViewOf = (params: URLSearchParams, path: string): DocsView | undefined => {
|
|
108
161
|
const q = params.get("q");
|
|
@@ -137,6 +190,9 @@ export function matchRoute(pathname: string, search = ""): Route {
|
|
|
137
190
|
if (path === PATHS.newProject) {
|
|
138
191
|
return { name: "new-project" };
|
|
139
192
|
}
|
|
193
|
+
if (path === PATHS.work) {
|
|
194
|
+
return { name: "work" };
|
|
195
|
+
}
|
|
140
196
|
if (path === PATHS.people) {
|
|
141
197
|
return { name: "people" };
|
|
142
198
|
}
|
|
@@ -146,6 +202,9 @@ export function matchRoute(pathname: string, search = ""): Route {
|
|
|
146
202
|
if (path === PATHS.newToken) {
|
|
147
203
|
return { name: "new-token" };
|
|
148
204
|
}
|
|
205
|
+
if (path === PATHS.categories) {
|
|
206
|
+
return { name: "categories" };
|
|
207
|
+
}
|
|
149
208
|
if (path === PATHS.connect) {
|
|
150
209
|
return { name: "connect", code: undefined };
|
|
151
210
|
}
|
|
@@ -167,7 +226,7 @@ export function matchRoute(pathname: string, search = ""): Route {
|
|
|
167
226
|
return NOT_FOUND;
|
|
168
227
|
}
|
|
169
228
|
if (page === undefined) {
|
|
170
|
-
return { name: "board", project };
|
|
229
|
+
return { name: "board", project, filter: boardFilterOf(params) };
|
|
171
230
|
}
|
|
172
231
|
if (page === "docs") {
|
|
173
232
|
// The rest of the path is the document's, or a folder's, or nothing for the root.
|
|
@@ -205,9 +264,27 @@ export function matchRoute(pathname: string, search = ""): Route {
|
|
|
205
264
|
}
|
|
206
265
|
return UUID.test(third) ? { name: "decision", project, id: third } : NOT_FOUND;
|
|
207
266
|
}
|
|
267
|
+
if (page === "roadmap" && third !== undefined) {
|
|
268
|
+
if (third === "new") {
|
|
269
|
+
return { name: "new-goal", project };
|
|
270
|
+
}
|
|
271
|
+
return UUID.test(third)
|
|
272
|
+
? { name: "goal", project, id: third, form: params.has("edit") ? "edit" : undefined }
|
|
273
|
+
: NOT_FOUND;
|
|
274
|
+
}
|
|
208
275
|
if (third !== undefined) {
|
|
209
276
|
return NOT_FOUND;
|
|
210
277
|
}
|
|
278
|
+
if (page === "calendar") {
|
|
279
|
+
const month = params.get("month");
|
|
280
|
+
if (month !== null && !isMonth(month)) {
|
|
281
|
+
return NOT_FOUND;
|
|
282
|
+
}
|
|
283
|
+
return { name: "calendar", project, month: month ?? undefined };
|
|
284
|
+
}
|
|
285
|
+
if (page === "roadmap") {
|
|
286
|
+
return { name: "roadmap", project };
|
|
287
|
+
}
|
|
211
288
|
const found = PLAIN_PAGES.find((candidate) => candidate === page);
|
|
212
289
|
return found === undefined ? NOT_FOUND : { name: found, project };
|
|
213
290
|
}
|
|
@@ -228,6 +305,25 @@ export function projectHref(key: string, page?: ProjectPage): string {
|
|
|
228
305
|
return page === undefined ? base : `${base}/${page}`;
|
|
229
306
|
}
|
|
230
307
|
|
|
308
|
+
/** Where a project's board is, narrowed as the filter says; the plain board for an empty one. */
|
|
309
|
+
export function boardHref(key: string, filter: BoardFilter = {}): string {
|
|
310
|
+
const parts: string[] = [];
|
|
311
|
+
// An empty value means "none": it keeps its sign; "mine" is a flag and has no value.
|
|
312
|
+
if (filter.category !== undefined) {
|
|
313
|
+
parts.push(`category=${encodeURIComponent(filter.category)}`);
|
|
314
|
+
}
|
|
315
|
+
if (filter.goal !== undefined) {
|
|
316
|
+
parts.push(`goal=${encodeURIComponent(filter.goal)}`);
|
|
317
|
+
}
|
|
318
|
+
if (filter.person !== undefined) {
|
|
319
|
+
parts.push(`person=${encodeURIComponent(filter.person)}`);
|
|
320
|
+
}
|
|
321
|
+
if (filter.mine === true) {
|
|
322
|
+
parts.push("mine");
|
|
323
|
+
}
|
|
324
|
+
return parts.length === 0 ? projectHref(key) : `${projectHref(key)}?${parts.join("&")}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
231
327
|
/** Where a task is written: the form. */
|
|
232
328
|
export function newTaskHref(key: string): string {
|
|
233
329
|
return `${PATHS.project}/${key}/tasks/new`;
|
|
@@ -249,6 +345,23 @@ export function decisionHref(key: string, id: string): string {
|
|
|
249
345
|
return `${PATHS.project}/${key}/decisions/${id}`;
|
|
250
346
|
}
|
|
251
347
|
|
|
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 {
|
|
350
|
+
const base = projectHref(key, "calendar");
|
|
351
|
+
return month === undefined ? base : `${base}?month=${month}`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Where a goal is set: the form. */
|
|
355
|
+
export function newGoalHref(key: string): string {
|
|
356
|
+
return `${PATHS.project}/${key}/roadmap/new`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Where one goal is read, or changed. */
|
|
360
|
+
export function goalHref(key: string, id: string, form?: "edit"): string {
|
|
361
|
+
const base = `${PATHS.project}/${key}/roadmap/${id}`;
|
|
362
|
+
return form === undefined ? base : `${base}?${form}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
252
365
|
/**
|
|
253
366
|
* Where a document of a project is read, or a folder of them listed (`""` for the root), and
|
|
254
367
|
* how: a form, a version, or a search over all of them.
|
package/src/routes.ts
CHANGED
|
@@ -17,8 +17,17 @@ export const REST_ROUTES = {
|
|
|
17
17
|
tokens: "/api/v1/tokens",
|
|
18
18
|
/** Connecting an agent (ADR-0011): begun as nobody, approved by a person, claimed by the command. */
|
|
19
19
|
connect: "/api/v1/connect",
|
|
20
|
+
/** The categories tasks are sorted into (ADR-0019): the workspace's vocabulary, kept by an administrator. */
|
|
21
|
+
categories: "/api/v1/categories",
|
|
22
|
+
/** What is on the asker's plate across the projects they may see: their open tasks, and the decisions waiting. */
|
|
23
|
+
work: "/api/v1/work",
|
|
20
24
|
} as const;
|
|
21
25
|
|
|
26
|
+
/** The REST path of one category, by its key. */
|
|
27
|
+
export function categoryPath(key: string): string {
|
|
28
|
+
return `${REST_ROUTES.categories}/${encodeURIComponent(key)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
/** Where the command claims the token of a connection, with its secret. */
|
|
23
32
|
export const CLAIM_PATH = `${REST_ROUTES.connect}/claim`;
|
|
24
33
|
|
|
@@ -44,6 +53,7 @@ export const PROJECT_COLLECTIONS = [
|
|
|
44
53
|
"skills",
|
|
45
54
|
"members",
|
|
46
55
|
"docs",
|
|
56
|
+
"goals",
|
|
47
57
|
] as const;
|
|
48
58
|
|
|
49
59
|
export type ProjectCollection = (typeof PROJECT_COLLECTIONS)[number];
|
package/src/schemas.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { isDocumentPath } from "./documents.js";
|
|
|
7
7
|
import {
|
|
8
8
|
MAX_AGENT_LENGTH,
|
|
9
9
|
MAX_BODY_LENGTH,
|
|
10
|
+
MAX_CATEGORY_KEY_LENGTH,
|
|
11
|
+
MAX_CATEGORY_NAME_LENGTH,
|
|
10
12
|
MAX_DOCUMENT_LENGTH,
|
|
11
13
|
MAX_DOCUMENT_PATH_LENGTH,
|
|
12
14
|
MAX_FILE_NAME_LENGTH,
|
|
@@ -32,7 +34,9 @@ import {
|
|
|
32
34
|
import { hasForbiddenCodePoint } from "./text.js";
|
|
33
35
|
import {
|
|
34
36
|
ARTIFACT_KINDS,
|
|
37
|
+
CATEGORY_COLORS,
|
|
35
38
|
EVENT_KINDS,
|
|
39
|
+
GOAL_STATES,
|
|
36
40
|
LINK_SOURCES,
|
|
37
41
|
PERSON_ROLES,
|
|
38
42
|
PROJECT_ROLES,
|
|
@@ -70,6 +74,8 @@ const url = z.string().check(z.trim(), z.maxLength(MAX_URL_LENGTH), z.url({ prot
|
|
|
70
74
|
const uuid = z.uuid();
|
|
71
75
|
/** An ISO 8601 instant, as the server writes it. */
|
|
72
76
|
const instant = z.iso.datetime();
|
|
77
|
+
/** A calendar day (`2026-10-20`): a plan is made in days, wherever the people are (ADR-0019). */
|
|
78
|
+
const day = z.iso.date();
|
|
73
79
|
const count = z.int().check(z.nonnegative());
|
|
74
80
|
|
|
75
81
|
/** A person of the workspace, as the pages show them. Nothing here is a secret. */
|
|
@@ -230,6 +236,48 @@ export const restTaskLinkSchema = z.object({
|
|
|
230
236
|
});
|
|
231
237
|
export type RestTaskLink = z.infer<typeof restTaskLinkSchema>;
|
|
232
238
|
|
|
239
|
+
/**
|
|
240
|
+
* A category's key: what the command and the filters say, as a project's key is. Lowercase
|
|
241
|
+
* letters, digits and hyphens; immutable once the category is made.
|
|
242
|
+
*/
|
|
243
|
+
const categoryKey = z
|
|
244
|
+
.string()
|
|
245
|
+
.check(
|
|
246
|
+
z.maxLength(MAX_CATEGORY_KEY_LENGTH),
|
|
247
|
+
z.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, "must be lowercase letters, digits and hyphens"),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
/** A category tasks are sorted into (ADR-0019): the workspace's own word for a kind of work. */
|
|
251
|
+
export const restCategorySchema = z.object({
|
|
252
|
+
key: categoryKey,
|
|
253
|
+
name: z.string(),
|
|
254
|
+
/** One of the names the pages paint a category in. */
|
|
255
|
+
color: z.enum(CATEGORY_COLORS),
|
|
256
|
+
createdAt: instant,
|
|
257
|
+
updatedAt: instant,
|
|
258
|
+
});
|
|
259
|
+
export type RestCategory = z.infer<typeof restCategorySchema>;
|
|
260
|
+
|
|
261
|
+
/** `GET /api/v1/categories`: the workspace's categories, by name. */
|
|
262
|
+
export const restCategoriesSchema = z.object({ items: z.array(restCategorySchema) });
|
|
263
|
+
export type RestCategories = z.infer<typeof restCategoriesSchema>;
|
|
264
|
+
|
|
265
|
+
/** What `POST /api/v1/categories` is sent, by an administrator: the key, the name, and a colour or the plain one. */
|
|
266
|
+
export const restCategoryInputSchema = z.object({
|
|
267
|
+
key: categoryKey,
|
|
268
|
+
name: line(MAX_CATEGORY_NAME_LENGTH),
|
|
269
|
+
/** Left out: `gray`. */
|
|
270
|
+
color: z.optional(z.enum(CATEGORY_COLORS)),
|
|
271
|
+
});
|
|
272
|
+
export type RestCategoryInput = z.infer<typeof restCategoryInputSchema>;
|
|
273
|
+
|
|
274
|
+
/** What `PATCH /api/v1/categories/<key>` is sent: only what changes; the key does not. */
|
|
275
|
+
export const restCategoryPatchSchema = z.object({
|
|
276
|
+
name: z.optional(line(MAX_CATEGORY_NAME_LENGTH)),
|
|
277
|
+
color: z.optional(z.enum(CATEGORY_COLORS)),
|
|
278
|
+
});
|
|
279
|
+
export type RestCategoryPatch = z.infer<typeof restCategoryPatchSchema>;
|
|
280
|
+
|
|
233
281
|
export const restTaskSchema = z.object({
|
|
234
282
|
id: uuid,
|
|
235
283
|
/** The task's number in its project, the one people say out loud. */
|
|
@@ -243,6 +291,18 @@ export const restTaskSchema = z.object({
|
|
|
243
291
|
assignee: z.nullable(restPersonSchema),
|
|
244
292
|
/** The task this one is part of, or `null`. */
|
|
245
293
|
parentId: z.nullable(uuid),
|
|
294
|
+
/** The category the task is sorted into, as the workspace keeps it, or `null`. */
|
|
295
|
+
category: z.nullable(
|
|
296
|
+
z.object({ key: z.string(), name: z.string(), color: z.enum(CATEGORY_COLORS) }),
|
|
297
|
+
),
|
|
298
|
+
/** The goal of the project's roadmap the task works towards, or `null`. */
|
|
299
|
+
goalId: z.nullable(uuid),
|
|
300
|
+
/** The plan (ADR-0019): the day the work is meant to begin, and the day it is due, each or `null`. */
|
|
301
|
+
startsOn: z.nullable(day),
|
|
302
|
+
dueOn: z.nullable(day),
|
|
303
|
+
/** What happened: when the task was first moved past ready, and when it was moved to done; each or `null`. The board keeps both. */
|
|
304
|
+
startedAt: z.nullable(instant),
|
|
305
|
+
doneAt: z.nullable(instant),
|
|
246
306
|
links: z.array(restTaskLinkSchema),
|
|
247
307
|
/** How many decisions about this task wait for a person. */
|
|
248
308
|
openDecisions: count,
|
|
@@ -270,6 +330,13 @@ export const restTaskInputSchema = z.object({
|
|
|
270
330
|
priority: z.optional(z.enum(TASK_PRIORITIES)),
|
|
271
331
|
assigneeId: z.optional(z.nullable(uuid)),
|
|
272
332
|
parentId: z.optional(z.nullable(uuid)),
|
|
333
|
+
/** The key of a category of the workspace, or `null` for none. */
|
|
334
|
+
category: z.optional(z.nullable(categoryKey)),
|
|
335
|
+
/** A goal of the project, or `null` for none. */
|
|
336
|
+
goalId: z.optional(z.nullable(uuid)),
|
|
337
|
+
/** The plan, in days; the due day is not before the start. */
|
|
338
|
+
startsOn: z.optional(z.nullable(day)),
|
|
339
|
+
dueOn: z.optional(z.nullable(day)),
|
|
273
340
|
links: z.optional(z.array(taskLinkInput).check(z.maxLength(MAX_LINKS))),
|
|
274
341
|
});
|
|
275
342
|
export type RestTaskInput = z.infer<typeof restTaskInputSchema>;
|
|
@@ -282,10 +349,68 @@ export const restTaskPatchSchema = z.object({
|
|
|
282
349
|
priority: z.optional(z.enum(TASK_PRIORITIES)),
|
|
283
350
|
assigneeId: z.optional(z.nullable(uuid)),
|
|
284
351
|
parentId: z.optional(z.nullable(uuid)),
|
|
352
|
+
category: z.optional(z.nullable(categoryKey)),
|
|
353
|
+
goalId: z.optional(z.nullable(uuid)),
|
|
354
|
+
startsOn: z.optional(z.nullable(day)),
|
|
355
|
+
dueOn: z.optional(z.nullable(day)),
|
|
285
356
|
links: z.optional(z.array(taskLinkInput).check(z.maxLength(MAX_LINKS))),
|
|
286
357
|
});
|
|
287
358
|
export type RestTaskPatch = z.infer<typeof restTaskPatchSchema>;
|
|
288
359
|
|
|
360
|
+
/** How many tasks a goal has in each state: what its progress is read from, live. */
|
|
361
|
+
export const restTaskCountsSchema = z.object({
|
|
362
|
+
idea: count,
|
|
363
|
+
ready: count,
|
|
364
|
+
in_progress: count,
|
|
365
|
+
in_review: count,
|
|
366
|
+
done: count,
|
|
367
|
+
dropped: count,
|
|
368
|
+
});
|
|
369
|
+
export type RestTaskCounts = z.infer<typeof restTaskCountsSchema>;
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* A goal of a project's roadmap (ADR-0019): what the project works towards, with the day it is
|
|
373
|
+
* meant to be reached by, and how far the tasks under it are.
|
|
374
|
+
*/
|
|
375
|
+
export const restGoalSchema = z.object({
|
|
376
|
+
id: uuid,
|
|
377
|
+
title: z.string(),
|
|
378
|
+
/** Markdown: what reaching the goal means. Shown as text or rendered to elements, never as HTML. */
|
|
379
|
+
body: z.string(),
|
|
380
|
+
state: z.enum(GOAL_STATES),
|
|
381
|
+
/** The day the goal is meant to be reached by, or `null`. */
|
|
382
|
+
dueOn: z.nullable(day),
|
|
383
|
+
createdBy: restPersonSchema,
|
|
384
|
+
/** The tasks that work towards it, counted by state. */
|
|
385
|
+
taskCounts: restTaskCountsSchema,
|
|
386
|
+
createdAt: instant,
|
|
387
|
+
updatedAt: instant,
|
|
388
|
+
});
|
|
389
|
+
export type RestGoal = z.infer<typeof restGoalSchema>;
|
|
390
|
+
|
|
391
|
+
/** `GET /api/v1/projects/<key>/goals`: the roadmap, the active goals first. */
|
|
392
|
+
export const restGoalsSchema = z.object({ items: z.array(restGoalSchema) });
|
|
393
|
+
export type RestGoals = z.infer<typeof restGoalsSchema>;
|
|
394
|
+
|
|
395
|
+
/** What `POST /api/v1/projects/<key>/goals` is sent: the title, and the rest with defaults. */
|
|
396
|
+
export const restGoalInputSchema = z.object({
|
|
397
|
+
title: line(MAX_TITLE_LENGTH),
|
|
398
|
+
body: z.optional(body),
|
|
399
|
+
/** Left out: `planned`. */
|
|
400
|
+
state: z.optional(z.enum(GOAL_STATES)),
|
|
401
|
+
dueOn: z.optional(z.nullable(day)),
|
|
402
|
+
});
|
|
403
|
+
export type RestGoalInput = z.infer<typeof restGoalInputSchema>;
|
|
404
|
+
|
|
405
|
+
/** What `PATCH /api/v1/projects/<key>/goals/<id>` is sent: only what changes. */
|
|
406
|
+
export const restGoalPatchSchema = z.object({
|
|
407
|
+
title: z.optional(line(MAX_TITLE_LENGTH)),
|
|
408
|
+
body: z.optional(body),
|
|
409
|
+
state: z.optional(z.enum(GOAL_STATES)),
|
|
410
|
+
dueOn: z.optional(z.nullable(day)),
|
|
411
|
+
});
|
|
412
|
+
export type RestGoalPatch = z.infer<typeof restGoalPatchSchema>;
|
|
413
|
+
|
|
289
414
|
export const restDecisionOptionSchema = z.object({
|
|
290
415
|
/** What an answer names: stable for the life of the decision. */
|
|
291
416
|
id: z.string(),
|
|
@@ -328,6 +453,19 @@ export type RestDecision = z.infer<typeof restDecisionSchema>;
|
|
|
328
453
|
export const restDecisionsSchema = z.object({ items: z.array(restDecisionSchema) });
|
|
329
454
|
export type RestDecisions = z.infer<typeof restDecisionsSchema>;
|
|
330
455
|
|
|
456
|
+
/** The project a thing on a person's plate belongs to, named for the page to say and link it. */
|
|
457
|
+
const projectRef = z.object({ key: projectKey, name: z.string() });
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* `GET /api/v1/work`: what is on the asker's plate across the projects they may see (ADR-0019):
|
|
461
|
+
* the open tasks assigned to them, and the decisions that wait for a person, each with its project.
|
|
462
|
+
*/
|
|
463
|
+
export const restWorkSchema = z.object({
|
|
464
|
+
tasks: z.array(z.object({ project: projectRef, task: restTaskSchema })),
|
|
465
|
+
decisions: z.array(z.object({ project: projectRef, decision: restDecisionSchema })),
|
|
466
|
+
});
|
|
467
|
+
export type RestWork = z.infer<typeof restWorkSchema>;
|
|
468
|
+
|
|
331
469
|
/** What `POST /api/v1/projects/<key>/decisions` is sent: the question, its context, and the options. */
|
|
332
470
|
export const restDecisionInputSchema = z.object({
|
|
333
471
|
question: line(MAX_QUESTION_LENGTH),
|
|
@@ -363,7 +501,7 @@ export type RestAnswerInput = z.infer<typeof restAnswerInputSchema>;
|
|
|
363
501
|
export const restEventDataSchema = z.object({
|
|
364
502
|
number: z.optional(z.int()),
|
|
365
503
|
title: z.optional(z.string()),
|
|
366
|
-
/** For `task.updated`, `project.updated` and `decision.updated`: which fields changed. */
|
|
504
|
+
/** For `task.updated`, `project.updated`, `goal.updated` and `decision.updated`: which fields changed. */
|
|
367
505
|
fields: z.optional(z.array(z.string())),
|
|
368
506
|
/** For the events about a document: its path, and the version written. */
|
|
369
507
|
path: z.optional(z.string()),
|
|
@@ -406,6 +544,8 @@ export const restEventSchema = z.object({
|
|
|
406
544
|
runId: z.nullable(uuid),
|
|
407
545
|
/** The document it is about, or `null`. */
|
|
408
546
|
documentId: z.nullable(uuid),
|
|
547
|
+
/** The goal it is about, or `null`. */
|
|
548
|
+
goalId: z.nullable(uuid),
|
|
409
549
|
data: restEventDataSchema,
|
|
410
550
|
createdAt: instant,
|
|
411
551
|
});
|