@skillcdn/console 0.1.12 → 0.1.14
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 -4
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +260 -6
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +7 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +22 -0
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +69 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/components/calendar.d.ts +15 -0
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +57 -30
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +1 -1
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/skills.d.ts.map +1 -1
- package/dist/components/skills.js +1 -1
- package/dist/components/skills.js.map +1 -1
- package/dist/components/work.d.ts +4 -0
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +8 -3
- package/dist/components/work.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +1 -1
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +35 -1
- package/dist/console.d.ts +3 -2
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +24 -58
- package/dist/console.js.map +1 -1
- package/dist/dates.d.ts +4 -0
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +26 -0
- package/dist/dates.js.map +1 -1
- package/dist/i18n/en.d.ts +7 -19
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +10 -22
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +10 -22
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +4 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +10 -2
- package/dist/router.js.map +1 -1
- package/docs/specs/cli.md +15 -2
- package/docs/specs/rest.md +13 -13
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +2 -2
- package/skills/working-the-board/SKILL.md +5 -1
- package/src/cli/cli.ts +333 -5
- package/src/cli/format.ts +27 -0
- package/src/cli/help.ts +86 -1
- package/src/components/calendar.tsx +219 -91
- package/src/components/decision-list.tsx +0 -1
- package/src/components/skills.tsx +0 -1
- package/src/components/work.tsx +66 -46
- package/src/connect-page.tsx +0 -1
- package/src/console.tsx +22 -74
- package/src/dates.ts +28 -0
- package/src/i18n/en.ts +10 -28
- package/src/i18n/ko.ts +10 -28
- package/src/index.ts +5 -0
- package/src/router.ts +15 -5
- package/src/styles/console.css +35 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
1
2
|
import { OPEN_TASK_STATES, type RestCategory, type RestPerson, type RestTask } from "../api.js";
|
|
2
3
|
import {
|
|
4
|
+
addDays,
|
|
3
5
|
addMonths,
|
|
4
6
|
daysBetween,
|
|
5
7
|
firstDayOfWeek,
|
|
8
|
+
formatDays,
|
|
6
9
|
formatMonth,
|
|
7
10
|
formatWeekday,
|
|
8
11
|
type GridDay,
|
|
@@ -10,15 +13,17 @@ import {
|
|
|
10
13
|
monthOf,
|
|
11
14
|
today as todayOf,
|
|
12
15
|
weekdays,
|
|
16
|
+
weekOf,
|
|
13
17
|
} from "../dates.js";
|
|
14
18
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
15
19
|
import { cx, PersonChip, personName } from "./ui.js";
|
|
16
20
|
|
|
17
|
-
// The calendar of a project (ADR-0019): the month as a grid
|
|
18
|
-
// due day lies across its days as a bar, in a lane of its own within
|
|
19
|
-
// of one week never cross; a task with a due day alone sits on it as
|
|
20
|
-
// start alone is marked on that day. Who has what says whether the work
|
|
21
|
-
// its data as props and nothing from the network; the month
|
|
21
|
+
// The calendar of a project (ADR-0019): the month as a grid, or one week on its own. A task
|
|
22
|
+
// planned from a start to a due day lies across its days as a bar, in a lane of its own within
|
|
23
|
+
// each week so that the bars of one week never cross; a task with a due day alone sits on it as
|
|
24
|
+
// a dot, and one with a start alone is marked on that day. Who has what says whether the work
|
|
25
|
+
// is on its days. Takes its data as props and nothing from the network; the month or the week
|
|
26
|
+
// shown is the address's (ADR-0010).
|
|
22
27
|
|
|
23
28
|
const isOpen = (task: RestTask): boolean => OPEN_TASK_STATES.includes(task.state);
|
|
24
29
|
|
|
@@ -99,15 +104,14 @@ function push(map: Map<string, RestTask[]>, day: string, task: RestTask): void {
|
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const weeks = monthGrid(props.month, firstDay);
|
|
107
|
+
/** The tasks that sit on one day each: due on it, or beginning on it with no due day yet. */
|
|
108
|
+
function onDays(tasks: readonly RestTask[]): {
|
|
109
|
+
readonly due: ReadonlyMap<string, readonly RestTask[]>;
|
|
110
|
+
readonly starting: ReadonlyMap<string, readonly RestTask[]>;
|
|
111
|
+
} {
|
|
108
112
|
const due = new Map<string, RestTask[]>();
|
|
109
113
|
const starting = new Map<string, RestTask[]>();
|
|
110
|
-
for (const task of
|
|
114
|
+
for (const task of tasks) {
|
|
111
115
|
if (spans(task)) {
|
|
112
116
|
continue;
|
|
113
117
|
}
|
|
@@ -121,6 +125,121 @@ export function Calendar(props: CalendarProps) {
|
|
|
121
125
|
for (const list of due.values()) {
|
|
122
126
|
list.sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || a.number - b.number);
|
|
123
127
|
}
|
|
128
|
+
return { due, starting };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** How a task reads on a day: struck through once it is over, and marked once it is late. */
|
|
132
|
+
const tone = (task: RestTask, day: string, now: string): string =>
|
|
133
|
+
cx(!isOpen(task) && "sc-calendar-over", isOpen(task) && day < now && "sc-calendar-late");
|
|
134
|
+
|
|
135
|
+
interface WeekRowProps {
|
|
136
|
+
readonly week: readonly GridDay[];
|
|
137
|
+
readonly segments: readonly WeekSegment[];
|
|
138
|
+
readonly due: ReadonlyMap<string, readonly RestTask[]>;
|
|
139
|
+
readonly starting: ReadonlyMap<string, readonly RestTask[]>;
|
|
140
|
+
readonly now: string;
|
|
141
|
+
readonly taskHref: (task: RestTask) => string;
|
|
142
|
+
/** What stands before a task's title where the tasks are of several projects: the project's mark. */
|
|
143
|
+
readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** One week as a row of cells: the bars across the days in their lanes, then the tasks on each day. */
|
|
147
|
+
function WeekRow(props: WeekRowProps) {
|
|
148
|
+
const words = useWords().calendar;
|
|
149
|
+
const { week, segments, now } = props;
|
|
150
|
+
const lanes = segments.reduce((most, segment) => Math.max(most, segment.lane + 1), 0);
|
|
151
|
+
return (
|
|
152
|
+
<tr>
|
|
153
|
+
{week.map((cell, column) => (
|
|
154
|
+
<td
|
|
155
|
+
key={cell.day}
|
|
156
|
+
className={cx(
|
|
157
|
+
"sc-calendar-cell",
|
|
158
|
+
!cell.inMonth && "sc-calendar-cell-outside",
|
|
159
|
+
cell.day === now && "sc-calendar-cell-today",
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
<span className="sc-calendar-day">{Number(cell.day.slice(8))}</span>
|
|
163
|
+
{Array.from({ length: lanes }, (_, lane) => {
|
|
164
|
+
const segment = segments.find(
|
|
165
|
+
(candidate) =>
|
|
166
|
+
candidate.lane === lane && candidate.from <= column && column <= candidate.to,
|
|
167
|
+
);
|
|
168
|
+
if (segment === undefined) {
|
|
169
|
+
// The lane is someone else's on this day: the bars below keep their height.
|
|
170
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: an empty lane is nothing but its position, and the lanes of a week never reorder
|
|
171
|
+
return <span key={lane} className="sc-calendar-lane" aria-hidden="true" />;
|
|
172
|
+
}
|
|
173
|
+
const { task } = segment;
|
|
174
|
+
return (
|
|
175
|
+
<a
|
|
176
|
+
key={task.id}
|
|
177
|
+
className={cx(
|
|
178
|
+
"sc-calendar-bar",
|
|
179
|
+
`sc-category-${task.category?.color ?? "gray"}`,
|
|
180
|
+
segment.from === column && segment.begins && "sc-calendar-bar-begins",
|
|
181
|
+
segment.to === column && segment.ends && "sc-calendar-bar-ends",
|
|
182
|
+
tone(task, task.dueOn ?? cell.day, now),
|
|
183
|
+
)}
|
|
184
|
+
href={props.taskHref(task)}
|
|
185
|
+
title={`#${task.number} ${task.title}`}
|
|
186
|
+
>
|
|
187
|
+
{segment.from === column && (
|
|
188
|
+
<>
|
|
189
|
+
{props.markOf?.(task)}
|
|
190
|
+
<span className="sc-calendar-task-title">{task.title}</span>
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
193
|
+
</a>
|
|
194
|
+
);
|
|
195
|
+
})}
|
|
196
|
+
{(props.due.get(cell.day) ?? []).map((task) => {
|
|
197
|
+
const mark = props.markOf?.(task);
|
|
198
|
+
return (
|
|
199
|
+
<a
|
|
200
|
+
key={task.id}
|
|
201
|
+
className={cx("sc-calendar-task", tone(task, cell.day, now))}
|
|
202
|
+
href={props.taskHref(task)}
|
|
203
|
+
title={`#${task.number} ${task.title}`}
|
|
204
|
+
>
|
|
205
|
+
{mark === undefined || mark === null ? (
|
|
206
|
+
<span
|
|
207
|
+
className={cx(
|
|
208
|
+
"sc-calendar-dot",
|
|
209
|
+
`sc-category-${task.category?.color ?? "gray"}`,
|
|
210
|
+
)}
|
|
211
|
+
/>
|
|
212
|
+
) : (
|
|
213
|
+
mark
|
|
214
|
+
)}
|
|
215
|
+
<span className="sc-calendar-task-title">{task.title}</span>
|
|
216
|
+
</a>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
219
|
+
{(props.starting.get(cell.day) ?? []).map((task) => (
|
|
220
|
+
<a
|
|
221
|
+
key={task.id}
|
|
222
|
+
className="sc-calendar-task sc-calendar-task-start"
|
|
223
|
+
href={props.taskHref(task)}
|
|
224
|
+
title={`#${task.number} ${task.title}: ${words.startsOnly}`}
|
|
225
|
+
>
|
|
226
|
+
{props.markOf?.(task)}
|
|
227
|
+
<span className="sc-calendar-task-title">{task.title}</span>
|
|
228
|
+
</a>
|
|
229
|
+
))}
|
|
230
|
+
</td>
|
|
231
|
+
))}
|
|
232
|
+
</tr>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function Calendar(props: CalendarProps) {
|
|
237
|
+
const words = useWords().calendar;
|
|
238
|
+
const { tag } = useLanguage();
|
|
239
|
+
const now = props.today ?? todayOf();
|
|
240
|
+
const firstDay = firstDayOfWeek(tag);
|
|
241
|
+
const weeks = monthGrid(props.month, firstDay);
|
|
242
|
+
const { due, starting } = onDays(props.tasks);
|
|
124
243
|
const inMonth = (day: string) => monthOf(day) === props.month;
|
|
125
244
|
const monthWeeks = weeks.map((week) => ({ week, segments: weekSegments(week, props.tasks) }));
|
|
126
245
|
const nothing =
|
|
@@ -131,8 +250,6 @@ export function Calendar(props: CalendarProps) {
|
|
|
131
250
|
(segment) => !week.slice(segment.from, segment.to + 1).some((cell) => cell.inMonth),
|
|
132
251
|
),
|
|
133
252
|
);
|
|
134
|
-
const tone = (task: RestTask, day: string) =>
|
|
135
|
-
cx(!isOpen(task) && "sc-calendar-over", isOpen(task) && day < now && "sc-calendar-late");
|
|
136
253
|
return (
|
|
137
254
|
<div className="sc-calendar">
|
|
138
255
|
<div className="sc-calendar-head">
|
|
@@ -171,83 +288,17 @@ export function Calendar(props: CalendarProps) {
|
|
|
171
288
|
</tr>
|
|
172
289
|
</thead>
|
|
173
290
|
<tbody>
|
|
174
|
-
{monthWeeks.map(({ week, segments }) =>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)}
|
|
186
|
-
>
|
|
187
|
-
<span className="sc-calendar-day">{Number(cell.day.slice(8))}</span>
|
|
188
|
-
{Array.from({ length: lanes }, (_, lane) => {
|
|
189
|
-
const segment = segments.find(
|
|
190
|
-
(candidate) =>
|
|
191
|
-
candidate.lane === lane &&
|
|
192
|
-
candidate.from <= column &&
|
|
193
|
-
column <= candidate.to,
|
|
194
|
-
);
|
|
195
|
-
if (segment === undefined) {
|
|
196
|
-
// The lane is someone else's on this day: the bars below keep their height.
|
|
197
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: an empty lane is nothing but its position, and the lanes of a week never reorder
|
|
198
|
-
return <span key={lane} className="sc-calendar-lane" aria-hidden="true" />;
|
|
199
|
-
}
|
|
200
|
-
const { task } = segment;
|
|
201
|
-
return (
|
|
202
|
-
<a
|
|
203
|
-
key={task.id}
|
|
204
|
-
className={cx(
|
|
205
|
-
"sc-calendar-bar",
|
|
206
|
-
`sc-category-${task.category?.color ?? "gray"}`,
|
|
207
|
-
segment.from === column && segment.begins && "sc-calendar-bar-begins",
|
|
208
|
-
segment.to === column && segment.ends && "sc-calendar-bar-ends",
|
|
209
|
-
tone(task, task.dueOn ?? cell.day),
|
|
210
|
-
)}
|
|
211
|
-
href={props.taskHref(task)}
|
|
212
|
-
title={`#${task.number} ${task.title}`}
|
|
213
|
-
>
|
|
214
|
-
{segment.from === column && (
|
|
215
|
-
<span className="sc-calendar-task-title">{task.title}</span>
|
|
216
|
-
)}
|
|
217
|
-
</a>
|
|
218
|
-
);
|
|
219
|
-
})}
|
|
220
|
-
{(due.get(cell.day) ?? []).map((task) => (
|
|
221
|
-
<a
|
|
222
|
-
key={task.id}
|
|
223
|
-
className={cx("sc-calendar-task", tone(task, cell.day))}
|
|
224
|
-
href={props.taskHref(task)}
|
|
225
|
-
title={`#${task.number} ${task.title}`}
|
|
226
|
-
>
|
|
227
|
-
<span
|
|
228
|
-
className={cx(
|
|
229
|
-
"sc-calendar-dot",
|
|
230
|
-
`sc-category-${task.category?.color ?? "gray"}`,
|
|
231
|
-
)}
|
|
232
|
-
/>
|
|
233
|
-
<span className="sc-calendar-task-title">{task.title}</span>
|
|
234
|
-
</a>
|
|
235
|
-
))}
|
|
236
|
-
{(starting.get(cell.day) ?? []).map((task) => (
|
|
237
|
-
<a
|
|
238
|
-
key={task.id}
|
|
239
|
-
className="sc-calendar-task sc-calendar-task-start"
|
|
240
|
-
href={props.taskHref(task)}
|
|
241
|
-
title={`#${task.number} ${task.title}: ${words.startsOnly}`}
|
|
242
|
-
>
|
|
243
|
-
<span className="sc-calendar-task-title">{task.title}</span>
|
|
244
|
-
</a>
|
|
245
|
-
))}
|
|
246
|
-
</td>
|
|
247
|
-
))}
|
|
248
|
-
</tr>
|
|
249
|
-
);
|
|
250
|
-
})}
|
|
291
|
+
{monthWeeks.map(({ week, segments }) => (
|
|
292
|
+
<WeekRow
|
|
293
|
+
key={week[0]?.day}
|
|
294
|
+
week={week}
|
|
295
|
+
segments={segments}
|
|
296
|
+
due={due}
|
|
297
|
+
starting={starting}
|
|
298
|
+
now={now}
|
|
299
|
+
taskHref={props.taskHref}
|
|
300
|
+
/>
|
|
301
|
+
))}
|
|
251
302
|
</tbody>
|
|
252
303
|
</table>
|
|
253
304
|
{(props.categories ?? []).length > 0 && (
|
|
@@ -267,6 +318,83 @@ export function Calendar(props: CalendarProps) {
|
|
|
267
318
|
);
|
|
268
319
|
}
|
|
269
320
|
|
|
321
|
+
export interface WeekProps {
|
|
322
|
+
readonly tasks: readonly RestTask[];
|
|
323
|
+
/** A day of the week shown; today's week when left out. */
|
|
324
|
+
readonly day?: string | undefined;
|
|
325
|
+
/** The day to count from; the browser's today when left out. */
|
|
326
|
+
readonly today?: string | undefined;
|
|
327
|
+
readonly taskHref: (task: RestTask) => string;
|
|
328
|
+
/** Where the week of a day is shown. */
|
|
329
|
+
readonly weekHref: (day: string) => string;
|
|
330
|
+
/** What stands before a task's title where the tasks are of several projects: the project's mark. */
|
|
331
|
+
readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** One week on its own: the days as columns, the tasks on them, and the weeks around a step away. */
|
|
335
|
+
export function Week(props: WeekProps) {
|
|
336
|
+
const words = useWords().calendar.week;
|
|
337
|
+
const { tag } = useLanguage();
|
|
338
|
+
const now = props.today ?? todayOf();
|
|
339
|
+
const firstDay = firstDayOfWeek(tag);
|
|
340
|
+
const days = weekOf(props.day ?? now, firstDay);
|
|
341
|
+
const first = days[0] ?? now;
|
|
342
|
+
const last = days[days.length - 1] ?? now;
|
|
343
|
+
const week: GridDay[] = days.map((day) => ({ day, inMonth: true }));
|
|
344
|
+
const { due, starting } = onDays(props.tasks);
|
|
345
|
+
const segments = weekSegments(week, props.tasks);
|
|
346
|
+
const nothing = segments.length === 0 && days.every((day) => !due.has(day) && !starting.has(day));
|
|
347
|
+
return (
|
|
348
|
+
<div className="sc-calendar sc-week">
|
|
349
|
+
<div className="sc-calendar-head">
|
|
350
|
+
<h2 className="sc-calendar-month">{formatDays(first, last, tag)}</h2>
|
|
351
|
+
<nav className="sc-calendar-nav" aria-label={words.title}>
|
|
352
|
+
<a
|
|
353
|
+
className="sc-button sc-button-ghost sc-button-sm"
|
|
354
|
+
href={props.weekHref(addDays(first, -7))}
|
|
355
|
+
aria-label={words.previous}
|
|
356
|
+
>
|
|
357
|
+
{"<"}
|
|
358
|
+
</a>
|
|
359
|
+
<a className="sc-button sc-button-ghost sc-button-sm" href={props.weekHref(now)}>
|
|
360
|
+
{words.thisWeek}
|
|
361
|
+
</a>
|
|
362
|
+
<a
|
|
363
|
+
className="sc-button sc-button-ghost sc-button-sm"
|
|
364
|
+
href={props.weekHref(addDays(first, 7))}
|
|
365
|
+
aria-label={words.next}
|
|
366
|
+
>
|
|
367
|
+
{">"}
|
|
368
|
+
</a>
|
|
369
|
+
</nav>
|
|
370
|
+
</div>
|
|
371
|
+
<table className="sc-calendar-grid" aria-label={words.title}>
|
|
372
|
+
<thead>
|
|
373
|
+
<tr>
|
|
374
|
+
{weekdays(firstDay).map((weekday) => (
|
|
375
|
+
<th key={weekday} scope="col">
|
|
376
|
+
{formatWeekday(weekday, tag)}
|
|
377
|
+
</th>
|
|
378
|
+
))}
|
|
379
|
+
</tr>
|
|
380
|
+
</thead>
|
|
381
|
+
<tbody>
|
|
382
|
+
<WeekRow
|
|
383
|
+
week={week}
|
|
384
|
+
segments={segments}
|
|
385
|
+
due={due}
|
|
386
|
+
starting={starting}
|
|
387
|
+
now={now}
|
|
388
|
+
taskHref={props.taskHref}
|
|
389
|
+
markOf={props.markOf}
|
|
390
|
+
/>
|
|
391
|
+
</tbody>
|
|
392
|
+
</table>
|
|
393
|
+
{nothing && <p className="sc-muted">{words.nothing}</p>}
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
270
398
|
/** One person's share of the open work, as the table of who has what says it. */
|
|
271
399
|
export interface WorkloadRow {
|
|
272
400
|
/** The person, or nobody for the tasks assigned to no one. */
|
|
@@ -326,7 +326,6 @@ export function DecisionList(props: DecisionListProps) {
|
|
|
326
326
|
<h2 className="sc-section-title">
|
|
327
327
|
{words.decisions.record} <span className="sc-column-count">{answered.length}</span>
|
|
328
328
|
</h2>
|
|
329
|
-
<p className="sc-lead">{words.decisions.recordLead}</p>
|
|
330
329
|
<DecisionLog
|
|
331
330
|
decisions={answered}
|
|
332
331
|
tasks={props.tasks}
|
package/src/components/work.tsx
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import type { RestTask, RestWork } from "../api.js";
|
|
2
2
|
import { addDays, today as todayOf } from "../dates.js";
|
|
3
3
|
import { useWords } from "../i18n/index.js";
|
|
4
|
+
import { Week } from "./calendar.js";
|
|
4
5
|
import { ProjectMark } from "./projects.js";
|
|
5
6
|
import { TaskList } from "./task-list.js";
|
|
6
7
|
import { PersonChip, Time } from "./ui.js";
|
|
7
8
|
|
|
8
|
-
// What is on a person's plate (ADR-0019), across the projects they may see: the
|
|
9
|
-
// assigned to them
|
|
10
|
-
// data as props and nothing from
|
|
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 assigned to them on its days; below, the same tasks by when they
|
|
11
|
+
// are due, and the decisions that wait for a person. Takes its data as props and nothing from
|
|
12
|
+
// the network; the week shown is the address's (ADR-0010).
|
|
11
13
|
|
|
12
14
|
export interface WorkListProps {
|
|
13
15
|
readonly work: RestWork;
|
|
14
16
|
/** The day to count from; the browser's today when left out. */
|
|
15
17
|
readonly today?: string | undefined;
|
|
18
|
+
/** A day of the week shown at the top; today's week when left out. */
|
|
19
|
+
readonly week?: string | undefined;
|
|
16
20
|
readonly taskHref: (projectKey: string, task: RestTask) => string;
|
|
17
21
|
readonly decisionHref: (projectKey: string, decisionId: string) => string;
|
|
22
|
+
/** Where the page is with the week of a day at the top. */
|
|
23
|
+
readonly weekHref: (day: string) => string;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
export function WorkList(props: WorkListProps) {
|
|
@@ -25,6 +31,10 @@ export function WorkList(props: WorkListProps) {
|
|
|
25
31
|
const { tasks, decisions } = props.work;
|
|
26
32
|
const projectOf = new Map(tasks.map((item) => [item.task.id, item.project]));
|
|
27
33
|
const hrefOf = (task: RestTask) => props.taskHref(projectOf.get(task.id)?.key ?? "", task);
|
|
34
|
+
const markOf = (task: RestTask) => {
|
|
35
|
+
const project = projectOf.get(task.id);
|
|
36
|
+
return project === undefined ? undefined : <ProjectMark project={project} size="sm" />;
|
|
37
|
+
};
|
|
28
38
|
const nameOf = (task: RestTask) => {
|
|
29
39
|
const project = projectOf.get(task.id);
|
|
30
40
|
return project === undefined ? undefined : (
|
|
@@ -45,49 +55,59 @@ export function WorkList(props: WorkListProps) {
|
|
|
45
55
|
}
|
|
46
56
|
return (
|
|
47
57
|
<div className="sc-work">
|
|
48
|
-
|
|
49
|
-
.
|
|
50
|
-
.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
58
|
+
<Week
|
|
59
|
+
tasks={tasks.map((item) => item.task)}
|
|
60
|
+
day={props.week}
|
|
61
|
+
today={now}
|
|
62
|
+
taskHref={hrefOf}
|
|
63
|
+
weekHref={props.weekHref}
|
|
64
|
+
markOf={markOf}
|
|
65
|
+
/>
|
|
66
|
+
<div className="sc-work-lists">
|
|
67
|
+
{groups
|
|
68
|
+
.filter((group) => group.tasks.length > 0)
|
|
69
|
+
.map((group) => (
|
|
70
|
+
<section key={group.title} className="sc-task-section" aria-label={group.title}>
|
|
71
|
+
<h2 className="sc-section-title">
|
|
72
|
+
{group.title} <span className="sc-column-count">{group.tasks.length}</span>
|
|
73
|
+
</h2>
|
|
74
|
+
<TaskList
|
|
75
|
+
tasks={group.tasks}
|
|
76
|
+
taskHref={hrefOf}
|
|
77
|
+
projectOf={nameOf}
|
|
78
|
+
showState
|
|
79
|
+
today={now}
|
|
80
|
+
/>
|
|
81
|
+
</section>
|
|
82
|
+
))}
|
|
83
|
+
<section className="sc-task-section" aria-label={work.decisions}>
|
|
84
|
+
<h2 className="sc-section-title">
|
|
85
|
+
{work.decisions} <span className="sc-column-count">{decisions.length}</span>
|
|
86
|
+
</h2>
|
|
87
|
+
{decisions.length === 0 ? (
|
|
88
|
+
<p className="sc-muted">{work.noDecisions}</p>
|
|
89
|
+
) : (
|
|
90
|
+
<ol className="sc-decision-log">
|
|
91
|
+
{decisions.map(({ project, decision }) => (
|
|
92
|
+
<li key={decision.id} className="sc-decision-row">
|
|
93
|
+
<a
|
|
94
|
+
className="sc-decision-row-question"
|
|
95
|
+
href={props.decisionHref(project.key, decision.id)}
|
|
96
|
+
>
|
|
97
|
+
{decision.question}
|
|
98
|
+
</a>
|
|
99
|
+
<span className="sc-task-row-project">
|
|
100
|
+
<ProjectMark project={project} size="sm" /> {work.inProject(project.name)}
|
|
101
|
+
</span>
|
|
102
|
+
<span className="sc-decision-row-meta">
|
|
103
|
+
<PersonChip person={decision.raisedBy} /> <Time iso={decision.createdAt} />
|
|
104
|
+
</span>
|
|
105
|
+
</li>
|
|
106
|
+
))}
|
|
107
|
+
</ol>
|
|
108
|
+
)}
|
|
109
|
+
</section>
|
|
110
|
+
</div>
|
|
91
111
|
</div>
|
|
92
112
|
);
|
|
93
113
|
}
|
package/src/connect-page.tsx
CHANGED
|
@@ -70,7 +70,6 @@ export function ConnectPage(props: ConnectPageProps) {
|
|
|
70
70
|
);
|
|
71
71
|
const ask = (
|
|
72
72
|
<>
|
|
73
|
-
<p className="sc-lead">{words.lead}</p>
|
|
74
73
|
<section className="sc-panel" aria-label={words.aria}>
|
|
75
74
|
<ConnectCodeForm busy={busy} onSubmit={(typed) => navigation.go(connectHref(typed))} />
|
|
76
75
|
</section>
|