@skillcdn/console 0.1.9 → 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 +6 -6
- 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 +303 -18
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +11 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +87 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +70 -14
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +44 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -4
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +24 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +66 -5
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +54 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +156 -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 +16 -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 +23 -7
- 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/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-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 +16 -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 +33 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +658 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +145 -13
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +19 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +51 -4
- 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 +172 -5
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +183 -6
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +178 -6
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +11 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -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 +51 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +80 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +14 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +17 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +489 -3
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +150 -9
- 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 +31 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +17 -8
- package/docs/specs/rest.md +48 -17
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +42 -0
- package/src/cli/cli.ts +362 -18
- package/src/cli/format.ts +96 -6
- package/src/cli/help.ts +77 -15
- package/src/client.ts +98 -7
- package/src/components/board.tsx +173 -13
- package/src/components/calendar.tsx +367 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +67 -5
- package/src/components/event-feed.tsx +46 -27
- package/src/components/goals.tsx +264 -0
- 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-form.tsx +78 -1
- package/src/components/task-list.tsx +64 -0
- package/src/components/task-view.tsx +75 -4
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +93 -0
- package/src/console.tsx +376 -9
- package/src/data.ts +83 -12
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +193 -6
- package/src/i18n/ko.ts +184 -6
- package/src/index.ts +62 -1
- package/src/limits.ts +11 -0
- package/src/marks.ts +40 -0
- package/src/router.ts +131 -6
- package/src/routes.ts +19 -0
- package/src/schemas.ts +187 -7
- package/src/styles/console.css +658 -0
- package/src/vocabulary.ts +43 -0
package/src/cli/help.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_PAGE_LIMIT,
|
|
2
3
|
MAX_AGENT_LENGTH,
|
|
3
4
|
MAX_ARTIFACTS_PER_RUN,
|
|
4
5
|
MAX_BODY_LENGTH,
|
|
@@ -51,6 +52,22 @@ const MARKDOWN = `Markdown, up to ${MAX_BODY_LENGTH} characters`;
|
|
|
51
52
|
const DOCUMENT_PATH = `a path of lowercase letters, digits and hyphens, with slashes between its folders (guides/onboarding), up to ${MAX_DOCUMENT_PATH_LENGTH} characters; it does not change once written`;
|
|
52
53
|
const DOCUMENT_REFUSALS = ["document.not_found: no page has that path in this project"];
|
|
53
54
|
|
|
55
|
+
const DAY = "a day as 2026-10-24, today, tomorrow, or +7 for seven days from today";
|
|
56
|
+
const TASK_FIELD_FLAGS =
|
|
57
|
+
"[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--assignee <login|me>] [--parent <number|id>]";
|
|
58
|
+
const TASK_FIELD_LIMITS = [
|
|
59
|
+
`starts, due: ${DAY}; the due day is not before the start`,
|
|
60
|
+
"category: the key of one of the workspace's categories (console categories); goal: the id of one of the project's goals (console goals)",
|
|
61
|
+
"assignee: a login of the workspace, or me; parent: a task of the project",
|
|
62
|
+
];
|
|
63
|
+
const TASK_FIELD_REFUSALS = [
|
|
64
|
+
"task.invalid_category: no category has that key",
|
|
65
|
+
"task.invalid_goal: the goal is not one of this project",
|
|
66
|
+
"task.invalid_dates: the due day is before the start",
|
|
67
|
+
"task.invalid_assignee: the person may not work in the project",
|
|
68
|
+
"task.invalid_parent: the parent is not a task of the project, or would make a loop",
|
|
69
|
+
];
|
|
70
|
+
|
|
54
71
|
export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
55
72
|
login: {
|
|
56
73
|
usage: "login --url <origin> [--agent <name>] [--token-stdin]",
|
|
@@ -82,18 +99,51 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
82
99
|
"Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over it.",
|
|
83
100
|
refusals: ["project.not_found: no project has the key, or it is not yours to see"],
|
|
84
101
|
},
|
|
85
|
-
|
|
86
|
-
usage: "
|
|
102
|
+
work: {
|
|
103
|
+
usage: "work",
|
|
87
104
|
about:
|
|
88
|
-
"
|
|
105
|
+
"What is on your plate across the projects you may see: the open tasks assigned to you, the sooner due first, each with its project; and the decisions waiting for a person. Needs no project.",
|
|
106
|
+
},
|
|
107
|
+
tasks: {
|
|
108
|
+
usage:
|
|
109
|
+
"tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--limit <n>] [--offset <n>]",
|
|
110
|
+
about: `The tasks on the board, newest first: number, state, priority, title, who owns and works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those assigned to you, --category and --goal those sorted into one, --due those due by a day: today, within the week, or overdue (the open ones past their day). A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more. States: idea, ready, in_progress, in_review, done, dropped.`,
|
|
89
111
|
},
|
|
90
112
|
task: {
|
|
113
|
+
usage: `task <number|id> | task new <title> [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task edit <number|id> [--title <title>] [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS}`,
|
|
114
|
+
about:
|
|
115
|
+
"One task in full, with its plan (category, goal, start and due days), what happened to it (when it was started and done, which the board keeps itself), the decisions about it and the runs on it; or a new task written down; or a task changed, only in what the flags say, where none clears a field (priorities: low, normal, high, urgent).",
|
|
116
|
+
limits: [
|
|
117
|
+
`title: one line, up to ${MAX_TITLE_LENGTH} characters`,
|
|
118
|
+
`body: ${MARKDOWN}`,
|
|
119
|
+
...TASK_FIELD_LIMITS,
|
|
120
|
+
],
|
|
121
|
+
refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
|
|
122
|
+
},
|
|
123
|
+
goals: {
|
|
124
|
+
usage: "goals",
|
|
125
|
+
about:
|
|
126
|
+
"The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its target day, and how far it is, counted from the tasks that work towards it.",
|
|
127
|
+
},
|
|
128
|
+
goal: {
|
|
91
129
|
usage:
|
|
92
|
-
|
|
130
|
+
'goal <id> | goal new "<title>" [--body <markdown>|--file <path>] [--state <state>] [--due <day>] | goal edit <id> [--title <title>] [--body <markdown>|--file <path>] [--state <state>] [--due <day|none>]',
|
|
131
|
+
about:
|
|
132
|
+
"One goal in full, with the tasks that work towards it; or a goal set on the roadmap, for an owner of the project; or one changed, only in what the flags say. A task works towards a goal through its --goal.",
|
|
133
|
+
limits: [
|
|
134
|
+
`title: one line, up to ${MAX_TITLE_LENGTH} characters`,
|
|
135
|
+
`body: ${MARKDOWN}`,
|
|
136
|
+
`due: ${DAY}`,
|
|
137
|
+
],
|
|
138
|
+
refusals: [
|
|
139
|
+
"goal.not_found: no goal of this project has that id",
|
|
140
|
+
"auth.forbidden: only an owner of the project sets its roadmap",
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
categories: {
|
|
144
|
+
usage: "categories",
|
|
93
145
|
about:
|
|
94
|
-
"
|
|
95
|
-
limits: [`title: one line, up to ${MAX_TITLE_LENGTH} characters`, `body: ${MARKDOWN}`],
|
|
96
|
-
refusals: ["task.not_found: no task has that number or id"],
|
|
146
|
+
"The categories tasks are sorted into, the same in every project: each with its key, which --category takes, its name and its colour. An administrator keeps them on the console's Categories page.",
|
|
97
147
|
},
|
|
98
148
|
skills: {
|
|
99
149
|
usage: "skills",
|
|
@@ -103,11 +153,10 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
103
153
|
take: {
|
|
104
154
|
usage: "take <number|id> [--agent <name>]",
|
|
105
155
|
about:
|
|
106
|
-
"Takes a task: a run begins, and the task is in progress and the person's.
|
|
156
|
+
"Takes a task: a run begins, and the task is in progress and the person's. A run still open on the task is taken over, since the board cannot know whether its agent still runs: it ends as abandoned, the feed says who took over, and that agent is refused on its next command. Look before taking (console task <number> lists the runs, with when each was last heard from), and take a task another agent was heard from on lately only when told to. --agent is what people see at work; left out, it is what the person called the token.",
|
|
107
157
|
limits: [`agent: one line, up to ${MAX_AGENT_LENGTH} characters`],
|
|
108
158
|
refusals: [
|
|
109
159
|
"task.not_found: no task has that number or id",
|
|
110
|
-
"run.task_taken: a run is open on the task already; console runs --open --mine shows whether it is yours",
|
|
111
160
|
"run.task_closed: the task is done or dropped",
|
|
112
161
|
],
|
|
113
162
|
},
|
|
@@ -157,8 +206,8 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
157
206
|
refusals: ["decision.not_found: no decision has that id"],
|
|
158
207
|
},
|
|
159
208
|
decisions: {
|
|
160
|
-
usage: "decisions [--open]",
|
|
161
|
-
about:
|
|
209
|
+
usage: "decisions [--open] [--limit <n>] [--offset <n>]",
|
|
210
|
+
about: `The decisions on the board, the waiting first; --open keeps only those. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on.`,
|
|
162
211
|
},
|
|
163
212
|
finish: {
|
|
164
213
|
usage: 'finish [--summary "<markdown>"|--file <path>]',
|
|
@@ -181,9 +230,8 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
181
230
|
refusals: RUN_REFUSALS,
|
|
182
231
|
},
|
|
183
232
|
runs: {
|
|
184
|
-
usage: "runs [--task <number|id>] [--open] [--mine]",
|
|
185
|
-
about:
|
|
186
|
-
"The runs, newest first: agents at work and what they did. --mine keeps those begun with this token.",
|
|
233
|
+
usage: "runs [--task <number|id>] [--open] [--mine] [--limit <n>] [--offset <n>]",
|
|
234
|
+
about: `The runs, newest first, each with when its agent was last heard from: --open keeps the ones not over, --mine those begun with this token. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on.`,
|
|
187
235
|
},
|
|
188
236
|
run: {
|
|
189
237
|
usage: "run <id>",
|
|
@@ -262,6 +310,8 @@ Usage: console <command> [arguments] [--json] [--url <origin>] [--project <key>]
|
|
|
262
310
|
|
|
263
311
|
const CLOSING = `Options everywhere: --json answers with the console's own JSON; --url names another console;
|
|
264
312
|
--project names the project to work in, over CONSOLE_PROJECT and over what console use kept here.
|
|
313
|
+
A listing (tasks, decisions, runs) is a page at a time: --limit <n> and --offset <n>, and a last line
|
|
314
|
+
says when there is more, so that nothing is read that is not needed.
|
|
265
315
|
Commands on a run (report, hand-in, ask, finish, fail) act on your one open run in the project;
|
|
266
316
|
with several open, say which with --run <id>. A Markdown body given as - is read from standard input.
|
|
267
317
|
console help <command> says what each command takes, the limits the console holds it to, and
|
|
@@ -283,7 +333,19 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
|
|
|
283
333
|
},
|
|
284
334
|
{
|
|
285
335
|
title: "The board",
|
|
286
|
-
commands: [
|
|
336
|
+
commands: [
|
|
337
|
+
"work",
|
|
338
|
+
"tasks",
|
|
339
|
+
"task",
|
|
340
|
+
"goals",
|
|
341
|
+
"goal",
|
|
342
|
+
"categories",
|
|
343
|
+
"decisions",
|
|
344
|
+
"decision",
|
|
345
|
+
"runs",
|
|
346
|
+
"run",
|
|
347
|
+
"skills",
|
|
348
|
+
],
|
|
287
349
|
},
|
|
288
350
|
{ title: "Working", commands: ["take", "report", "hand-in", "ask", "finish", "fail", "abandon"] },
|
|
289
351
|
{
|
package/src/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AUTH_ROUTES,
|
|
3
3
|
CLAIM_PATH,
|
|
4
|
+
categoryPath,
|
|
4
5
|
connectPath,
|
|
5
6
|
personTokensPath,
|
|
6
7
|
projectPath,
|
|
@@ -10,6 +11,10 @@ import {
|
|
|
10
11
|
import {
|
|
11
12
|
type RestAnswerInput,
|
|
12
13
|
type RestArtifactInput,
|
|
14
|
+
type RestCategories,
|
|
15
|
+
type RestCategory,
|
|
16
|
+
type RestCategoryInput,
|
|
17
|
+
type RestCategoryPatch,
|
|
13
18
|
type RestClaim,
|
|
14
19
|
type RestConnectInput,
|
|
15
20
|
type RestConnection,
|
|
@@ -22,6 +27,10 @@ import {
|
|
|
22
27
|
type RestDocumentInput,
|
|
23
28
|
type RestDocuments,
|
|
24
29
|
type RestEvents,
|
|
30
|
+
type RestGoal,
|
|
31
|
+
type RestGoalInput,
|
|
32
|
+
type RestGoalPatch,
|
|
33
|
+
type RestGoals,
|
|
25
34
|
type RestMe,
|
|
26
35
|
type RestMember,
|
|
27
36
|
type RestMemberInput,
|
|
@@ -50,6 +59,9 @@ import {
|
|
|
50
59
|
type RestTokens,
|
|
51
60
|
type RestVersion,
|
|
52
61
|
type RestVersions,
|
|
62
|
+
type RestWork,
|
|
63
|
+
restCategoriesSchema,
|
|
64
|
+
restCategorySchema,
|
|
53
65
|
restClaimSchema,
|
|
54
66
|
restConnectionSchema,
|
|
55
67
|
restConnectRequestSchema,
|
|
@@ -59,6 +71,8 @@ import {
|
|
|
59
71
|
restDocumentsSchema,
|
|
60
72
|
restErrorSchema,
|
|
61
73
|
restEventsSchema,
|
|
74
|
+
restGoalSchema,
|
|
75
|
+
restGoalsSchema,
|
|
62
76
|
restMemberSchema,
|
|
63
77
|
restMembersSchema,
|
|
64
78
|
restMeSchema,
|
|
@@ -75,6 +89,7 @@ import {
|
|
|
75
89
|
restTokensSchema,
|
|
76
90
|
restVersionSchema,
|
|
77
91
|
restVersionsSchema,
|
|
92
|
+
restWorkSchema,
|
|
78
93
|
} from "./schemas.js";
|
|
79
94
|
import type { TaskState } from "./vocabulary.js";
|
|
80
95
|
|
|
@@ -135,6 +150,27 @@ export interface EventFilter {
|
|
|
135
150
|
readonly document?: string | undefined;
|
|
136
151
|
}
|
|
137
152
|
|
|
153
|
+
/** One page of a listing: how many items, from which one on; the server's bounds apply. */
|
|
154
|
+
export interface Page {
|
|
155
|
+
readonly limit?: number | undefined;
|
|
156
|
+
readonly offset?: number | undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** What the board is narrowed to: one state, the open ones, a category, a goal, an assignee, or what is due by a day. */
|
|
160
|
+
export interface TaskFilter extends Page {
|
|
161
|
+
readonly state?: TaskState | undefined;
|
|
162
|
+
/** True for the tasks not done or dropped; false for the rest. */
|
|
163
|
+
readonly open?: boolean | undefined;
|
|
164
|
+
/** The key of a category. */
|
|
165
|
+
readonly category?: string | undefined;
|
|
166
|
+
/** The id of a goal. */
|
|
167
|
+
readonly goal?: string | undefined;
|
|
168
|
+
/** The id of the person the tasks are assigned to. */
|
|
169
|
+
readonly assignee?: string | undefined;
|
|
170
|
+
/** A day (`2026-10-20`): the tasks due on it or before it. */
|
|
171
|
+
readonly dueBy?: string | undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
138
174
|
/** What a folder's listing, or a search, of the documents is narrowed to. */
|
|
139
175
|
export interface DocumentFilter {
|
|
140
176
|
/** The folder to list, `""` or left out for the root. */
|
|
@@ -153,20 +189,24 @@ export interface ProjectClient {
|
|
|
153
189
|
get(signal?: AbortSignal): Promise<RestProject>;
|
|
154
190
|
/** Changes the project's settings; for an owner. */
|
|
155
191
|
update(patch: RestProjectPatch): Promise<RestProject>;
|
|
192
|
+
/** Sets the project's own image for its mark, sent as a form; for an owner on the console's own pages. */
|
|
193
|
+
setIcon(input: FileUpload): Promise<RestProject>;
|
|
194
|
+
/** Takes the image away: the mark is the symbol on the colour again. */
|
|
195
|
+
removeIcon(): Promise<RestProject>;
|
|
156
196
|
/** Those listed in the project, by login. */
|
|
157
197
|
members(signal?: AbortSignal): Promise<RestMembers>;
|
|
158
198
|
/** Lists a person of the workspace in the project; for an owner. */
|
|
159
199
|
addMember(input: RestMemberInput): Promise<RestMember>;
|
|
160
200
|
updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
|
|
161
201
|
removeMember(personId: string): Promise<void>;
|
|
162
|
-
tasks(filter?:
|
|
202
|
+
tasks(filter?: TaskFilter, signal?: AbortSignal): Promise<RestTasks>;
|
|
163
203
|
/** One task, by its id or by its number. */
|
|
164
204
|
task(ref: string, signal?: AbortSignal): Promise<RestTask>;
|
|
165
205
|
createTask(input: RestTaskInput): Promise<RestTask>;
|
|
166
206
|
updateTask(id: string, patch: RestTaskPatch): Promise<RestTask>;
|
|
167
|
-
/** The decisions, or only those that wait, or only those about one task. */
|
|
207
|
+
/** The decisions, or only those that wait, or only those about one task; a page of them. */
|
|
168
208
|
decisions(
|
|
169
|
-
filter?: { readonly open?: boolean; readonly task?: string },
|
|
209
|
+
filter?: { readonly open?: boolean; readonly task?: string } & Page,
|
|
170
210
|
signal?: AbortSignal,
|
|
171
211
|
): Promise<RestDecisions>;
|
|
172
212
|
decision(id: string, signal?: AbortSignal): Promise<RestDecision>;
|
|
@@ -179,9 +219,9 @@ export interface ProjectClient {
|
|
|
179
219
|
answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
|
|
180
220
|
/** Grows a decision's record: its context, or what followed. */
|
|
181
221
|
updateDecision(id: string, patch: RestDecisionPatch): Promise<RestDecision>;
|
|
182
|
-
/** The runs, newest first; or only those on one task, only the open ones, only the asker's own. */
|
|
222
|
+
/** The runs, newest first; or only those on one task, only the open ones, only the asker's own; a page of them. */
|
|
183
223
|
runs(
|
|
184
|
-
filter?: { readonly task?: string; readonly open?: boolean; readonly mine?: boolean },
|
|
224
|
+
filter?: { readonly task?: string; readonly open?: boolean; readonly mine?: boolean } & Page,
|
|
185
225
|
signal?: AbortSignal,
|
|
186
226
|
): Promise<RestRuns>;
|
|
187
227
|
run(id: string, signal?: AbortSignal): Promise<RestRun>;
|
|
@@ -221,6 +261,12 @@ export interface ProjectClient {
|
|
|
221
261
|
attachFile(path: string, input: FileUpload): Promise<RestDocument>;
|
|
222
262
|
/** Where the bytes of a file attached to a document are read, by the file's id. */
|
|
223
263
|
documentFileUrl(path: string, fileId: string): string;
|
|
264
|
+
/** The roadmap (ADR-0019): the goals, the active first, each with its tasks counted by state. */
|
|
265
|
+
goals(signal?: AbortSignal): Promise<RestGoals>;
|
|
266
|
+
goal(id: string, signal?: AbortSignal): Promise<RestGoal>;
|
|
267
|
+
/** Sets a goal; for an owner, with a session or a token. */
|
|
268
|
+
createGoal(input: RestGoalInput): Promise<RestGoal>;
|
|
269
|
+
updateGoal(id: string, patch: RestGoalPatch): Promise<RestGoal>;
|
|
224
270
|
}
|
|
225
271
|
|
|
226
272
|
export interface ConsoleClient {
|
|
@@ -238,6 +284,15 @@ export interface ConsoleClient {
|
|
|
238
284
|
project(key: string): ProjectClient;
|
|
239
285
|
/** The workspace's own events, the ones about no project, after event number `after`. */
|
|
240
286
|
events(after: number, signal?: AbortSignal): Promise<RestEvents>;
|
|
287
|
+
/** The categories tasks are sorted into (ADR-0019), by name. */
|
|
288
|
+
categories(signal?: AbortSignal): Promise<RestCategories>;
|
|
289
|
+
/** Makes a category; for an administrator signed in. */
|
|
290
|
+
createCategory(input: RestCategoryInput): Promise<RestCategory>;
|
|
291
|
+
updateCategory(key: string, patch: RestCategoryPatch): Promise<RestCategory>;
|
|
292
|
+
/** Removes a category no task uses; for an administrator signed in. */
|
|
293
|
+
removeCategory(key: string): Promise<void>;
|
|
294
|
+
/** What is on the asker's plate across the projects they may see: their open tasks, and the decisions waiting. */
|
|
295
|
+
work(signal?: AbortSignal): Promise<RestWork>;
|
|
241
296
|
/** The tokens of whoever asks. */
|
|
242
297
|
tokens(signal?: AbortSignal): Promise<RestTokens>;
|
|
243
298
|
/** The tokens of another person: for an administrator. */
|
|
@@ -336,6 +391,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
336
391
|
|
|
337
392
|
const flag = (value: boolean | undefined): string | undefined =>
|
|
338
393
|
value === undefined ? undefined : String(value);
|
|
394
|
+
const number = (value: number | undefined): string | undefined =>
|
|
395
|
+
value === undefined ? undefined : String(value);
|
|
339
396
|
|
|
340
397
|
/** A file as the parts of a form: the bytes with their name and type, and the label. */
|
|
341
398
|
const formOf = (input: FileUpload): FormData => {
|
|
@@ -360,6 +417,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
360
417
|
key,
|
|
361
418
|
get: (signal) => request("GET", `${base}${at()}`, undefined, restProjectSchema, signal),
|
|
362
419
|
update: (patch) => request("PATCH", `${base}${at()}`, patch, restProjectSchema),
|
|
420
|
+
setIcon: (input) => request("POST", `${base}${at()}/icon`, formOf(input), restProjectSchema),
|
|
421
|
+
removeIcon: () => request("DELETE", `${base}${at()}/icon`, undefined, restProjectSchema),
|
|
363
422
|
members: (signal) =>
|
|
364
423
|
request("GET", `${base}${at("members")}`, undefined, restMembersSchema, signal),
|
|
365
424
|
addMember: (input) => request("POST", `${base}${at("members")}`, input, restMemberSchema),
|
|
@@ -370,7 +429,16 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
370
429
|
tasks: (filter = {}, signal) =>
|
|
371
430
|
request(
|
|
372
431
|
"GET",
|
|
373
|
-
withQuery(at("tasks"), {
|
|
432
|
+
withQuery(at("tasks"), {
|
|
433
|
+
state: filter.state,
|
|
434
|
+
open: flag(filter.open),
|
|
435
|
+
category: filter.category,
|
|
436
|
+
goal: filter.goal,
|
|
437
|
+
assignee: filter.assignee,
|
|
438
|
+
dueBy: filter.dueBy,
|
|
439
|
+
limit: number(filter.limit),
|
|
440
|
+
offset: number(filter.offset),
|
|
441
|
+
}),
|
|
374
442
|
undefined,
|
|
375
443
|
restTasksSchema,
|
|
376
444
|
signal,
|
|
@@ -383,7 +451,12 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
383
451
|
decisions: (filter = {}, signal) =>
|
|
384
452
|
request(
|
|
385
453
|
"GET",
|
|
386
|
-
withQuery(at("decisions"), {
|
|
454
|
+
withQuery(at("decisions"), {
|
|
455
|
+
open: flag(filter.open),
|
|
456
|
+
task: filter.task,
|
|
457
|
+
limit: number(filter.limit),
|
|
458
|
+
offset: number(filter.offset),
|
|
459
|
+
}),
|
|
387
460
|
undefined,
|
|
388
461
|
restDecisionsSchema,
|
|
389
462
|
signal,
|
|
@@ -411,6 +484,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
411
484
|
task: filter.task,
|
|
412
485
|
open: flag(filter.open),
|
|
413
486
|
mine: flag(filter.mine),
|
|
487
|
+
limit: number(filter.limit),
|
|
488
|
+
offset: number(filter.offset),
|
|
414
489
|
}),
|
|
415
490
|
undefined,
|
|
416
491
|
restRunsSchema,
|
|
@@ -485,6 +560,13 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
485
560
|
request("POST", `${base}${at("docs", path)}/files`, formOf(input), restDocumentSchema),
|
|
486
561
|
documentFileUrl: (path, fileId) =>
|
|
487
562
|
`${base}${at("docs", path)}/files/${encodeURIComponent(fileId)}`,
|
|
563
|
+
goals: (signal) =>
|
|
564
|
+
request("GET", `${base}${at("goals")}`, undefined, restGoalsSchema, signal),
|
|
565
|
+
goal: (id, signal) =>
|
|
566
|
+
request("GET", `${base}${at("goals", id)}`, undefined, restGoalSchema, signal),
|
|
567
|
+
createGoal: (input) => request("POST", `${base}${at("goals")}`, input, restGoalSchema),
|
|
568
|
+
updateGoal: (id, patch) =>
|
|
569
|
+
request("PATCH", `${base}${at("goals", id)}`, patch, restGoalSchema),
|
|
488
570
|
};
|
|
489
571
|
};
|
|
490
572
|
|
|
@@ -507,6 +589,15 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
507
589
|
restEventsSchema,
|
|
508
590
|
signal,
|
|
509
591
|
),
|
|
592
|
+
categories: (signal) =>
|
|
593
|
+
request("GET", `${base}${REST_ROUTES.categories}`, undefined, restCategoriesSchema, signal),
|
|
594
|
+
createCategory: (input) =>
|
|
595
|
+
request("POST", `${base}${REST_ROUTES.categories}`, input, restCategorySchema),
|
|
596
|
+
updateCategory: (key, patch) =>
|
|
597
|
+
request("PATCH", `${base}${categoryPath(key)}`, patch, restCategorySchema),
|
|
598
|
+
removeCategory: (key) => request("DELETE", `${base}${categoryPath(key)}`, undefined, undefined),
|
|
599
|
+
work: (signal) =>
|
|
600
|
+
request("GET", `${base}${REST_ROUTES.work}`, undefined, restWorkSchema, signal),
|
|
510
601
|
tokens: (signal) =>
|
|
511
602
|
request("GET", `${base}${REST_ROUTES.tokens}`, undefined, restTokensSchema, signal),
|
|
512
603
|
createToken: (input) =>
|
package/src/components/board.tsx
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type RestCategory,
|
|
4
|
+
type RestGoal,
|
|
5
|
+
type RestPerson,
|
|
6
|
+
type RestTask,
|
|
7
|
+
TASK_STATES,
|
|
8
|
+
type TaskState,
|
|
9
|
+
} from "../api.js";
|
|
3
10
|
import { useWords } from "../i18n/index.js";
|
|
4
|
-
import {
|
|
11
|
+
import type { BoardFilter } from "../router.js";
|
|
12
|
+
import { Avatar, Badge, Button, CategoryBadge, cx, DueBadge, PriorityBadge } from "./ui.js";
|
|
5
13
|
|
|
6
|
-
// The board: one column per state, the tasks in it,
|
|
7
|
-
//
|
|
14
|
+
// The board: one column per state, the tasks in it, the sooner due first and the newest after.
|
|
15
|
+
// A card says what a person needs at a glance and nothing more: the number and the title, the
|
|
16
|
+
// category, what is due, who is on it, and what blocks it. Whether an agent is at it is not
|
|
17
|
+
// said, since the board cannot know (ADR-0020); the task's page shows its runs. It takes its
|
|
18
|
+
// data as props and nothing from the network; what it is told on a click goes back up.
|
|
8
19
|
|
|
9
20
|
export interface BoardProps {
|
|
10
21
|
readonly tasks: readonly RestTask[];
|
|
@@ -13,6 +24,8 @@ export interface BoardProps {
|
|
|
13
24
|
readonly onOpen: (task: RestTask) => void;
|
|
14
25
|
/** Called when a person moves a task to another column. Left out, the board is read-only. */
|
|
15
26
|
readonly onMove?: ((task: RestTask, state: TaskState) => void) | undefined;
|
|
27
|
+
/** The day to count due days from; the browser's today when left out. */
|
|
28
|
+
readonly today?: string | undefined;
|
|
16
29
|
/** What a column shows when it has nothing; the way to write the first task, for example. */
|
|
17
30
|
readonly empty?: ReactNode;
|
|
18
31
|
}
|
|
@@ -22,6 +35,7 @@ export function TaskCard(props: {
|
|
|
22
35
|
readonly href: string;
|
|
23
36
|
readonly onOpen: (task: RestTask) => void;
|
|
24
37
|
readonly onMove?: ((task: RestTask, state: TaskState) => void) | undefined;
|
|
38
|
+
readonly today?: string | undefined;
|
|
25
39
|
}) {
|
|
26
40
|
const words = useWords();
|
|
27
41
|
const { task } = props;
|
|
@@ -40,20 +54,16 @@ export function TaskCard(props: {
|
|
|
40
54
|
<span className="sc-card-number">#{task.number}</span> {task.title}
|
|
41
55
|
</a>
|
|
42
56
|
<div className="sc-card-meta">
|
|
57
|
+
{task.category !== null && <CategoryBadge category={task.category} />}
|
|
43
58
|
<PriorityBadge priority={task.priority} />
|
|
59
|
+
{task.dueOn !== null && (
|
|
60
|
+
<DueBadge dueOn={task.dueOn} state={task.state} today={props.today} />
|
|
61
|
+
)}
|
|
44
62
|
{task.openDecisions > 0 && (
|
|
45
63
|
<Badge tone="warning" title={words.board.decisionsTitle}>
|
|
46
64
|
{words.board.decisions(task.openDecisions)}
|
|
47
65
|
</Badge>
|
|
48
66
|
)}
|
|
49
|
-
{task.openRuns > 0 && (
|
|
50
|
-
<Badge tone="accent" title={words.board.agentAtWorkTitle}>
|
|
51
|
-
{words.board.agentAtWork}
|
|
52
|
-
</Badge>
|
|
53
|
-
)}
|
|
54
|
-
{task.links.length > 0 && (
|
|
55
|
-
<Badge tone="neutral">{words.board.links(task.links.length)}</Badge>
|
|
56
|
-
)}
|
|
57
67
|
<span className="sc-card-spacer" />
|
|
58
68
|
{task.assignee !== null && <Avatar person={task.assignee} size="sm" />}
|
|
59
69
|
</div>
|
|
@@ -76,12 +86,26 @@ export function TaskCard(props: {
|
|
|
76
86
|
);
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
/** The order a column reads in: what is due soonest first, what has no day after, the newest first among those. */
|
|
90
|
+
export function byDue(a: RestTask, b: RestTask): number {
|
|
91
|
+
if (a.dueOn !== b.dueOn) {
|
|
92
|
+
if (a.dueOn === null) {
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
if (b.dueOn === null) {
|
|
96
|
+
return -1;
|
|
97
|
+
}
|
|
98
|
+
return a.dueOn.localeCompare(b.dueOn);
|
|
99
|
+
}
|
|
100
|
+
return b.number - a.number;
|
|
101
|
+
}
|
|
102
|
+
|
|
79
103
|
export function Board(props: BoardProps) {
|
|
80
104
|
const words = useWords();
|
|
81
105
|
return (
|
|
82
106
|
<div className="sc-board">
|
|
83
107
|
{TASK_STATES.map((state) => {
|
|
84
|
-
const tasks = props.tasks.filter((task) => task.state === state);
|
|
108
|
+
const tasks = props.tasks.filter((task) => task.state === state).sort(byDue);
|
|
85
109
|
return (
|
|
86
110
|
<section
|
|
87
111
|
key={state}
|
|
@@ -100,6 +124,7 @@ export function Board(props: BoardProps) {
|
|
|
100
124
|
href={props.taskHref(task)}
|
|
101
125
|
onOpen={props.onOpen}
|
|
102
126
|
onMove={props.onMove}
|
|
127
|
+
today={props.today}
|
|
103
128
|
/>
|
|
104
129
|
))}
|
|
105
130
|
{tasks.length === 0 && state === "idea" && props.empty}
|
|
@@ -110,3 +135,138 @@ export function Board(props: BoardProps) {
|
|
|
110
135
|
</div>
|
|
111
136
|
);
|
|
112
137
|
}
|
|
138
|
+
|
|
139
|
+
/** What the filter keeps of the board: a category or none, a goal or none, a person, one's own. */
|
|
140
|
+
export function filterTasks(
|
|
141
|
+
tasks: readonly RestTask[],
|
|
142
|
+
filter: BoardFilter,
|
|
143
|
+
me?: RestPerson | undefined,
|
|
144
|
+
): RestTask[] {
|
|
145
|
+
return tasks.filter((task) => {
|
|
146
|
+
if (filter.category !== undefined && (task.category?.key ?? "") !== filter.category) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (filter.goal !== undefined && (task.goalId ?? "") !== filter.goal) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (filter.person !== undefined && task.assignee?.id !== filter.person) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
if (filter.mine === true && (me === undefined || task.assignee?.id !== me.id)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface BoardFiltersProps {
|
|
163
|
+
readonly filter: BoardFilter;
|
|
164
|
+
readonly categories: readonly RestCategory[];
|
|
165
|
+
readonly goals: readonly RestGoal[];
|
|
166
|
+
readonly people: readonly RestPerson[];
|
|
167
|
+
/** Whoever is looking, for "mine". */
|
|
168
|
+
readonly me?: RestPerson | undefined;
|
|
169
|
+
/** How many tasks the filter keeps, of how many. */
|
|
170
|
+
readonly shown: number;
|
|
171
|
+
readonly total: number;
|
|
172
|
+
/** Called with the filter as it is to be: the composition shows the board at its address. */
|
|
173
|
+
readonly onChange: (filter: BoardFilter) => void;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The ways to narrow the board, each a change of its address (ADR-0010). */
|
|
177
|
+
export function BoardFilters(props: BoardFiltersProps) {
|
|
178
|
+
const words = useWords().board.filters;
|
|
179
|
+
const { filter } = props;
|
|
180
|
+
const narrowed = Object.keys(filter).length > 0;
|
|
181
|
+
const goals = props.goals.filter((goal) => goal.state !== "dropped");
|
|
182
|
+
const change = (patch: BoardFilter, drop?: keyof BoardFilter) => {
|
|
183
|
+
const next: Record<string, unknown> = { ...filter, ...patch };
|
|
184
|
+
if (drop !== undefined) {
|
|
185
|
+
delete next[drop];
|
|
186
|
+
}
|
|
187
|
+
props.onChange(next as BoardFilter);
|
|
188
|
+
};
|
|
189
|
+
return (
|
|
190
|
+
<form
|
|
191
|
+
className="sc-filters"
|
|
192
|
+
aria-label={words.aria}
|
|
193
|
+
onSubmit={(event) => event.preventDefault()}
|
|
194
|
+
>
|
|
195
|
+
<label className="sc-field sc-field-inline">
|
|
196
|
+
<span className="sc-visually-hidden">{words.anyCategory}</span>
|
|
197
|
+
<select
|
|
198
|
+
className="sc-input"
|
|
199
|
+
value={filter.category ?? "*"}
|
|
200
|
+
onChange={(event) =>
|
|
201
|
+
event.target.value === "*"
|
|
202
|
+
? change({}, "category")
|
|
203
|
+
: change({ category: event.target.value })
|
|
204
|
+
}
|
|
205
|
+
>
|
|
206
|
+
<option value="*">{words.anyCategory}</option>
|
|
207
|
+
<option value="">{words.noCategory}</option>
|
|
208
|
+
{props.categories.map((category) => (
|
|
209
|
+
<option key={category.key} value={category.key}>
|
|
210
|
+
{category.name}
|
|
211
|
+
</option>
|
|
212
|
+
))}
|
|
213
|
+
</select>
|
|
214
|
+
</label>
|
|
215
|
+
{goals.length > 0 && (
|
|
216
|
+
<label className="sc-field sc-field-inline">
|
|
217
|
+
<span className="sc-visually-hidden">{words.anyGoal}</span>
|
|
218
|
+
<select
|
|
219
|
+
className="sc-input"
|
|
220
|
+
value={filter.goal ?? "*"}
|
|
221
|
+
onChange={(event) =>
|
|
222
|
+
event.target.value === "*" ? change({}, "goal") : change({ goal: event.target.value })
|
|
223
|
+
}
|
|
224
|
+
>
|
|
225
|
+
<option value="*">{words.anyGoal}</option>
|
|
226
|
+
<option value="">{words.noGoal}</option>
|
|
227
|
+
{goals.map((goal) => (
|
|
228
|
+
<option key={goal.id} value={goal.id}>
|
|
229
|
+
{goal.title}
|
|
230
|
+
</option>
|
|
231
|
+
))}
|
|
232
|
+
</select>
|
|
233
|
+
</label>
|
|
234
|
+
)}
|
|
235
|
+
<label className="sc-field sc-field-inline">
|
|
236
|
+
<span className="sc-visually-hidden">{words.everyone}</span>
|
|
237
|
+
<select
|
|
238
|
+
className="sc-input"
|
|
239
|
+
value={filter.mine === true ? "mine" : (filter.person ?? "*")}
|
|
240
|
+
onChange={(event) => {
|
|
241
|
+
const value = event.target.value;
|
|
242
|
+
if (value === "*") {
|
|
243
|
+
change({ mine: undefined, person: undefined });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (value === "mine") {
|
|
247
|
+
change({ mine: true, person: undefined });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
change({ person: value, mine: undefined });
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<option value="*">{words.everyone}</option>
|
|
254
|
+
{props.me !== undefined && <option value="mine">{words.mine}</option>}
|
|
255
|
+
{props.people
|
|
256
|
+
.filter((person) => person.id !== props.me?.id)
|
|
257
|
+
.map((person) => (
|
|
258
|
+
<option key={person.id} value={person.id}>
|
|
259
|
+
{person.login}
|
|
260
|
+
</option>
|
|
261
|
+
))}
|
|
262
|
+
</select>
|
|
263
|
+
</label>
|
|
264
|
+
<span className="sc-filters-count sc-muted">{words.showing(props.shown, props.total)}</span>
|
|
265
|
+
{narrowed && (
|
|
266
|
+
<Button variant="ghost" size="sm" onClick={() => props.onChange({})}>
|
|
267
|
+
{words.clear}
|
|
268
|
+
</Button>
|
|
269
|
+
)}
|
|
270
|
+
</form>
|
|
271
|
+
);
|
|
272
|
+
}
|