@skillcdn/console 0.3.1 → 0.3.2
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 +2 -2
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +105 -18
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +9 -7
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +13 -11
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +13 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +28 -6
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +55 -14
- package/dist/compat.js.map +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +6 -4
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +13 -7
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +36 -24
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/picker.d.ts +39 -0
- package/dist/components/picker.d.ts.map +1 -0
- package/dist/components/picker.js +155 -0
- package/dist/components/picker.js.map +1 -0
- package/dist/components/task-form.d.ts +18 -4
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +56 -13
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.js +2 -2
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.d.ts +7 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -5
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +7 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +11 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +5 -3
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +163 -11
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +24 -4
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +11 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +22 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +11 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +16 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +17 -9
- 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 +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -1
- package/dist/schemas.d.ts +12 -11
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +11 -8
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +2 -0
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +2 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +9 -9
- package/docs/specs/rest.md +14 -14
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +2 -2
- package/skills/deploying-a-console/SKILL.md +2 -2
- package/skills/deploying-a-console/references/resources.md +1 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/skills/working-the-board/references/terms.md +2 -2
- package/src/api.ts +5 -0
- package/src/cli/cli.ts +131 -18
- package/src/cli/format.ts +11 -7
- package/src/cli/help.ts +15 -11
- package/src/cli/main.ts +1 -0
- package/src/client.ts +23 -10
- package/src/compat.ts +63 -14
- package/src/components/board.tsx +10 -4
- package/src/components/calendar.tsx +68 -32
- package/src/components/picker.tsx +297 -0
- package/src/components/task-form.tsx +149 -63
- package/src/components/task-list.tsx +2 -2
- package/src/components/task-view.tsx +23 -9
- package/src/components/ui.tsx +25 -0
- package/src/components/work.tsx +6 -4
- package/src/console.tsx +29 -3
- package/src/data.ts +44 -1
- package/src/i18n/en.ts +16 -8
- package/src/i18n/ko.ts +17 -9
- package/src/index.ts +16 -1
- package/src/limits.ts +2 -0
- package/src/schemas.ts +11 -7
- package/src/styles/console.css +163 -11
- package/src/vocabulary.ts +3 -0
package/src/cli/format.ts
CHANGED
|
@@ -147,10 +147,14 @@ export function formatMore(more: boolean, limit: number, offset: number): string
|
|
|
147
147
|
return more ? `\n... more: --offset ${offset + limit}` : "";
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/** The logins of the people at work on a task, in the order they came to it. */
|
|
151
|
+
const logins = (people: readonly RestPerson[]): string =>
|
|
152
|
+
people.map((person) => person.login).join(", ");
|
|
153
|
+
|
|
150
154
|
export function formatTaskLine(task: RestTask): string {
|
|
151
155
|
const notes = [`owner ${task.owner.login}`];
|
|
152
|
-
if (task.
|
|
153
|
-
notes.push(`
|
|
156
|
+
if (task.workers.length > 0) {
|
|
157
|
+
notes.push(`workers ${logins(task.workers)}`);
|
|
154
158
|
}
|
|
155
159
|
if (task.category !== null) {
|
|
156
160
|
notes.push(task.category.key);
|
|
@@ -199,7 +203,7 @@ export function formatTask(
|
|
|
199
203
|
): string {
|
|
200
204
|
const lines = [
|
|
201
205
|
`#${task.number} ${task.title}`,
|
|
202
|
-
`state: ${task.state} priority: ${task.priority} owner: ${task.owner.login}
|
|
206
|
+
`state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} workers: ${task.workers.length === 0 ? "nobody" : logins(task.workers)}`,
|
|
203
207
|
`id: ${task.id}${task.parentId === null ? "" : ` part of: ${task.parentId}`}`,
|
|
204
208
|
...scheduleLines(task),
|
|
205
209
|
];
|
|
@@ -375,9 +379,9 @@ export function formatCategoryLine(category: RestCategory): string {
|
|
|
375
379
|
export function formatWork(work: RestWork): string {
|
|
376
380
|
const lines: string[] = [];
|
|
377
381
|
if (work.tasks.length === 0) {
|
|
378
|
-
lines.push("No open task is
|
|
382
|
+
lines.push("No open task is on your plate.");
|
|
379
383
|
} else {
|
|
380
|
-
lines.push("tasks
|
|
384
|
+
lines.push("tasks you work on:");
|
|
381
385
|
for (const { project, task } of work.tasks) {
|
|
382
386
|
lines.push(` ${project.key} ${formatTaskLine(task)}`);
|
|
383
387
|
}
|
|
@@ -392,10 +396,10 @@ export function formatWork(work: RestWork): string {
|
|
|
392
396
|
/** Another person's plate: their open tasks, each with its project; no decisions, which wait for anyone. */
|
|
393
397
|
export function formatPersonWork(person: RestPerson, work: RestPersonWork): string {
|
|
394
398
|
if (work.tasks.length === 0) {
|
|
395
|
-
return `No open task is
|
|
399
|
+
return `No open task is on ${person.login}'s plate.`;
|
|
396
400
|
}
|
|
397
401
|
return [
|
|
398
|
-
`tasks
|
|
402
|
+
`tasks ${person.login} works on:`,
|
|
399
403
|
...work.tasks.map(({ project, task }) => ` ${project.key} ${formatTaskLine(task)}`),
|
|
400
404
|
].join("\n");
|
|
401
405
|
}
|
package/src/cli/help.ts
CHANGED
|
@@ -23,9 +23,11 @@ import {
|
|
|
23
23
|
MAX_QUESTION_LENGTH,
|
|
24
24
|
MAX_REPORTS_PER_RUN,
|
|
25
25
|
MAX_RULES_LENGTH,
|
|
26
|
+
MAX_SEARCH_LENGTH,
|
|
26
27
|
MAX_SUMMARY_LENGTH,
|
|
27
28
|
MAX_TITLE_LENGTH,
|
|
28
29
|
MAX_VERSIONS_PER_DOCUMENT,
|
|
30
|
+
MAX_WORKERS,
|
|
29
31
|
MIN_OPTIONS,
|
|
30
32
|
} from "../limits.js";
|
|
31
33
|
import { COLOR_NAMES } from "../vocabulary.js";
|
|
@@ -84,17 +86,17 @@ const DAY = "a day as 2026-10-24, today, tomorrow, or +7 for seven days from tod
|
|
|
84
86
|
const COLORS = `one of ${COLOR_NAMES.join(", ")}`;
|
|
85
87
|
const KEY = "lowercase letters, digits and hyphens";
|
|
86
88
|
const TASK_FIELD_FLAGS =
|
|
87
|
-
"[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--
|
|
89
|
+
"[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--worker <login|me|none>]... [--parent <number|id>]";
|
|
88
90
|
const TASK_FIELD_LIMITS = [
|
|
89
91
|
`starts, due: ${DAY}; the due day is not before the start`,
|
|
90
92
|
"category: the key of one of the workspace's categories (console categories); goal: the id of one of the project's goals (console goals)",
|
|
91
|
-
|
|
93
|
+
`worker: a login of the workspace, or me; given again, or with commas between, for several, up to ${MAX_WORKERS}, in place of the workers there were; none alone for nobody. parent: a task of the project, by its number or its id; console tasks --search <words> finds one by its title`,
|
|
92
94
|
];
|
|
93
95
|
const TASK_FIELD_REFUSALS = [
|
|
94
96
|
"task.invalid_category: no category has that key",
|
|
95
97
|
"task.invalid_goal: the goal is not one of this project",
|
|
96
98
|
"task.invalid_dates: the due day is before the start",
|
|
97
|
-
"task.
|
|
99
|
+
"task.invalid_worker: a person named as a worker may not work in the project",
|
|
98
100
|
"task.invalid_parent: the parent is not a task of the project, or would make a loop",
|
|
99
101
|
];
|
|
100
102
|
|
|
@@ -147,14 +149,15 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
147
149
|
synopsis:
|
|
148
150
|
"What is on your plate across your projects: your open tasks, and the decisions waiting.",
|
|
149
151
|
about:
|
|
150
|
-
"What is on your plate across the projects you may see: the open tasks
|
|
152
|
+
"What is on your plate across the projects you may see: the open tasks you work on, the sooner due first, each with its project; and the decisions waiting for a person. Needs no project. --person <login> shows another person's plate, for an administrator or for an owner of a project they are in: the open tasks they work on within the projects you may see, without the decisions, which wait for anyone.",
|
|
151
153
|
},
|
|
152
154
|
tasks: {
|
|
153
155
|
usage:
|
|
154
|
-
"tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--unassigned] [--archived] [--limit <n>] [--offset <n>]",
|
|
156
|
+
"tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--unassigned] [--search <words>] [--archived] [--limit <n>] [--offset <n>]",
|
|
155
157
|
synopsis:
|
|
156
|
-
"The tasks on the board, newest first, narrowed by state,
|
|
157
|
-
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
|
|
158
|
+
"The tasks on the board, newest first, narrowed by state, worker, category, goal, due day or words of the title.",
|
|
159
|
+
about: `The tasks on the board, newest first: number, state, priority, title, who owns and who works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those you work on, --unassigned those nobody works on (not with --mine), --category and --goal those sorted into one, --due those due by a day: today, this week (through its last day, the week beginning as the workspace says, console workspace, or as your language has it), or overdue (the open ones past their day), --search those whose title has every word given, whatever the case, and the task of that number for a number (12 or #12): how a task is found by its title, as a parent or otherwise. The tasks put away (ADR-0029) are left out unless --archived asks for them alone. 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.`,
|
|
160
|
+
limits: [`search: 1 to ${MAX_SEARCH_LENGTH} characters`],
|
|
158
161
|
},
|
|
159
162
|
task: {
|
|
160
163
|
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} | task archive <number|id> | task archive --goal <id> | task restore <number|id>`,
|
|
@@ -171,10 +174,11 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
171
174
|
refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
|
|
172
175
|
},
|
|
173
176
|
goals: {
|
|
174
|
-
usage:
|
|
177
|
+
usage:
|
|
178
|
+
"goals [--category <key>] [--state <state>] [--search <words>] [--limit <n>] [--offset <n>]",
|
|
175
179
|
synopsis: "The roadmap: the project's goals, with their state, their days and how far each is.",
|
|
176
|
-
about:
|
|
177
|
-
|
|
180
|
+
about: `The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its category, the days it starts and is meant to be reached by, and how far it is, counted from the tasks that work towards it. --category keeps those sorted into one, --state those in one state, --search those whose title has every word given, whatever the case: how a goal's id is found for --goal. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more.`,
|
|
181
|
+
limits: [`search: 1 to ${MAX_SEARCH_LENGTH} characters`],
|
|
178
182
|
},
|
|
179
183
|
goal: {
|
|
180
184
|
usage:
|
|
@@ -206,7 +210,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
206
210
|
usage: "people",
|
|
207
211
|
synopsis: "The people you see, by login, with what each is in the workspace.",
|
|
208
212
|
about:
|
|
209
|
-
"The people of the workspace you see, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Everyone, unless the workspace shows a member only the people they share a project with (console workspace says which; an administrator sees everyone). Then, where you see everyone, the logins an administrator let in that nobody has signed in with yet. The logins are what --
|
|
213
|
+
"The people of the workspace you see, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Everyone, unless the workspace shows a member only the people they share a project with (console workspace says which; an administrator sees everyone). Then, where you see everyone, the logins an administrator let in that nobody has signed in with yet. The logins are what --worker, member and person take; a login is found exactly, so an owner lists someone the list does not show.",
|
|
210
214
|
},
|
|
211
215
|
workspace: {
|
|
212
216
|
usage:
|
package/src/cli/main.ts
CHANGED
|
@@ -60,6 +60,7 @@ process.exitCode = await runCli(process.argv.slice(2), {
|
|
|
60
60
|
exists: existsSync,
|
|
61
61
|
}),
|
|
62
62
|
notices: createNoticeStore(configDir),
|
|
63
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
63
64
|
skillsDir: existsSync(skillsDir) ? skillsDir : undefined,
|
|
64
65
|
listen: listenOn,
|
|
65
66
|
});
|
package/src/client.ts
CHANGED
|
@@ -214,14 +214,18 @@ export interface Page {
|
|
|
214
214
|
readonly offset?: number | undefined;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
/** What the
|
|
218
|
-
|
|
219
|
-
export interface GoalFilter {
|
|
217
|
+
/** What the roadmap is narrowed by: the goals in one state, the open ones, those sorted into one category (ADR-0026), or those whose title has some words; and which page of it. */
|
|
218
|
+
export interface GoalFilter extends Page {
|
|
220
219
|
readonly state?: GoalState | undefined;
|
|
220
|
+
/** True for the goals planned or active; false for the reached and the dropped. */
|
|
221
|
+
readonly open?: boolean | undefined;
|
|
221
222
|
/** The key of a category of the workspace. */
|
|
222
223
|
readonly category?: string | undefined;
|
|
224
|
+
/** Words of the title, each found in it whatever the case. */
|
|
225
|
+
readonly q?: string | undefined;
|
|
223
226
|
}
|
|
224
227
|
|
|
228
|
+
/** What the board is narrowed to: one state, the open ones, a category, a goal, a worker, what is due by a day, or words of the title; and which page of it. */
|
|
225
229
|
export interface TaskFilter extends Page {
|
|
226
230
|
readonly state?: TaskState | undefined;
|
|
227
231
|
/** True for the tasks not done or dropped; false for the rest. */
|
|
@@ -230,10 +234,12 @@ export interface TaskFilter extends Page {
|
|
|
230
234
|
readonly category?: string | undefined;
|
|
231
235
|
/** The id of a goal. */
|
|
232
236
|
readonly goal?: string | undefined;
|
|
233
|
-
/** The id of
|
|
234
|
-
readonly
|
|
235
|
-
/** True for the tasks
|
|
237
|
+
/** The id of a person among the workers of the tasks. */
|
|
238
|
+
readonly worker?: string | undefined;
|
|
239
|
+
/** True for the tasks with no worker. */
|
|
236
240
|
readonly unassigned?: boolean | undefined;
|
|
241
|
+
/** Words of the title, each found in it whatever the case; `#12` or `12` finds the task of that number too. */
|
|
242
|
+
readonly q?: string | undefined;
|
|
237
243
|
/** A day (`2026-10-20`): the tasks due on it or before it. */
|
|
238
244
|
readonly dueBy?: string | undefined;
|
|
239
245
|
/** True for the tasks put away alone (ADR-0029); false, or left out, for the current ones. */
|
|
@@ -380,8 +386,7 @@ export interface ProjectClient {
|
|
|
380
386
|
attachFile(path: string, input: FileUpload): Promise<RestDocument>;
|
|
381
387
|
/** Where the bytes of a file attached to a document are read, by the file's id. */
|
|
382
388
|
documentFileUrl(path: string, fileId: string): string;
|
|
383
|
-
/** The roadmap (ADR-0019)
|
|
384
|
-
/** The roadmap, or what the filter keeps of it: the active goals first. */
|
|
389
|
+
/** The roadmap (ADR-0019), or what the filter keeps of it, a page at a time: the active goals first, each with its tasks counted by state. */
|
|
385
390
|
goals(filter?: GoalFilter, signal?: AbortSignal): Promise<RestGoals>;
|
|
386
391
|
goal(id: string, signal?: AbortSignal): Promise<RestGoal>;
|
|
387
392
|
/** Sets a goal; for an owner, with a session or a token. */
|
|
@@ -642,9 +647,10 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
642
647
|
open: flag(filter.open),
|
|
643
648
|
category: filter.category,
|
|
644
649
|
goal: filter.goal,
|
|
645
|
-
|
|
650
|
+
worker: filter.worker,
|
|
646
651
|
unassigned: flag(filter.unassigned),
|
|
647
652
|
dueBy: filter.dueBy,
|
|
653
|
+
q: filter.q,
|
|
648
654
|
limit: number(filter.limit),
|
|
649
655
|
offset: number(filter.offset),
|
|
650
656
|
}),
|
|
@@ -862,7 +868,14 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
862
868
|
goals: (filter = {}, signal) =>
|
|
863
869
|
request(
|
|
864
870
|
"GET",
|
|
865
|
-
withQuery(at("goals"), {
|
|
871
|
+
withQuery(at("goals"), {
|
|
872
|
+
state: filter.state,
|
|
873
|
+
open: flag(filter.open),
|
|
874
|
+
category: filter.category,
|
|
875
|
+
q: filter.q,
|
|
876
|
+
limit: number(filter.limit),
|
|
877
|
+
offset: number(filter.offset),
|
|
878
|
+
}),
|
|
866
879
|
undefined,
|
|
867
880
|
restGoalsSchema,
|
|
868
881
|
signal,
|
package/src/compat.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Whether the command and the console go together (ADR-0025). The command names its version on
|
|
1
|
+
// Whether the command and the console go together (ADR-0025, ADR-0038). The command names its version on
|
|
2
2
|
// every request, in the product token of its user-agent; the console answers every request of
|
|
3
3
|
// the API with its own version and the range of command versions it takes, and refuses a
|
|
4
4
|
// command outside the range with `426` and the words to update. One rule for both sides, kept
|
|
5
5
|
// here so that the command and the console read the same one, and narrowed by the console's
|
|
6
|
-
// policy below only, never widened: what the rule promises stays predictable.
|
|
6
|
+
// policy below only, never widened: what the rule promises stays predictable. The command holds
|
|
7
|
+
// the console to a floor of its own the same way (ADR-0038).
|
|
7
8
|
|
|
8
9
|
/** The product token the command names itself with: `skillcdn-console/<version>`. */
|
|
9
10
|
export const CLIENT_PRODUCT = "skillcdn-console";
|
|
@@ -74,15 +75,19 @@ export function inRange(version: Version, range: VersionRange): boolean {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
|
-
* The common rule. Before 1.0, a command
|
|
78
|
-
*
|
|
79
|
-
* console's own
|
|
80
|
-
*
|
|
81
|
-
* major breaks, as the version
|
|
78
|
+
* The common rule. Before 1.0, the line: a command and a console of one major.minor go
|
|
79
|
+
* together whatever the patch, since the minor is SkillCDN's line and a patch is a release of
|
|
80
|
+
* the console's own (ADR-0034), and a release that changes what passes between them raises the
|
|
81
|
+
* floors below instead (ADR-0038). From 1.0, the console's major and a minor within one of the
|
|
82
|
+
* console's, either way, whatever the patch: a minor adds and a major breaks, as the version
|
|
83
|
+
* promises from then on.
|
|
82
84
|
*/
|
|
83
85
|
export function commonRange(server: Version): VersionRange {
|
|
84
86
|
if (server.major === 0) {
|
|
85
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
atLeast: { major: 0, minor: server.minor, patch: 0 },
|
|
89
|
+
below: { major: 0, minor: server.minor + 1, patch: 0 },
|
|
90
|
+
};
|
|
86
91
|
}
|
|
87
92
|
return {
|
|
88
93
|
atLeast: { major: server.major, minor: Math.max(0, server.minor - 1), patch: 0 },
|
|
@@ -103,13 +108,25 @@ export interface ClientPolicy {
|
|
|
103
108
|
readonly refused?: readonly string[] | undefined;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
111
|
+
/**
|
|
112
|
+
* The console's policy: the oldest command this console answers, raised by the release that
|
|
113
|
+
* last changed what the command reads or sends (ADR-0038), whose changelog entry begins with
|
|
114
|
+
* `Breaking:`; `pnpm check` holds it to a version the package has, or is about to have.
|
|
115
|
+
*/
|
|
116
|
+
export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.3.2" };
|
|
108
117
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The oldest console this command works with (ADR-0038): raised by the release that last
|
|
120
|
+
* changed what the command asks of the console, whose changelog entry begins with `Breaking:`.
|
|
121
|
+
* The command compares it with the version every answer names and refuses a console below it
|
|
122
|
+
* itself, naming the command to install instead; `pnpm check` holds it as it holds the
|
|
123
|
+
* console's floor.
|
|
124
|
+
*/
|
|
125
|
+
export const CONSOLE_AT_LEAST = "0.3.2";
|
|
126
|
+
|
|
127
|
+
/** `common` from `atLeast` on, where that floor is within it; `common` as it is otherwise. */
|
|
128
|
+
function narrowed(common: VersionRange, atLeast: string | undefined): VersionRange {
|
|
129
|
+
const floor = parseVersion(atLeast);
|
|
113
130
|
return floor !== undefined &&
|
|
114
131
|
compareVersions(floor, common.atLeast) > 0 &&
|
|
115
132
|
compareVersions(floor, common.below) < 0
|
|
@@ -117,6 +134,16 @@ export function clientRange(server: Version, policy: ClientPolicy = CLIENT_POLIC
|
|
|
117
134
|
: common;
|
|
118
135
|
}
|
|
119
136
|
|
|
137
|
+
/** The range of command versions a console of `server` takes: the common rule, narrowed by the policy's floor where that floor is within it. */
|
|
138
|
+
export function clientRange(server: Version, policy: ClientPolicy = CLIENT_POLICY): VersionRange {
|
|
139
|
+
return narrowed(commonRange(server), policy.atLeast);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The range of console versions a command of `client` works with: the common rule, narrowed by the command's floor where that floor is within it. */
|
|
143
|
+
export function consoleRange(client: Version, atLeast: string = CONSOLE_AT_LEAST): VersionRange {
|
|
144
|
+
return narrowed(commonRange(client), atLeast);
|
|
145
|
+
}
|
|
146
|
+
|
|
120
147
|
/**
|
|
121
148
|
* What a command of one version is to a console of another: within the range and the same
|
|
122
149
|
* (`current`), older (`behind`) or newer (`ahead`), which the console answers; or outside it,
|
|
@@ -152,6 +179,28 @@ export function judgeClient(
|
|
|
152
179
|
return { verdict: order === 0 ? "current" : order < 0 ? "behind" : "ahead", range };
|
|
153
180
|
}
|
|
154
181
|
|
|
182
|
+
/**
|
|
183
|
+
* What a console of one version is to a command of another (ADR-0038): within the range the
|
|
184
|
+
* command works with, the same (`current`), older (`behind`) or newer (`ahead`); below it
|
|
185
|
+
* (`outdated`), which the command refuses itself, naming the command to install; or beyond it
|
|
186
|
+
* (`too_new`), which the console refuses the command for.
|
|
187
|
+
*/
|
|
188
|
+
export function judgeConsole(
|
|
189
|
+
server: Version,
|
|
190
|
+
client: Version,
|
|
191
|
+
atLeast: string = CONSOLE_AT_LEAST,
|
|
192
|
+
): ClientJudgement {
|
|
193
|
+
const range = consoleRange(client, atLeast);
|
|
194
|
+
if (compareVersions(server, range.atLeast) < 0) {
|
|
195
|
+
return { verdict: "outdated", range };
|
|
196
|
+
}
|
|
197
|
+
if (compareVersions(server, range.below) >= 0) {
|
|
198
|
+
return { verdict: "too_new", range };
|
|
199
|
+
}
|
|
200
|
+
const order = compareVersions(server, client);
|
|
201
|
+
return { verdict: order === 0 ? "current" : order < 0 ? "behind" : "ahead", range };
|
|
202
|
+
}
|
|
203
|
+
|
|
155
204
|
/** What the command sends as its user-agent: its product token, and, in a comment, where it runs. */
|
|
156
205
|
export function userAgentOf(version: string, comment?: string): string {
|
|
157
206
|
// A comment holds printable ASCII without parentheses or backslashes; nothing else is said.
|
package/src/components/board.tsx
CHANGED
|
@@ -20,7 +20,6 @@ import { useWords } from "../i18n/index.js";
|
|
|
20
20
|
import type { BoardFilter } from "../router.js";
|
|
21
21
|
import { isPlainClick } from "./shell.js";
|
|
22
22
|
import {
|
|
23
|
-
Avatar,
|
|
24
23
|
Badge,
|
|
25
24
|
Button,
|
|
26
25
|
CategoryBadge,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
DueBadge,
|
|
29
28
|
PriorityBadge,
|
|
30
29
|
personName,
|
|
30
|
+
Workers,
|
|
31
31
|
} from "./ui.js";
|
|
32
32
|
|
|
33
33
|
// The board: one column per state, the tasks in it, the sooner due first and the newest after.
|
|
@@ -165,7 +165,7 @@ export function TaskCard(props: TaskCardProps) {
|
|
|
165
165
|
</Badge>
|
|
166
166
|
)}
|
|
167
167
|
<span className="sc-card-spacer" />
|
|
168
|
-
|
|
168
|
+
<Workers workers={task.workers} size="sm" />
|
|
169
169
|
</div>
|
|
170
170
|
</article>
|
|
171
171
|
);
|
|
@@ -584,10 +584,16 @@ export function filterTasks(
|
|
|
584
584
|
if (filter.goal !== undefined && (task.goalId ?? "") !== filter.goal) {
|
|
585
585
|
return false;
|
|
586
586
|
}
|
|
587
|
-
if (
|
|
587
|
+
if (
|
|
588
|
+
filter.person !== undefined &&
|
|
589
|
+
!task.workers.some((worker) => worker.id === filter.person)
|
|
590
|
+
) {
|
|
588
591
|
return false;
|
|
589
592
|
}
|
|
590
|
-
if (
|
|
593
|
+
if (
|
|
594
|
+
filter.mine === true &&
|
|
595
|
+
(me === undefined || !task.workers.some((worker) => worker.id === me.id))
|
|
596
|
+
) {
|
|
591
597
|
return false;
|
|
592
598
|
}
|
|
593
599
|
return true;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReactNode, type SyntheticEvent, useState } from "react";
|
|
1
|
+
import { type CSSProperties, type ReactNode, type SyntheticEvent, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
OPEN_TASK_STATES,
|
|
4
4
|
type RestCategory,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
weekOf,
|
|
26
26
|
} from "../dates.js";
|
|
27
27
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
28
|
-
import {
|
|
28
|
+
import { CategoryBadge, cx, PersonChip, personName, StateBadge, Time, Workers } from "./ui.js";
|
|
29
29
|
|
|
30
30
|
// The calendar of a project (ADR-0019): the month as a grid, or one week on its own. A task
|
|
31
31
|
// planned from a start to a due day lies across its days as a bar, in a lane of its own within
|
|
@@ -262,12 +262,18 @@ export function TaskTip(props: TaskTipProps) {
|
|
|
262
262
|
</dd>
|
|
263
263
|
</div>
|
|
264
264
|
<div>
|
|
265
|
-
<dt>{words.task.
|
|
265
|
+
<dt>{words.task.workers}</dt>
|
|
266
266
|
<dd>
|
|
267
|
-
{task.
|
|
267
|
+
{task.workers.length === 0 ? (
|
|
268
268
|
<span className="sc-muted">{words.common.nobody}</span>
|
|
269
269
|
) : (
|
|
270
|
-
<
|
|
270
|
+
<ul className="sc-worker-list">
|
|
271
|
+
{task.workers.map((worker) => (
|
|
272
|
+
<li key={worker.id}>
|
|
273
|
+
<PersonChip person={worker} />
|
|
274
|
+
</li>
|
|
275
|
+
))}
|
|
276
|
+
</ul>
|
|
271
277
|
)}
|
|
272
278
|
</dd>
|
|
273
279
|
</div>
|
|
@@ -356,6 +362,9 @@ function goalsOnDays(goals: readonly RestGoal[]): ReadonlyMap<string, readonly R
|
|
|
356
362
|
return on;
|
|
357
363
|
}
|
|
358
364
|
|
|
365
|
+
/** How many days a bar has in the week, for its title to run along: a custom property the stylesheet reads. */
|
|
366
|
+
const spanStyle = (span: number): CSSProperties => ({ "--sc-span": span }) as CSSProperties;
|
|
367
|
+
|
|
359
368
|
/** One week as a row of cells: the bars across the days in their lanes, then the tasks on each day. */
|
|
360
369
|
function WeekRow(props: WeekRowProps) {
|
|
361
370
|
const { week, segments, now } = props;
|
|
@@ -371,9 +380,7 @@ function WeekRow(props: WeekRowProps) {
|
|
|
371
380
|
<TaskTip task={task} goals={props.goals} place={props.pointed.place} />
|
|
372
381
|
) : null;
|
|
373
382
|
const who = (task: RestTask): ReactNode =>
|
|
374
|
-
props.people === true
|
|
375
|
-
<Avatar person={task.assignee} size="xs" />
|
|
376
|
-
) : null;
|
|
383
|
+
props.people === true ? <Workers workers={task.workers} size="xs" shown={2} /> : null;
|
|
377
384
|
const lanes = segments.reduce((most, segment) => Math.max(most, segment.lane + 1), 0);
|
|
378
385
|
return (
|
|
379
386
|
<tr>
|
|
@@ -413,6 +420,13 @@ function WeekRow(props: WeekRowProps) {
|
|
|
413
420
|
return <span key={lane} className="sc-calendar-lane" aria-hidden="true" />;
|
|
414
421
|
}
|
|
415
422
|
const { task } = segment;
|
|
423
|
+
// The first piece carries the title; with more days in the week, the title runs along
|
|
424
|
+
// the bar as far as they reach, over the pieces after it, instead of stopping at the
|
|
425
|
+
// first day's edge.
|
|
426
|
+
const span = segment.to - segment.from + 1;
|
|
427
|
+
const spanning = segment.from === column && span > 1;
|
|
428
|
+
// Every piece of the task under the pointer lights together, in every week it crosses.
|
|
429
|
+
const pointed = props.pointed !== null && props.pointed.task.id === task.id;
|
|
416
430
|
return (
|
|
417
431
|
<a
|
|
418
432
|
key={task.id}
|
|
@@ -421,8 +435,11 @@ function WeekRow(props: WeekRowProps) {
|
|
|
421
435
|
`sc-category-${task.category?.color ?? "gray"}`,
|
|
422
436
|
segment.from === column && segment.begins && "sc-calendar-bar-begins",
|
|
423
437
|
segment.to === column && segment.ends && "sc-calendar-bar-ends",
|
|
438
|
+
spanning && "sc-calendar-bar-spanning",
|
|
439
|
+
pointed && "sc-calendar-bar-pointed",
|
|
424
440
|
tone(task, now),
|
|
425
441
|
)}
|
|
442
|
+
style={spanning ? spanStyle(span) : undefined}
|
|
426
443
|
href={props.taskHref(task)}
|
|
427
444
|
{...pointing(task, cell.day)}
|
|
428
445
|
>
|
|
@@ -430,7 +447,14 @@ function WeekRow(props: WeekRowProps) {
|
|
|
430
447
|
<>
|
|
431
448
|
{props.markOf?.(task)}
|
|
432
449
|
{who(task)}
|
|
433
|
-
<span
|
|
450
|
+
<span
|
|
451
|
+
className={cx(
|
|
452
|
+
"sc-calendar-task-title",
|
|
453
|
+
spanning && "sc-calendar-task-title-spanning",
|
|
454
|
+
)}
|
|
455
|
+
>
|
|
456
|
+
{task.title}
|
|
457
|
+
</span>
|
|
434
458
|
</>
|
|
435
459
|
)}
|
|
436
460
|
{tipIn(task, cell.day)}
|
|
@@ -644,39 +668,44 @@ export function Week(props: WeekProps) {
|
|
|
644
668
|
|
|
645
669
|
/** One person's share of the open work, as the table of who has what says it. */
|
|
646
670
|
export interface WorkloadRow {
|
|
647
|
-
/** The person, or nobody for the tasks
|
|
671
|
+
/** The person, or nobody for the tasks with no worker. */
|
|
648
672
|
readonly person: RestPerson | undefined;
|
|
649
673
|
readonly open: number;
|
|
650
674
|
readonly inProgress: number;
|
|
651
|
-
/** Open, and due
|
|
652
|
-
readonly
|
|
675
|
+
/** Open, and due from today through the last day of this week, as the workspace begins its weeks (ADR-0039). */
|
|
676
|
+
readonly dueThisWeek: number;
|
|
653
677
|
/** Open, and past its day. */
|
|
654
678
|
readonly overdue: number;
|
|
655
679
|
}
|
|
656
680
|
|
|
657
|
-
/**
|
|
658
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Who has what, from the open tasks: a task counted for each of its workers, by name, and the
|
|
683
|
+
* tasks with none last; this week is the days from today through the week's last day, the week
|
|
684
|
+
* beginning on `firstDay` (1 for Monday, 7 for Sunday).
|
|
685
|
+
*/
|
|
686
|
+
export function workloadOf(tasks: readonly RestTask[], today: string, firstDay = 1): WorkloadRow[] {
|
|
687
|
+
const weekEnd = weekOf(today, firstDay).at(-1) ?? today;
|
|
659
688
|
const rows = new Map<string, { person: RestPerson | undefined; tasks: RestTask[] }>();
|
|
660
689
|
for (const task of tasks.filter(isOpen)) {
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
row
|
|
690
|
+
const workers: readonly (RestPerson | undefined)[] =
|
|
691
|
+
task.workers.length === 0 ? [undefined] : task.workers;
|
|
692
|
+
for (const worker of workers) {
|
|
693
|
+
const key = worker?.id ?? "";
|
|
694
|
+
const row = rows.get(key);
|
|
695
|
+
if (row === undefined) {
|
|
696
|
+
rows.set(key, { person: worker, tasks: [task] });
|
|
697
|
+
} else {
|
|
698
|
+
row.tasks.push(task);
|
|
699
|
+
}
|
|
667
700
|
}
|
|
668
701
|
}
|
|
669
702
|
const count = (list: readonly RestTask[]): Omit<WorkloadRow, "person"> => ({
|
|
670
703
|
open: list.length,
|
|
671
704
|
inProgress: list.filter((task) => task.state === "in_progress" || task.state === "in_review")
|
|
672
705
|
.length,
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
const days = daysBetween(today, task.dueOn);
|
|
678
|
-
return days >= 0 && days <= 7;
|
|
679
|
-
}).length,
|
|
706
|
+
dueThisWeek: list.filter(
|
|
707
|
+
(task) => task.dueOn !== null && task.dueOn >= today && task.dueOn <= weekEnd,
|
|
708
|
+
).length,
|
|
680
709
|
overdue: list.filter((task) => task.dueOn !== null && task.dueOn < today).length,
|
|
681
710
|
});
|
|
682
711
|
return [...rows.values()]
|
|
@@ -695,14 +724,21 @@ export function workloadOf(tasks: readonly RestTask[], today: string): WorkloadR
|
|
|
695
724
|
export interface WorkloadProps {
|
|
696
725
|
readonly tasks: readonly RestTask[];
|
|
697
726
|
readonly today?: string | undefined;
|
|
698
|
-
/**
|
|
727
|
+
/** The day the week begins on, as the workspace says (ADR-0035); the language's when left out. */
|
|
728
|
+
readonly weekStart?: WeekStart | undefined;
|
|
729
|
+
/** Where the board is, narrowed to one person's tasks, or to those with no worker. */
|
|
699
730
|
readonly personHref?: ((person: RestPerson | undefined) => string) | undefined;
|
|
700
731
|
}
|
|
701
732
|
|
|
702
|
-
/** Who has what: a row per person, and what of theirs is late or due
|
|
733
|
+
/** Who has what: a row per person, and what of theirs is late or due this week. */
|
|
703
734
|
export function Workload(props: WorkloadProps) {
|
|
704
735
|
const words = useWords().calendar.workload;
|
|
705
|
-
const
|
|
736
|
+
const { tag } = useLanguage();
|
|
737
|
+
const rows = workloadOf(
|
|
738
|
+
props.tasks,
|
|
739
|
+
props.today ?? todayOf(),
|
|
740
|
+
firstDayOfWeek(deviceLanguage(tag), props.weekStart),
|
|
741
|
+
);
|
|
706
742
|
if (rows.length === 0) {
|
|
707
743
|
return null;
|
|
708
744
|
}
|
|
@@ -713,7 +749,7 @@ export function Workload(props: WorkloadProps) {
|
|
|
713
749
|
<th scope="col">{words.person}</th>
|
|
714
750
|
<th scope="col">{words.open}</th>
|
|
715
751
|
<th scope="col">{words.inProgress}</th>
|
|
716
|
-
<th scope="col">{words.
|
|
752
|
+
<th scope="col">{words.dueThisWeek}</th>
|
|
717
753
|
<th scope="col">{words.overdue}</th>
|
|
718
754
|
</tr>
|
|
719
755
|
</thead>
|
|
@@ -731,7 +767,7 @@ export function Workload(props: WorkloadProps) {
|
|
|
731
767
|
<th scope="row">{href === undefined ? who : <a href={href}>{who}</a>}</th>
|
|
732
768
|
<td>{row.open}</td>
|
|
733
769
|
<td>{row.inProgress}</td>
|
|
734
|
-
<td className={cx(row.
|
|
770
|
+
<td className={cx(row.dueThisWeek > 0 && "sc-workload-soon")}>{row.dueThisWeek}</td>
|
|
735
771
|
<td className={cx(row.overdue > 0 && "sc-workload-late")}>{row.overdue}</td>
|
|
736
772
|
</tr>
|
|
737
773
|
);
|