@skillcdn/console 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/api.d.ts +4 -4
- 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 +4 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +201 -25
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +15 -11
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +59 -26
- 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/cli/pack.d.ts +7 -2
- package/dist/cli/pack.d.ts.map +1 -1
- package/dist/cli/pack.js +16 -2
- package/dist/cli/pack.js.map +1 -1
- package/dist/client.d.ts +20 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +17 -3
- 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 +12 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +57 -7
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +17 -7
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +89 -38
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-form.d.ts +12 -4
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +24 -6
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +4 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +9 -6
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/documents.d.ts.map +1 -1
- package/dist/components/documents.js +3 -3
- package/dist/components/documents.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +63 -8
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/my-decisions.d.ts +20 -0
- package/dist/components/my-decisions.d.ts.map +1 -0
- package/dist/components/my-decisions.js +36 -0
- package/dist/components/my-decisions.js.map +1 -0
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +1 -1
- package/dist/components/people.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/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -3
- package/dist/components/projects.js.map +1 -1
- package/dist/components/roadmap.js +1 -1
- package/dist/components/roadmap.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +1 -1
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +36 -1
- package/dist/components/shell.js.map +1 -1
- 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 +7 -6
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +17 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +26 -11
- 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 +1794 -433
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +93 -10
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +21 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +24 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +45 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +52 -10
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +54 -12
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- 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/router.d.ts +9 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +12 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +3 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +3 -1
- package/dist/routes.js.map +1 -1
- package/dist/rules.js +1 -1
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +252 -12
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +27 -10
- 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 +14 -13
- package/docs/specs/rest.md +24 -19
- package/package.json +2 -2
- package/skills/building-a-console/SKILL.md +4 -4
- package/skills/building-a-console/references/the-default-console.md +4 -4
- package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
- 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 +8 -6
- package/skills/working-the-board/references/terms.md +3 -3
- package/src/api.ts +7 -0
- package/src/cli/cli.ts +254 -25
- package/src/cli/format.ts +22 -11
- package/src/cli/help.ts +63 -26
- package/src/cli/main.ts +1 -0
- package/src/cli/pack.ts +17 -1
- package/src/client.ts +44 -11
- package/src/compat.ts +63 -14
- package/src/components/board.tsx +124 -25
- package/src/components/calendar.tsx +258 -117
- package/src/components/decision-form.tsx +71 -23
- package/src/components/decision-list.tsx +30 -3
- package/src/components/documents.tsx +27 -11
- package/src/components/event-feed.tsx +116 -33
- package/src/components/my-decisions.tsx +138 -0
- package/src/components/people.tsx +3 -1
- package/src/components/picker.tsx +297 -0
- package/src/components/projects.tsx +7 -5
- package/src/components/roadmap.tsx +1 -1
- package/src/components/runs.tsx +5 -1
- package/src/components/shell.tsx +36 -1
- package/src/components/task-form.tsx +149 -63
- package/src/components/task-list.tsx +3 -3
- package/src/components/task-view.tsx +71 -56
- package/src/components/ui.tsx +89 -11
- package/src/components/work.tsx +6 -4
- package/src/console.tsx +157 -29
- package/src/data.ts +61 -1
- package/src/i18n/en.ts +54 -10
- package/src/i18n/ko.ts +56 -12
- package/src/index.ts +19 -1
- package/src/limits.ts +2 -0
- package/src/router.ts +18 -1
- package/src/routes.ts +3 -1
- package/src/rules.ts +1 -1
- package/src/schemas.ts +29 -9
- package/src/styles/console.css +1794 -433
- package/src/vocabulary.ts +3 -0
package/src/components/ui.tsx
CHANGED
|
@@ -43,11 +43,23 @@ export function Button(
|
|
|
43
43
|
|
|
44
44
|
export function Badge(props: {
|
|
45
45
|
readonly tone?: "neutral" | "accent" | "point" | "success" | "warning" | "danger";
|
|
46
|
+
/** What the badge says, which the stylesheet shows in its shape: a state carries a dot, a priority is outlined, a due day is a square-cornered tag, a count is figures. */
|
|
47
|
+
readonly kind?: "state" | "priority" | "due" | "count" | undefined;
|
|
48
|
+
/** A class of the badge's own beside the tone's and the kind's, for a step within a kind. */
|
|
49
|
+
readonly className?: string | undefined;
|
|
46
50
|
readonly title?: string;
|
|
47
51
|
readonly children: ReactNode;
|
|
48
52
|
}) {
|
|
49
53
|
return (
|
|
50
|
-
<span
|
|
54
|
+
<span
|
|
55
|
+
className={cx(
|
|
56
|
+
"sc-badge",
|
|
57
|
+
`sc-badge-${props.tone ?? "neutral"}`,
|
|
58
|
+
props.kind !== undefined && `sc-badge-kind-${props.kind}`,
|
|
59
|
+
props.className,
|
|
60
|
+
)}
|
|
61
|
+
title={props.title}
|
|
62
|
+
>
|
|
51
63
|
{props.children}
|
|
52
64
|
</span>
|
|
53
65
|
);
|
|
@@ -145,6 +157,31 @@ export function PersonChip(props: { readonly person: RestPerson; readonly size?:
|
|
|
145
157
|
);
|
|
146
158
|
}
|
|
147
159
|
|
|
160
|
+
/** How many of a task's workers are shown as pictures before the rest are a count. */
|
|
161
|
+
const WORKERS_SHOWN = 3;
|
|
162
|
+
|
|
163
|
+
/** The people at work on a task (ADR-0037), as pictures side by side with a count of the rest; nothing while nobody is. */
|
|
164
|
+
export function Workers(props: {
|
|
165
|
+
readonly workers: readonly RestPerson[];
|
|
166
|
+
readonly size?: "xs" | "sm";
|
|
167
|
+
/** How many are shown as pictures; the rest are a count. */
|
|
168
|
+
readonly shown?: number;
|
|
169
|
+
}) {
|
|
170
|
+
if (props.workers.length === 0) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const shown = props.shown ?? WORKERS_SHOWN;
|
|
174
|
+
const rest = props.workers.length - shown;
|
|
175
|
+
return (
|
|
176
|
+
<span className="sc-workers" title={props.workers.map(personName).join(", ")}>
|
|
177
|
+
{props.workers.slice(0, shown).map((worker) => (
|
|
178
|
+
<Avatar key={worker.id} person={worker} size={props.size ?? "sm"} />
|
|
179
|
+
))}
|
|
180
|
+
{rest > 0 && <span className="sc-workers-more">+{rest}</span>}
|
|
181
|
+
</span>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
148
185
|
/** The words of the vocabulary as people read them, in English: the default pack's. */
|
|
149
186
|
export const STATE_LABELS: Readonly<Record<TaskState, string>> = {
|
|
150
187
|
idea: "Idea",
|
|
@@ -170,7 +207,7 @@ export const ROLE_LABELS: Readonly<Record<PersonRole, string>> = {
|
|
|
170
207
|
export function RoleBadge(props: { readonly role: PersonRole }) {
|
|
171
208
|
const words = useWords();
|
|
172
209
|
return (
|
|
173
|
-
<Badge tone={props.role === "admin" ? "
|
|
210
|
+
<Badge tone={props.role === "admin" ? "accent" : "neutral"}>
|
|
174
211
|
{words.vocabulary.role[props.role]}
|
|
175
212
|
</Badge>
|
|
176
213
|
);
|
|
@@ -186,7 +223,11 @@ export function StateBadge(props: { readonly state: TaskState }) {
|
|
|
186
223
|
: props.state === "in_progress" || props.state === "in_review"
|
|
187
224
|
? "accent"
|
|
188
225
|
: "neutral";
|
|
189
|
-
return
|
|
226
|
+
return (
|
|
227
|
+
<Badge tone={tone} kind="state">
|
|
228
|
+
{words.vocabulary.state[props.state]}
|
|
229
|
+
</Badge>
|
|
230
|
+
);
|
|
190
231
|
}
|
|
191
232
|
|
|
192
233
|
export function PriorityBadge(props: { readonly priority: TaskPriority }) {
|
|
@@ -196,7 +237,11 @@ export function PriorityBadge(props: { readonly priority: TaskPriority }) {
|
|
|
196
237
|
}
|
|
197
238
|
const tone =
|
|
198
239
|
props.priority === "urgent" ? "danger" : props.priority === "high" ? "point" : "neutral";
|
|
199
|
-
return
|
|
240
|
+
return (
|
|
241
|
+
<Badge tone={tone} kind="priority">
|
|
242
|
+
{words.vocabulary.priority[props.priority]}
|
|
243
|
+
</Badge>
|
|
244
|
+
);
|
|
200
245
|
}
|
|
201
246
|
|
|
202
247
|
/** A category as a badge, in its colour: a class the stylesheet paints, never a value typed by hand. */
|
|
@@ -220,7 +265,11 @@ export function GoalStateBadge(props: { readonly state: GoalState }) {
|
|
|
220
265
|
: props.state === "dropped"
|
|
221
266
|
? "neutral"
|
|
222
267
|
: "neutral";
|
|
223
|
-
return
|
|
268
|
+
return (
|
|
269
|
+
<Badge tone={tone} kind="state">
|
|
270
|
+
{words.vocabulary.goalState[props.state]}
|
|
271
|
+
</Badge>
|
|
272
|
+
);
|
|
224
273
|
}
|
|
225
274
|
|
|
226
275
|
/**
|
|
@@ -241,29 +290,45 @@ export function DueBadge(props: {
|
|
|
241
290
|
const days = daysBetween(now, props.dueOn);
|
|
242
291
|
const day = formatDay(props.dueOn, tag);
|
|
243
292
|
if (!open) {
|
|
244
|
-
return
|
|
293
|
+
return (
|
|
294
|
+
<Badge tone="neutral" kind="due">
|
|
295
|
+
{words.board.due.on(day)}
|
|
296
|
+
</Badge>
|
|
297
|
+
);
|
|
245
298
|
}
|
|
246
299
|
if (days < 0) {
|
|
247
300
|
return (
|
|
248
|
-
<Badge tone="danger" title={day}>
|
|
301
|
+
<Badge tone="danger" kind="due" title={day}>
|
|
249
302
|
{words.board.due.overdue(-days)}
|
|
250
303
|
</Badge>
|
|
251
304
|
);
|
|
252
305
|
}
|
|
253
306
|
if (days === 0) {
|
|
254
|
-
return
|
|
307
|
+
return (
|
|
308
|
+
<Badge tone="point" kind="due" className="sc-badge-due-today">
|
|
309
|
+
{words.board.due.today}
|
|
310
|
+
</Badge>
|
|
311
|
+
);
|
|
255
312
|
}
|
|
256
313
|
if (days === 1) {
|
|
257
|
-
return
|
|
314
|
+
return (
|
|
315
|
+
<Badge tone="point" kind="due">
|
|
316
|
+
{words.board.due.tomorrow}
|
|
317
|
+
</Badge>
|
|
318
|
+
);
|
|
258
319
|
}
|
|
259
320
|
if (days <= 7) {
|
|
260
321
|
return (
|
|
261
|
-
<Badge tone="
|
|
322
|
+
<Badge tone="warning" kind="due" title={day}>
|
|
262
323
|
{words.board.due.inDays(days)}
|
|
263
324
|
</Badge>
|
|
264
325
|
);
|
|
265
326
|
}
|
|
266
|
-
return
|
|
327
|
+
return (
|
|
328
|
+
<Badge tone="neutral" kind="due">
|
|
329
|
+
{words.board.due.on(day)}
|
|
330
|
+
</Badge>
|
|
331
|
+
);
|
|
267
332
|
}
|
|
268
333
|
|
|
269
334
|
/** How far a goal is, read from its tasks: the done ones, the ones at work, and the rest. */
|
|
@@ -377,3 +442,16 @@ export function Time(props: {
|
|
|
377
442
|
</time>
|
|
378
443
|
);
|
|
379
444
|
}
|
|
445
|
+
|
|
446
|
+
/** A link that reads as a button: where a form is, for instance. */
|
|
447
|
+
export function LinkButton(props: {
|
|
448
|
+
readonly href: string;
|
|
449
|
+
readonly variant?: "primary" | "secondary";
|
|
450
|
+
readonly children: ReactNode;
|
|
451
|
+
}) {
|
|
452
|
+
return (
|
|
453
|
+
<a className={`sc-button sc-button-${props.variant ?? "secondary"}`} href={props.href}>
|
|
454
|
+
{props.children}
|
|
455
|
+
</a>
|
|
456
|
+
);
|
|
457
|
+
}
|
package/src/components/work.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { RestTask, RestWork, WeekStart } from "../api.js";
|
|
2
|
-
import {
|
|
3
|
-
import { useWords } from "../i18n/index.js";
|
|
2
|
+
import { deviceLanguage, firstDayOfWeek, today as todayOf, weekOf } from "../dates.js";
|
|
3
|
+
import { useLanguage, useWords } from "../i18n/index.js";
|
|
4
4
|
import { Week } from "./calendar.js";
|
|
5
5
|
import { ProjectMark } from "./projects.js";
|
|
6
6
|
import { TaskList } from "./task-list.js";
|
|
7
7
|
import { PersonChip, Time } from "./ui.js";
|
|
8
8
|
|
|
9
9
|
// What is on a person's plate (ADR-0019), across the projects they may see: the week at the
|
|
10
|
-
// top, with the open tasks
|
|
10
|
+
// top, with the open tasks they work on on its days, and what of theirs was done or
|
|
11
11
|
// dropped on them, as a project's calendar shows it; below, the open tasks by when they are
|
|
12
12
|
// due, and the decisions that wait for a person. Takes its data as props and nothing from the
|
|
13
13
|
// network; the week shown is the address's (ADR-0010).
|
|
@@ -35,8 +35,10 @@ export interface WorkListProps {
|
|
|
35
35
|
export function WorkList(props: WorkListProps) {
|
|
36
36
|
const words = useWords();
|
|
37
37
|
const work = words.work;
|
|
38
|
+
const { tag } = useLanguage();
|
|
38
39
|
const now = props.today ?? todayOf();
|
|
39
|
-
|
|
40
|
+
// This week ends with the week's last day, as the workspace begins its weeks (ADR-0039).
|
|
41
|
+
const weekEnd = weekOf(now, firstDayOfWeek(deviceLanguage(tag), props.weekStart)).at(-1) ?? now;
|
|
40
42
|
const { tasks, decisions } = props.work;
|
|
41
43
|
const closed = props.work.closed ?? [];
|
|
42
44
|
const projectOf = new Map([...tasks, ...closed].map((item) => [item.task.id, item.project]));
|
package/src/console.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type RestDecisionInput,
|
|
23
23
|
type RestDecisionPatch,
|
|
24
24
|
type RestEvent,
|
|
25
|
+
type RestMyDecisions,
|
|
25
26
|
type RestProject,
|
|
26
27
|
type RestProjectInput,
|
|
27
28
|
type RestProjectPatch,
|
|
@@ -45,6 +46,7 @@ import { DecisionForm } from "./components/decision-form.js";
|
|
|
45
46
|
import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
|
|
46
47
|
import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
|
|
47
48
|
import { GoalForm, GoalList, type GoalListProps, GoalView, goalMonth } from "./components/goals.js";
|
|
49
|
+
import { MyDecisions } from "./components/my-decisions.js";
|
|
48
50
|
import { PackTokenWays } from "./components/pack-token.js";
|
|
49
51
|
import { PAGE_SIZE } from "./components/paged.js";
|
|
50
52
|
import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
|
|
@@ -76,6 +78,7 @@ import {
|
|
|
76
78
|
Callout,
|
|
77
79
|
CategoryBadge,
|
|
78
80
|
EmptyState,
|
|
81
|
+
LinkButton,
|
|
79
82
|
personName,
|
|
80
83
|
Spinner,
|
|
81
84
|
StateBadge,
|
|
@@ -385,19 +388,6 @@ function NotWithToken() {
|
|
|
385
388
|
return <EmptyState title={words.title} body={words.body} />;
|
|
386
389
|
}
|
|
387
390
|
|
|
388
|
-
/** A link that reads as a button: where a form is, for instance. */
|
|
389
|
-
function LinkButton(props: {
|
|
390
|
-
readonly href: string;
|
|
391
|
-
readonly variant?: "primary" | "secondary";
|
|
392
|
-
readonly children: React.ReactNode;
|
|
393
|
-
}) {
|
|
394
|
-
return (
|
|
395
|
-
<a className={`sc-button sc-button-${props.variant ?? "secondary"}`} href={props.href}>
|
|
396
|
-
{props.children}
|
|
397
|
-
</a>
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
391
|
function ProjectsPage(props: {
|
|
402
392
|
readonly data: ConsoleData;
|
|
403
393
|
readonly components: ConsoleComponents;
|
|
@@ -600,6 +590,28 @@ function TaskPage(props: {
|
|
|
600
590
|
(fetched !== undefined && (fetched.id === route.id || fetched.number === route.number)
|
|
601
591
|
? fetched
|
|
602
592
|
: undefined);
|
|
593
|
+
// The task this one is part of: among the board's, or asked for by itself when it was put away.
|
|
594
|
+
const parentId = task?.parentId ?? null;
|
|
595
|
+
const parentOnBoard =
|
|
596
|
+
parentId === null ? undefined : data.tasks.find((candidate) => candidate.id === parentId);
|
|
597
|
+
const [parentFetched, setParentFetched] = useState<RestTask | undefined>(undefined);
|
|
598
|
+
const parentWanted =
|
|
599
|
+
parentId !== null && parentOnBoard === undefined && data.projectLoaded ? parentId : undefined;
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
if (parentWanted === undefined) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const controller = new AbortController();
|
|
605
|
+
fetchTask(parentWanted, controller.signal)
|
|
606
|
+
.then((found) => {
|
|
607
|
+
if (!controller.signal.aborted) {
|
|
608
|
+
setParentFetched(found);
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
.catch(() => undefined);
|
|
612
|
+
return () => controller.abort();
|
|
613
|
+
}, [parentWanted, fetchTask]);
|
|
614
|
+
const parent = parentOnBoard ?? (parentFetched?.id === parentId ? parentFetched : undefined);
|
|
603
615
|
const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
|
|
604
616
|
const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
|
|
605
617
|
const { taskHistory, taskComments } = data.actions;
|
|
@@ -653,6 +665,9 @@ function TaskPage(props: {
|
|
|
653
665
|
task={task}
|
|
654
666
|
people={data.people.filter(inWorkspace)}
|
|
655
667
|
tasks={data.tasks}
|
|
668
|
+
parent={parent}
|
|
669
|
+
findTasks={data.actions.findTasks}
|
|
670
|
+
findGoals={data.actions.findGoals}
|
|
656
671
|
decisions={data.decisions}
|
|
657
672
|
runs={data.runs}
|
|
658
673
|
comments={comments}
|
|
@@ -805,9 +820,9 @@ function ProjectPage(props: {
|
|
|
805
820
|
<section className="sc-panel sc-panel-form" aria-label={words.task.newAria}>
|
|
806
821
|
<TaskForm
|
|
807
822
|
people={data.people.filter(inWorkspace)}
|
|
808
|
-
|
|
823
|
+
findTasks={data.actions.findTasks}
|
|
809
824
|
categories={data.categories}
|
|
810
|
-
|
|
825
|
+
findGoals={data.actions.findGoals}
|
|
811
826
|
busy={busy}
|
|
812
827
|
onSubmit={(input: RestTaskInput) =>
|
|
813
828
|
void act(async () => {
|
|
@@ -888,6 +903,19 @@ function ProjectPage(props: {
|
|
|
888
903
|
empty={<p className="sc-muted">{calendar.nothingOverdue}</p>}
|
|
889
904
|
/>
|
|
890
905
|
</section>
|
|
906
|
+
{open.length > 0 && (
|
|
907
|
+
<section className="sc-task-section" aria-label={calendar.workload.title}>
|
|
908
|
+
<h2 className="sc-section-title">{calendar.workload.title}</h2>
|
|
909
|
+
<Workload
|
|
910
|
+
tasks={shown}
|
|
911
|
+
today={now}
|
|
912
|
+
weekStart={weekStartOf(data.me?.workspace)}
|
|
913
|
+
personHref={(person) =>
|
|
914
|
+
boardHref(key, person === undefined ? {} : { person: person.id })
|
|
915
|
+
}
|
|
916
|
+
/>
|
|
917
|
+
</section>
|
|
918
|
+
)}
|
|
891
919
|
<section className="sc-task-section" aria-label={calendar.unscheduled}>
|
|
892
920
|
<h2 className="sc-section-title">
|
|
893
921
|
{calendar.unscheduled} <span className="sc-column-count">{unscheduled.length}</span>
|
|
@@ -901,18 +929,6 @@ function ProjectPage(props: {
|
|
|
901
929
|
/>
|
|
902
930
|
</section>
|
|
903
931
|
</div>
|
|
904
|
-
{open.length > 0 && (
|
|
905
|
-
<section className="sc-task-section" aria-label={calendar.workload.title}>
|
|
906
|
-
<h2 className="sc-section-title">{calendar.workload.title}</h2>
|
|
907
|
-
<Workload
|
|
908
|
-
tasks={shown}
|
|
909
|
-
today={now}
|
|
910
|
-
personHref={(person) =>
|
|
911
|
-
boardHref(key, person === undefined ? {} : { person: person.id })
|
|
912
|
-
}
|
|
913
|
-
/>
|
|
914
|
-
</section>
|
|
915
|
-
)}
|
|
916
932
|
</>
|
|
917
933
|
);
|
|
918
934
|
}
|
|
@@ -1143,7 +1159,9 @@ function ProjectPage(props: {
|
|
|
1143
1159
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1144
1160
|
<section className="sc-panel sc-panel-form" aria-label={words.decisions.raiseAria}>
|
|
1145
1161
|
<DecisionForm
|
|
1146
|
-
|
|
1162
|
+
findTasks={data.actions.findTasks}
|
|
1163
|
+
deciders={data.members.map((member) => member.person)}
|
|
1164
|
+
me={data.me?.person ?? undefined}
|
|
1147
1165
|
busy={busy}
|
|
1148
1166
|
onSubmit={(input: RestDecisionInput) =>
|
|
1149
1167
|
void act(async () => {
|
|
@@ -1796,7 +1814,7 @@ function WorkspaceSettingsPage(props: {
|
|
|
1796
1814
|
}
|
|
1797
1815
|
|
|
1798
1816
|
/** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
|
|
1799
|
-
/** Whether a person is in: who a picker of
|
|
1817
|
+
/** Whether a person is in: who a picker of workers and of members offers (ADR-0022). */
|
|
1800
1818
|
const inWorkspace = (person: { readonly status: "active" | "deactivated" }): boolean =>
|
|
1801
1819
|
person.status === "active";
|
|
1802
1820
|
|
|
@@ -1986,6 +2004,108 @@ function PlatePage(props: {
|
|
|
1986
2004
|
}
|
|
1987
2005
|
|
|
1988
2006
|
/** What is on the person's own plate, across their projects. */
|
|
2007
|
+
/** How many of a person's decisions are read at a time. */
|
|
2008
|
+
const MY_DECISIONS_PAGE = 50;
|
|
2009
|
+
|
|
2010
|
+
/** One kind of a person's decisions, read a page at a time, as the page shows it. */
|
|
2011
|
+
interface DecisionsRead {
|
|
2012
|
+
readonly items: RestMyDecisions["items"] | undefined;
|
|
2013
|
+
readonly more: boolean;
|
|
2014
|
+
readonly busy: boolean;
|
|
2015
|
+
readonly problem: string | undefined;
|
|
2016
|
+
readonly readMore: () => void;
|
|
2017
|
+
/** Reads the first page again, after an answer changed what waits. */
|
|
2018
|
+
readonly reload: () => void;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
/** The decisions addressed to the person, or, with `raised`, those they raised for someone else (ADR-0042). */
|
|
2022
|
+
function useMyDecisions(
|
|
2023
|
+
myDecisions: ConsoleData["actions"]["myDecisions"],
|
|
2024
|
+
raised: boolean,
|
|
2025
|
+
couldNotLoad: string,
|
|
2026
|
+
): DecisionsRead {
|
|
2027
|
+
const [items, setItems] = useState<RestMyDecisions["items"] | undefined>(undefined);
|
|
2028
|
+
const [more, setMore] = useState(false);
|
|
2029
|
+
const [busy, setBusy] = useState(false);
|
|
2030
|
+
const [problem, setProblem] = useState<string | undefined>(undefined);
|
|
2031
|
+
const read = useCallback(
|
|
2032
|
+
async (offset: number, signal?: AbortSignal) => {
|
|
2033
|
+
setBusy(true);
|
|
2034
|
+
try {
|
|
2035
|
+
const page = await myDecisions({ raised, limit: MY_DECISIONS_PAGE, offset }, signal);
|
|
2036
|
+
if (signal?.aborted === true) {
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
setItems((before) => (offset === 0 ? page.items : [...(before ?? []), ...page.items]));
|
|
2040
|
+
setMore(page.more);
|
|
2041
|
+
} catch (failure: unknown) {
|
|
2042
|
+
if (signal?.aborted !== true) {
|
|
2043
|
+
setProblem(errorWords(failure, couldNotLoad));
|
|
2044
|
+
}
|
|
2045
|
+
} finally {
|
|
2046
|
+
if (signal?.aborted !== true) {
|
|
2047
|
+
setBusy(false);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
[myDecisions, raised, couldNotLoad],
|
|
2052
|
+
);
|
|
2053
|
+
useEffect(() => {
|
|
2054
|
+
const controller = new AbortController();
|
|
2055
|
+
setItems(undefined);
|
|
2056
|
+
setProblem(undefined);
|
|
2057
|
+
void read(0, controller.signal);
|
|
2058
|
+
return () => controller.abort();
|
|
2059
|
+
}, [read]);
|
|
2060
|
+
const readMore = useCallback(() => void read(items?.length ?? 0), [read, items]);
|
|
2061
|
+
const reload = useCallback(() => void read(0), [read]);
|
|
2062
|
+
return { items, more, busy, problem, readMore, reload };
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* A person's decisions (ADR-0042), laid out as a project's decisions page is: those waiting for
|
|
2067
|
+
* their answer, answered here, those they asked of others, and the record of both.
|
|
2068
|
+
*/
|
|
2069
|
+
function MyDecisionsPage(props: { readonly data: ConsoleData }) {
|
|
2070
|
+
const words = useWords();
|
|
2071
|
+
const { myDecisions, answerDecisionIn } = props.data.actions;
|
|
2072
|
+
const addressed = useMyDecisions(myDecisions, false, words.myDecisions.couldNotLoad);
|
|
2073
|
+
const raised = useMyDecisions(myDecisions, true, words.myDecisions.couldNotLoad);
|
|
2074
|
+
const { busy, error, act } = useAction();
|
|
2075
|
+
const problem = addressed.problem ?? raised.problem ?? error;
|
|
2076
|
+
const onAnswer = (projectKey: string, decision: RestDecision, input: RestAnswerInput) =>
|
|
2077
|
+
void act(async () => {
|
|
2078
|
+
await answerDecisionIn(projectKey, decision.id, input);
|
|
2079
|
+
addressed.reload();
|
|
2080
|
+
raised.reload();
|
|
2081
|
+
});
|
|
2082
|
+
return (
|
|
2083
|
+
<>
|
|
2084
|
+
<div className="sc-page-head">
|
|
2085
|
+
<h1 className="sc-page-title">{words.myDecisions.title}</h1>
|
|
2086
|
+
</div>
|
|
2087
|
+
{problem !== undefined && <Callout tone="danger">{problem}</Callout>}
|
|
2088
|
+
{addressed.items === undefined || raised.items === undefined ? (
|
|
2089
|
+
problem === undefined && (
|
|
2090
|
+
<div className="sc-loading">
|
|
2091
|
+
<Spinner label={words.myDecisions.loading} />
|
|
2092
|
+
</div>
|
|
2093
|
+
)
|
|
2094
|
+
) : addressed.items.length === 0 && raised.items.length === 0 ? (
|
|
2095
|
+
<EmptyState title={words.myDecisions.empty.title} body={words.myDecisions.empty.body} />
|
|
2096
|
+
) : (
|
|
2097
|
+
<MyDecisions
|
|
2098
|
+
addressed={{ items: addressed.items, more: addressed.more, onMore: addressed.readMore }}
|
|
2099
|
+
raised={{ items: raised.items, more: raised.more, onMore: raised.readMore }}
|
|
2100
|
+
decisionHref={decisionHref}
|
|
2101
|
+
onAnswer={onAnswer}
|
|
2102
|
+
busy={busy || addressed.busy || raised.busy}
|
|
2103
|
+
/>
|
|
2104
|
+
)}
|
|
2105
|
+
</>
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
1989
2109
|
function WorkPage(props: {
|
|
1990
2110
|
readonly data: ConsoleData;
|
|
1991
2111
|
readonly components: ConsoleComponents;
|
|
@@ -2052,6 +2172,9 @@ function WorkspacePage(props: {
|
|
|
2052
2172
|
if (route.name === "work") {
|
|
2053
2173
|
return <WorkPage data={data} components={components} week={route.week} />;
|
|
2054
2174
|
}
|
|
2175
|
+
if (route.name === "my-decisions") {
|
|
2176
|
+
return <MyDecisionsPage data={data} />;
|
|
2177
|
+
}
|
|
2055
2178
|
if (route.name === "person-work") {
|
|
2056
2179
|
return <PersonWorkPage id={route.id} data={data} components={components} week={route.week} />;
|
|
2057
2180
|
}
|
|
@@ -2295,6 +2418,11 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
2295
2418
|
current: route.name === "projects" || route.name === "new-project",
|
|
2296
2419
|
},
|
|
2297
2420
|
{ href: PATHS.work, label: words.nav.work, current: route.name === "work" },
|
|
2421
|
+
{
|
|
2422
|
+
href: PATHS.decisions,
|
|
2423
|
+
label: words.nav.myDecisions,
|
|
2424
|
+
current: route.name === "my-decisions",
|
|
2425
|
+
},
|
|
2298
2426
|
{
|
|
2299
2427
|
href: PATHS.people,
|
|
2300
2428
|
label: words.nav.people,
|
package/src/data.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type RestGoal,
|
|
25
25
|
type RestGoalInput,
|
|
26
26
|
type RestGoalPatch,
|
|
27
|
+
type RestGoals,
|
|
27
28
|
type RestListing,
|
|
28
29
|
type RestListingInput,
|
|
29
30
|
type RestMe,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
type RestMemberInput,
|
|
32
33
|
type RestMemberPatch,
|
|
33
34
|
type RestMePatch,
|
|
35
|
+
type RestMyDecisions,
|
|
34
36
|
type RestOrganization,
|
|
35
37
|
type RestPackConnect,
|
|
36
38
|
type RestPackConnectStarted,
|
|
@@ -107,8 +109,22 @@ export interface ConsoleActions {
|
|
|
107
109
|
page: { readonly limit: number; readonly offset: number },
|
|
108
110
|
signal?: AbortSignal,
|
|
109
111
|
): Promise<RestTasks>;
|
|
112
|
+
/** What words find among the current tasks of the project, a page at a time, for a picker (ADR-0037); the first page of them for no words. */
|
|
113
|
+
findTasks(
|
|
114
|
+
query: string,
|
|
115
|
+
page: { readonly limit: number; readonly offset: number },
|
|
116
|
+
signal?: AbortSignal,
|
|
117
|
+
): Promise<RestTasks>;
|
|
118
|
+
/** What words find among the open goals of the project, a page at a time, for a picker. */
|
|
119
|
+
findGoals(
|
|
120
|
+
query: string,
|
|
121
|
+
page: { readonly limit: number; readonly offset: number },
|
|
122
|
+
signal?: AbortSignal,
|
|
123
|
+
): Promise<RestGoals>;
|
|
110
124
|
raiseDecision(input: RestDecisionInput): Promise<RestDecision>;
|
|
111
125
|
answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
|
|
126
|
+
/** Answers a decision of any project the person may see, by its project's key: for the person's own page of decisions (ADR-0042). */
|
|
127
|
+
answerDecisionIn(projectKey: string, id: string, input: RestAnswerInput): Promise<RestDecision>;
|
|
112
128
|
/** Grows a decision's record: its context, or what followed. */
|
|
113
129
|
updateDecision(id: string, patch: RestDecisionPatch): Promise<RestDecision>;
|
|
114
130
|
/** Sets a goal on the roadmap of the project the page is on; for an owner. */
|
|
@@ -116,6 +132,17 @@ export interface ConsoleActions {
|
|
|
116
132
|
updateGoal(id: string, patch: RestGoalPatch): Promise<RestGoal>;
|
|
117
133
|
/** What is on the person's plate across their projects, read when the page asks. */
|
|
118
134
|
work(signal?: AbortSignal): Promise<RestWork>;
|
|
135
|
+
/** The decisions addressed to the person (ADR-0042) across their projects, a page at a time, read when the page asks. */
|
|
136
|
+
myDecisions(
|
|
137
|
+
filter: {
|
|
138
|
+
readonly open?: boolean;
|
|
139
|
+
/** Those the person raised for someone else, in place of those addressed to them. */
|
|
140
|
+
readonly raised?: boolean;
|
|
141
|
+
readonly limit: number;
|
|
142
|
+
readonly offset: number;
|
|
143
|
+
},
|
|
144
|
+
signal?: AbortSignal,
|
|
145
|
+
): Promise<RestMyDecisions>;
|
|
119
146
|
/** The categories tasks are sorted into: kept by an administrator. */
|
|
120
147
|
createCategory(input: RestCategoryInput): Promise<RestCategory>;
|
|
121
148
|
updateCategory(key: string, patch: RestCategoryPatch): Promise<RestCategory>;
|
|
@@ -265,6 +292,32 @@ async function readTasks(
|
|
|
265
292
|
return { items };
|
|
266
293
|
}
|
|
267
294
|
|
|
295
|
+
/** How many pages of the roadmap are read before it is shown as it stands. */
|
|
296
|
+
const GOALS_MAX_PAGES = 20;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The goals of a project, whole: page after page while the server says there is more, since
|
|
300
|
+
* the roadmap, the calendar and the filters draw them all.
|
|
301
|
+
*/
|
|
302
|
+
async function readGoals(
|
|
303
|
+
scope: ProjectClient,
|
|
304
|
+
signal: AbortSignal | undefined,
|
|
305
|
+
): Promise<{ readonly items: RestGoal[] }> {
|
|
306
|
+
const items: RestGoal[] = [];
|
|
307
|
+
for (let page = 0; page < GOALS_MAX_PAGES; page += 1) {
|
|
308
|
+
const result = await scope.goals({ limit: LIST_LIMIT, offset: page * LIST_LIMIT }, signal);
|
|
309
|
+
items.push(...result.items);
|
|
310
|
+
if (!result.more) {
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { items };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The words of a search as the API takes them: none for a blank. */
|
|
318
|
+
const searchWordsOf = (query: string): { readonly q?: string } =>
|
|
319
|
+
query.trim() === "" ? {} : { q: query.trim() };
|
|
320
|
+
|
|
268
321
|
/** How many pages of the decisions are read before the record is shown as it stands. */
|
|
269
322
|
const DECISIONS_MAX_PAGES = 20;
|
|
270
323
|
|
|
@@ -353,7 +406,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
353
406
|
scope.get(signal),
|
|
354
407
|
readTasks(scope, signal),
|
|
355
408
|
readDecisions(scope, signal),
|
|
356
|
-
scope
|
|
409
|
+
readGoals(scope, signal),
|
|
357
410
|
scope.runs(undefined, signal),
|
|
358
411
|
scope.members(signal),
|
|
359
412
|
]);
|
|
@@ -608,11 +661,17 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
608
661
|
return archived;
|
|
609
662
|
},
|
|
610
663
|
archivedTasks: (page, signal) => scope().tasks({ archived: true, ...page }, signal),
|
|
664
|
+
findTasks: (query, page, signal) =>
|
|
665
|
+
scope().tasks({ archived: false, ...searchWordsOf(query), ...page }, signal),
|
|
666
|
+
findGoals: (query, page, signal) =>
|
|
667
|
+
scope().goals({ open: true, ...searchWordsOf(query), ...page }, signal),
|
|
611
668
|
async raiseDecision(input) {
|
|
612
669
|
const decision = await scope().raiseDecision(input);
|
|
613
670
|
await refreshProject();
|
|
614
671
|
return decision;
|
|
615
672
|
},
|
|
673
|
+
answerDecisionIn: (projectKey, id, input) =>
|
|
674
|
+
client.project(projectKey).answerDecision(id, input),
|
|
616
675
|
async answerDecision(id, input) {
|
|
617
676
|
const decision = await scope().answerDecision(id, input);
|
|
618
677
|
await refreshProject();
|
|
@@ -636,6 +695,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
636
695
|
return goal;
|
|
637
696
|
},
|
|
638
697
|
work: (signal) => client.work(signal),
|
|
698
|
+
myDecisions: (filter, signal) => client.myDecisions(filter, signal),
|
|
639
699
|
async createCategory(input) {
|
|
640
700
|
const category = await client.createCategory(input);
|
|
641
701
|
setCategories((await client.categories()).items);
|