@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.
Files changed (179) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +4 -0
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +201 -25
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +15 -11
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +59 -26
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/main.js +1 -0
  17. package/dist/cli/main.js.map +1 -1
  18. package/dist/cli/pack.d.ts +7 -2
  19. package/dist/cli/pack.d.ts.map +1 -1
  20. package/dist/cli/pack.js +16 -2
  21. package/dist/cli/pack.js.map +1 -1
  22. package/dist/client.d.ts +20 -10
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +17 -3
  25. package/dist/client.js.map +1 -1
  26. package/dist/compat.d.ts +28 -6
  27. package/dist/compat.d.ts.map +1 -1
  28. package/dist/compat.js +55 -14
  29. package/dist/compat.js.map +1 -1
  30. package/dist/components/board.d.ts +12 -1
  31. package/dist/components/board.d.ts.map +1 -1
  32. package/dist/components/board.js +57 -7
  33. package/dist/components/board.js.map +1 -1
  34. package/dist/components/calendar.d.ts +17 -7
  35. package/dist/components/calendar.d.ts.map +1 -1
  36. package/dist/components/calendar.js +89 -38
  37. package/dist/components/calendar.js.map +1 -1
  38. package/dist/components/decision-form.d.ts +12 -4
  39. package/dist/components/decision-form.d.ts.map +1 -1
  40. package/dist/components/decision-form.js +24 -6
  41. package/dist/components/decision-form.js.map +1 -1
  42. package/dist/components/decision-list.d.ts +4 -0
  43. package/dist/components/decision-list.d.ts.map +1 -1
  44. package/dist/components/decision-list.js +9 -6
  45. package/dist/components/decision-list.js.map +1 -1
  46. package/dist/components/documents.d.ts.map +1 -1
  47. package/dist/components/documents.js +3 -3
  48. package/dist/components/documents.js.map +1 -1
  49. package/dist/components/event-feed.d.ts.map +1 -1
  50. package/dist/components/event-feed.js +63 -8
  51. package/dist/components/event-feed.js.map +1 -1
  52. package/dist/components/my-decisions.d.ts +20 -0
  53. package/dist/components/my-decisions.d.ts.map +1 -0
  54. package/dist/components/my-decisions.js +36 -0
  55. package/dist/components/my-decisions.js.map +1 -0
  56. package/dist/components/people.d.ts.map +1 -1
  57. package/dist/components/people.js +1 -1
  58. package/dist/components/people.js.map +1 -1
  59. package/dist/components/picker.d.ts +39 -0
  60. package/dist/components/picker.d.ts.map +1 -0
  61. package/dist/components/picker.js +155 -0
  62. package/dist/components/picker.js.map +1 -0
  63. package/dist/components/projects.d.ts.map +1 -1
  64. package/dist/components/projects.js +3 -3
  65. package/dist/components/projects.js.map +1 -1
  66. package/dist/components/roadmap.js +1 -1
  67. package/dist/components/roadmap.js.map +1 -1
  68. package/dist/components/runs.d.ts.map +1 -1
  69. package/dist/components/runs.js +1 -1
  70. package/dist/components/runs.js.map +1 -1
  71. package/dist/components/shell.d.ts.map +1 -1
  72. package/dist/components/shell.js +36 -1
  73. package/dist/components/shell.js.map +1 -1
  74. package/dist/components/task-form.d.ts +18 -4
  75. package/dist/components/task-form.d.ts.map +1 -1
  76. package/dist/components/task-form.js +56 -13
  77. package/dist/components/task-form.js.map +1 -1
  78. package/dist/components/task-list.js +2 -2
  79. package/dist/components/task-list.js.map +1 -1
  80. package/dist/components/task-view.d.ts +7 -1
  81. package/dist/components/task-view.d.ts.map +1 -1
  82. package/dist/components/task-view.js +7 -6
  83. package/dist/components/task-view.js.map +1 -1
  84. package/dist/components/ui.d.ts +17 -0
  85. package/dist/components/ui.d.ts.map +1 -1
  86. package/dist/components/ui.js +26 -11
  87. package/dist/components/ui.js.map +1 -1
  88. package/dist/components/work.d.ts.map +1 -1
  89. package/dist/components/work.js +5 -3
  90. package/dist/components/work.js.map +1 -1
  91. package/dist/console.css +1794 -433
  92. package/dist/console.d.ts.map +1 -1
  93. package/dist/console.js +93 -10
  94. package/dist/console.js.map +1 -1
  95. package/dist/data.d.ts +21 -1
  96. package/dist/data.d.ts.map +1 -1
  97. package/dist/data.js +24 -1
  98. package/dist/data.js.map +1 -1
  99. package/dist/i18n/en.d.ts +45 -3
  100. package/dist/i18n/en.d.ts.map +1 -1
  101. package/dist/i18n/en.js +52 -10
  102. package/dist/i18n/en.js.map +1 -1
  103. package/dist/i18n/ko.d.ts.map +1 -1
  104. package/dist/i18n/ko.js +54 -12
  105. package/dist/i18n/ko.js.map +1 -1
  106. package/dist/index.d.ts +5 -3
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +5 -3
  109. package/dist/index.js.map +1 -1
  110. package/dist/limits.d.ts +2 -0
  111. package/dist/limits.d.ts.map +1 -1
  112. package/dist/limits.js +2 -0
  113. package/dist/limits.js.map +1 -1
  114. package/dist/router.d.ts +9 -1
  115. package/dist/router.d.ts.map +1 -1
  116. package/dist/router.js +12 -0
  117. package/dist/router.js.map +1 -1
  118. package/dist/routes.d.ts +3 -1
  119. package/dist/routes.d.ts.map +1 -1
  120. package/dist/routes.js +3 -1
  121. package/dist/routes.js.map +1 -1
  122. package/dist/rules.js +1 -1
  123. package/dist/rules.js.map +1 -1
  124. package/dist/schemas.d.ts +252 -12
  125. package/dist/schemas.d.ts.map +1 -1
  126. package/dist/schemas.js +27 -10
  127. package/dist/schemas.js.map +1 -1
  128. package/dist/vocabulary.d.ts +2 -0
  129. package/dist/vocabulary.d.ts.map +1 -1
  130. package/dist/vocabulary.js +2 -0
  131. package/dist/vocabulary.js.map +1 -1
  132. package/docs/specs/cli.md +14 -13
  133. package/docs/specs/rest.md +24 -19
  134. package/package.json +2 -2
  135. package/skills/building-a-console/SKILL.md +4 -4
  136. package/skills/building-a-console/references/the-default-console.md +4 -4
  137. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  138. package/skills/deploying-a-console/SKILL.md +2 -2
  139. package/skills/deploying-a-console/references/resources.md +1 -1
  140. package/skills/working-the-board/SKILL.md +8 -6
  141. package/skills/working-the-board/references/terms.md +3 -3
  142. package/src/api.ts +7 -0
  143. package/src/cli/cli.ts +254 -25
  144. package/src/cli/format.ts +22 -11
  145. package/src/cli/help.ts +63 -26
  146. package/src/cli/main.ts +1 -0
  147. package/src/cli/pack.ts +17 -1
  148. package/src/client.ts +44 -11
  149. package/src/compat.ts +63 -14
  150. package/src/components/board.tsx +124 -25
  151. package/src/components/calendar.tsx +258 -117
  152. package/src/components/decision-form.tsx +71 -23
  153. package/src/components/decision-list.tsx +30 -3
  154. package/src/components/documents.tsx +27 -11
  155. package/src/components/event-feed.tsx +116 -33
  156. package/src/components/my-decisions.tsx +138 -0
  157. package/src/components/people.tsx +3 -1
  158. package/src/components/picker.tsx +297 -0
  159. package/src/components/projects.tsx +7 -5
  160. package/src/components/roadmap.tsx +1 -1
  161. package/src/components/runs.tsx +5 -1
  162. package/src/components/shell.tsx +36 -1
  163. package/src/components/task-form.tsx +149 -63
  164. package/src/components/task-list.tsx +3 -3
  165. package/src/components/task-view.tsx +71 -56
  166. package/src/components/ui.tsx +89 -11
  167. package/src/components/work.tsx +6 -4
  168. package/src/console.tsx +157 -29
  169. package/src/data.ts +61 -1
  170. package/src/i18n/en.ts +54 -10
  171. package/src/i18n/ko.ts +56 -12
  172. package/src/index.ts +19 -1
  173. package/src/limits.ts +2 -0
  174. package/src/router.ts +18 -1
  175. package/src/routes.ts +3 -1
  176. package/src/rules.ts +1 -1
  177. package/src/schemas.ts +29 -9
  178. package/src/styles/console.css +1794 -433
  179. package/src/vocabulary.ts +3 -0
@@ -1,10 +1,11 @@
1
- import { type FormEvent, useState } from "react";
1
+ import { type FormEvent, type ReactNode, useState } from "react";
2
2
  import {
3
3
  MAX_BODY_LENGTH,
4
4
  MAX_LINK_LABEL_LENGTH,
5
5
  MAX_LINKS,
6
6
  MAX_TITLE_LENGTH,
7
7
  MAX_URL_LENGTH,
8
+ MAX_WORKERS,
8
9
  type RestCategory,
9
10
  type RestGoal,
10
11
  type RestPerson,
@@ -14,22 +15,33 @@ import {
14
15
  TASK_STATES,
15
16
  } from "../api.js";
16
17
  import { useWords } from "../i18n/index.js";
17
- import { Button, personName } from "./ui.js";
18
+ import { Picker, type PickerSearch } from "./picker.js";
19
+ import { Button, GoalStateBadge, PersonChip, personName, StateBadge } from "./ui.js";
18
20
 
19
21
  // Writing a task, or changing one. The bounds are the package's, said before a request is made.
20
22
  // The plan is two days (ADR-0019); what happened to the task is the board's to keep, not a
21
- // field here.
23
+ // field here. The workers are any number of the people (ADR-0037); the task this one is part of
24
+ // and the goal it works towards are found by searching, a page at a time, never offered whole.
25
+
26
+ /** What words find among the tasks of the project, a page at a time. */
27
+ export type TaskSearch = PickerSearch<RestTask>;
28
+ /** What words find among the open goals of the project, a page at a time. */
29
+ export type GoalSearch = PickerSearch<RestGoal>;
22
30
 
23
31
  export interface TaskFormProps {
24
32
  readonly people: readonly RestPerson[];
25
33
  /** The task to change; left out, the form writes a new one. */
26
34
  readonly task?: RestTask | undefined;
27
- /** The tasks a new one may be part of. */
28
- readonly parents?: readonly RestTask[] | undefined;
35
+ /** The task this one is part of, as it stands, for the form to show it; the search finds another. */
36
+ readonly parent?: RestTask | undefined;
37
+ /** Left out, a task cannot be made part of one here. */
38
+ readonly findTasks?: TaskSearch | undefined;
29
39
  /** The categories of the workspace, to sort the task into one. */
30
40
  readonly categories?: readonly RestCategory[] | undefined;
31
- /** The goals of the project, for the task to work towards one. */
32
- readonly goals?: readonly RestGoal[] | undefined;
41
+ /** The goal the task works towards, as it stands, for the form to show it; the search finds another. */
42
+ readonly goal?: RestGoal | undefined;
43
+ /** Left out, no goal is offered. */
44
+ readonly findGoals?: GoalSearch | undefined;
33
45
  readonly busy?: boolean | undefined;
34
46
  readonly error?: string | undefined;
35
47
  readonly onSubmit: (input: RestTaskInput) => void;
@@ -41,6 +53,25 @@ interface LinkDraft {
41
53
  readonly label: string;
42
54
  }
43
55
 
56
+ /** A task as a picker names one: its number and its title, with its state. */
57
+ export function taskLabel(task: RestTask): ReactNode {
58
+ return (
59
+ <>
60
+ <span className="sc-card-number">#{task.number}</span> {task.title}{" "}
61
+ <StateBadge state={task.state} />
62
+ </>
63
+ );
64
+ }
65
+
66
+ /** A goal as a picker names one: its title, with its state. */
67
+ export function goalLabel(goal: RestGoal): ReactNode {
68
+ return (
69
+ <>
70
+ {goal.title} <GoalStateBadge state={goal.state} />
71
+ </>
72
+ );
73
+ }
74
+
44
75
  export function TaskForm(props: TaskFormProps) {
45
76
  const words = useWords();
46
77
  const form = words.task.form;
@@ -49,10 +80,16 @@ export function TaskForm(props: TaskFormProps) {
49
80
  const [body, setBody] = useState(task?.body ?? "");
50
81
  const [state, setState] = useState(task?.state ?? "idea");
51
82
  const [priority, setPriority] = useState(task?.priority ?? "normal");
52
- const [assigneeId, setAssigneeId] = useState(task?.assignee?.id ?? "");
53
- const [parentId, setParentId] = useState(task?.parentId ?? "");
83
+ const [workerIds, setWorkerIds] = useState<readonly string[]>(
84
+ task?.workers.map((worker) => worker.id) ?? [],
85
+ );
86
+ // What is chosen is kept by id, so that a parent or a goal the page has not read back yet is
87
+ // kept as it is rather than dropped; the one chosen here is kept whole, to be shown.
88
+ const [parentId, setParentId] = useState<string | null>(task?.parentId ?? null);
89
+ const [parentChosen, setParentChosen] = useState<RestTask | undefined>(undefined);
90
+ const [goalId, setGoalId] = useState<string | null>(task?.goalId ?? null);
91
+ const [goalChosen, setGoalChosen] = useState<RestGoal | undefined>(undefined);
54
92
  const [category, setCategory] = useState(task?.category?.key ?? "");
55
- const [goalId, setGoalId] = useState(task?.goalId ?? "");
56
93
  const [startsOn, setStartsOn] = useState(task?.startsOn ?? "");
57
94
  const [dueOn, setDueOn] = useState(task?.dueOn ?? "");
58
95
  const [links, setLinks] = useState<readonly LinkDraft[]>(
@@ -66,10 +103,10 @@ export function TaskForm(props: TaskFormProps) {
66
103
  body,
67
104
  state,
68
105
  priority,
69
- assigneeId: assigneeId === "" ? null : assigneeId,
70
- parentId: parentId === "" ? null : parentId,
106
+ workerIds: [...workerIds],
107
+ parentId,
71
108
  category: category === "" ? null : category,
72
- goalId: goalId === "" ? null : goalId,
109
+ goalId,
73
110
  startsOn: startsOn === "" ? null : startsOn,
74
111
  dueOn: dueOn === "" ? null : dueOn,
75
112
  links: links
@@ -81,12 +118,33 @@ export function TaskForm(props: TaskFormProps) {
81
118
  });
82
119
  };
83
120
 
84
- const parents = (props.parents ?? []).filter((candidate) => candidate.id !== task?.id);
121
+ // A worker is one of the people, or one the task had who is no longer among them.
122
+ const personOf = (id: string): RestPerson | undefined =>
123
+ props.people.find((person) => person.id === id) ??
124
+ task?.workers.find((worker) => worker.id === id);
125
+ const chosenWorkers = workerIds.flatMap((id) => {
126
+ const person = personOf(id);
127
+ return person === undefined ? [] : [person];
128
+ });
129
+ const otherPeople = props.people.filter((person) => !workerIds.includes(person.id));
130
+ const parent =
131
+ parentId === null
132
+ ? undefined
133
+ : parentChosen?.id === parentId
134
+ ? parentChosen
135
+ : props.parent?.id === parentId
136
+ ? props.parent
137
+ : undefined;
138
+ const goal =
139
+ goalId === null
140
+ ? undefined
141
+ : goalChosen?.id === goalId
142
+ ? goalChosen
143
+ : props.goal?.id === goalId
144
+ ? props.goal
145
+ : undefined;
146
+ const held = <span className="sc-muted">&hellip;</span>;
85
147
  const categories = props.categories ?? [];
86
- // A goal let go is not offered, unless the task is on it already.
87
- const goals = (props.goals ?? []).filter(
88
- (goal) => goal.state !== "dropped" || goal.id === task?.goalId,
89
- );
90
148
  const datesInOrder = startsOn === "" || dueOn === "" || startsOn <= dueOn;
91
149
 
92
150
  return (
@@ -142,21 +200,6 @@ export function TaskForm(props: TaskFormProps) {
142
200
  ))}
143
201
  </select>
144
202
  </label>
145
- <label className="sc-field">
146
- <span className="sc-field-label">{form.assignee}</span>
147
- <select
148
- className="sc-input"
149
- value={assigneeId}
150
- onChange={(event) => setAssigneeId(event.target.value)}
151
- >
152
- <option value="">{words.common.nobody}</option>
153
- {props.people.map((person) => (
154
- <option key={person.id} value={person.id}>
155
- {personName(person)}
156
- </option>
157
- ))}
158
- </select>
159
- </label>
160
203
  {categories.length > 0 && (
161
204
  <label className="sc-field">
162
205
  <span className="sc-field-label">{form.category}</span>
@@ -175,6 +218,44 @@ export function TaskForm(props: TaskFormProps) {
175
218
  </label>
176
219
  )}
177
220
  </div>
221
+ <fieldset className="sc-field sc-workers-field">
222
+ <legend className="sc-field-label">{form.workers}</legend>
223
+ <div className="sc-chips">
224
+ {chosenWorkers.map((person) => (
225
+ <span key={person.id} className="sc-chip">
226
+ <PersonChip person={person} />
227
+ <button
228
+ type="button"
229
+ className="sc-chip-remove"
230
+ aria-label={`${words.common.remove}: ${personName(person)}`}
231
+ onClick={() => setWorkerIds(workerIds.filter((id) => id !== person.id))}
232
+ >
233
+ <span aria-hidden="true">&times;</span>
234
+ </button>
235
+ </span>
236
+ ))}
237
+ {chosenWorkers.length === 0 && <span className="sc-muted">{words.common.nobody}</span>}
238
+ {otherPeople.length > 0 && workerIds.length < MAX_WORKERS && (
239
+ <select
240
+ className="sc-input sc-chip-add"
241
+ aria-label={form.addWorker}
242
+ value=""
243
+ onChange={(event) => {
244
+ if (event.target.value !== "") {
245
+ setWorkerIds([...workerIds, event.target.value]);
246
+ }
247
+ }}
248
+ >
249
+ <option value="">{form.addWorker}</option>
250
+ {otherPeople.map((person) => (
251
+ <option key={person.id} value={person.id}>
252
+ {personName(person)}
253
+ </option>
254
+ ))}
255
+ </select>
256
+ )}
257
+ </div>
258
+ </fieldset>
178
259
  <div className="sc-field-row">
179
260
  <label className="sc-field">
180
261
  <span className="sc-field-label">{form.startsOn}</span>
@@ -195,39 +276,44 @@ export function TaskForm(props: TaskFormProps) {
195
276
  onChange={(event) => setDueOn(event.target.value)}
196
277
  />
197
278
  </label>
198
- {goals.length > 0 && (
199
- <label className="sc-field">
279
+ {props.findGoals !== undefined && (
280
+ <div className="sc-field">
200
281
  <span className="sc-field-label">{form.goal}</span>
201
- <select
202
- className="sc-input"
203
- value={goalId}
204
- onChange={(event) => setGoalId(event.target.value)}
205
- >
206
- <option value="">{form.noGoal}</option>
207
- {goals.map((goal) => (
208
- <option key={goal.id} value={goal.id}>
209
- {goal.title}
210
- </option>
211
- ))}
212
- </select>
213
- </label>
282
+ <Picker
283
+ value={goal}
284
+ held={goalId === null ? undefined : held}
285
+ search={props.findGoals}
286
+ keyOf={(candidate) => candidate.id}
287
+ render={goalLabel}
288
+ label={form.goal}
289
+ placeholder={form.searchGoals}
290
+ disabled={props.busy}
291
+ onChange={(chosen) => {
292
+ setGoalChosen(chosen);
293
+ setGoalId(chosen?.id ?? null);
294
+ }}
295
+ />
296
+ </div>
214
297
  )}
215
- {parents.length > 0 && (
216
- <label className="sc-field">
298
+ {props.findTasks !== undefined && (
299
+ <div className="sc-field">
217
300
  <span className="sc-field-label">{form.partOf}</span>
218
- <select
219
- className="sc-input"
220
- value={parentId}
221
- onChange={(event) => setParentId(event.target.value)}
222
- >
223
- <option value="">{words.common.nothing}</option>
224
- {parents.map((candidate) => (
225
- <option key={candidate.id} value={candidate.id}>
226
- #{candidate.number} {candidate.title}
227
- </option>
228
- ))}
229
- </select>
230
- </label>
301
+ <Picker
302
+ value={parent}
303
+ held={parentId === null ? undefined : held}
304
+ search={props.findTasks}
305
+ keyOf={(candidate) => candidate.id}
306
+ render={taskLabel}
307
+ label={form.partOf}
308
+ placeholder={form.searchTasks}
309
+ disabled={props.busy}
310
+ exclude={(candidate) => candidate.id === task?.id}
311
+ onChange={(chosen) => {
312
+ setParentChosen(chosen);
313
+ setParentId(chosen?.id ?? null);
314
+ }}
315
+ />
316
+ </div>
231
317
  )}
232
318
  </div>
233
319
  <fieldset className="sc-field sc-links">
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
2
2
  import type { RestTask } from "../api.js";
3
3
  import { useWords } from "../i18n/index.js";
4
4
  import { ShowMore, usePaged } from "./paged.js";
5
- import { Avatar, Badge, CategoryBadge, DueBadge, StateBadge } from "./ui.js";
5
+ import { Badge, CategoryBadge, DueBadge, StateBadge, Workers } from "./ui.js";
6
6
 
7
7
  // A list of tasks as rows, where the board's columns are not the point: the calendar's late
8
8
  // and unscheduled tasks, a goal's tasks, a person's own work. Each row says what a card says,
@@ -45,14 +45,14 @@ export function TaskList(props: TaskListProps) {
45
45
  {task.category !== null && <CategoryBadge category={task.category} />}
46
46
  {props.showState === true && <StateBadge state={task.state} />}
47
47
  {task.openDecisions > 0 && (
48
- <Badge tone="warning" title={words.board.decisionsTitle}>
48
+ <Badge tone="warning" kind="count" title={words.board.decisionsTitle}>
49
49
  {words.board.decisions(task.openDecisions)}
50
50
  </Badge>
51
51
  )}
52
52
  {task.dueOn !== null && (
53
53
  <DueBadge dueOn={task.dueOn} state={task.state} today={props.today} />
54
54
  )}
55
- {task.assignee !== null && <Avatar person={task.assignee} size="sm" />}
55
+ <Workers workers={task.workers} size="sm" />
56
56
  </span>
57
57
  </li>
58
58
  );
@@ -21,7 +21,7 @@ import { type CommentDraft, TaskThread } from "./comments.js";
21
21
  import { DecisionForm } from "./decision-form.js";
22
22
  import { EventFeed } from "./event-feed.js";
23
23
  import { Markdown } from "./markdown.js";
24
- import { TaskForm } from "./task-form.js";
24
+ import { type GoalSearch, TaskForm, type TaskSearch } from "./task-form.js";
25
25
  import {
26
26
  Badge,
27
27
  Button,
@@ -44,8 +44,13 @@ import {
44
44
  export interface TaskViewProps {
45
45
  readonly task: RestTask;
46
46
  readonly people: readonly RestPerson[];
47
- /** Every task of the board, for the breakdown and for what this one may be part of. */
47
+ /** The current tasks of the board, for the breakdown and for naming what this one is part of. */
48
48
  readonly tasks: readonly RestTask[];
49
+ /** The task this one is part of, when the page has it: one put away is not among `tasks`. Left out, it is looked for among them. */
50
+ readonly parent?: RestTask | undefined;
51
+ /** What words find among the tasks and the open goals of the project, for the form (ADR-0037); left out, the form offers neither. */
52
+ readonly findTasks?: TaskSearch | undefined;
53
+ readonly findGoals?: GoalSearch | undefined;
49
54
  readonly decisions: readonly RestDecision[];
50
55
  /** The runs of the board, or of this task; the ones on this task are shown. */
51
56
  readonly runs?: readonly RestRun[] | undefined;
@@ -97,9 +102,10 @@ export function TaskView(props: TaskViewProps) {
97
102
  const { tag } = useLanguage();
98
103
  const { task } = props;
99
104
  const parent =
100
- task.parentId === null
105
+ props.parent ??
106
+ (task.parentId === null
101
107
  ? undefined
102
- : props.tasks.find((candidate) => candidate.id === task.parentId);
108
+ : props.tasks.find((candidate) => candidate.id === task.parentId));
103
109
  const subtasks = props.tasks.filter((candidate) => candidate.parentId === task.id);
104
110
  const about = props.decisions.filter((decision) => decision.taskId === task.id);
105
111
  const runs = (props.runs ?? []).filter((run) => run.taskId === task.id);
@@ -119,9 +125,11 @@ export function TaskView(props: TaskViewProps) {
119
125
  <TaskForm
120
126
  people={props.people}
121
127
  task={task}
122
- parents={props.tasks}
128
+ parent={parent}
129
+ findTasks={props.findTasks}
123
130
  categories={props.categories}
124
- goals={props.goals}
131
+ goal={goal}
132
+ findGoals={props.findGoals}
125
133
  busy={props.busy}
126
134
  error={props.error}
127
135
  onSubmit={(input) => props.onChange(task, input)}
@@ -134,26 +142,56 @@ export function TaskView(props: TaskViewProps) {
134
142
  return (
135
143
  <div className="sc-task">
136
144
  <header className="sc-task-header">
137
- {parent !== undefined && (
138
- <p className="sc-task-parent">
139
- {words.task.partOf}{" "}
140
- <a href={props.taskHref(parent)}>
141
- #{parent.number} {parent.title}
142
- </a>
143
- </p>
144
- )}
145
- <h1 className="sc-task-title">
146
- <span className="sc-card-number">#{task.number}</span> {task.title}
147
- </h1>
148
- <div className="sc-task-badges">
149
- <StateBadge state={task.state} />
150
- {task.archivedAt !== null && <Badge tone="neutral">{words.task.archived}</Badge>}
151
- {task.category !== null && <CategoryBadge category={task.category} />}
152
- <PriorityBadge priority={task.priority} />
153
- {task.dueOn !== null && (
154
- <DueBadge dueOn={task.dueOn} state={task.state} today={props.today} />
145
+ <div className="sc-task-heading">
146
+ {parent !== undefined && (
147
+ <p className="sc-task-parent">
148
+ {words.task.partOf}{" "}
149
+ <a href={props.taskHref(parent)}>
150
+ #{parent.number} {parent.title}
151
+ </a>
152
+ </p>
155
153
  )}
154
+ <h1 className="sc-task-title">
155
+ <span className="sc-card-number">#{task.number}</span> {task.title}
156
+ </h1>
157
+ <div className="sc-task-badges">
158
+ <StateBadge state={task.state} />
159
+ {task.archivedAt !== null && <Badge tone="neutral">{words.task.archived}</Badge>}
160
+ {task.category !== null && <CategoryBadge category={task.category} />}
161
+ <PriorityBadge priority={task.priority} />
162
+ {task.dueOn !== null && (
163
+ <DueBadge dueOn={task.dueOn} state={task.state} today={props.today} />
164
+ )}
165
+ </div>
156
166
  </div>
167
+ {props.formHref !== undefined && (
168
+ <div className="sc-task-actions">
169
+ <a className="sc-button sc-button-secondary" href={props.formHref("edit")}>
170
+ {words.common.edit}
171
+ </a>
172
+ <a className="sc-button sc-button-secondary" href={props.formHref("ask")}>
173
+ {words.task.raiseDecision}
174
+ </a>
175
+ {props.onArchive !== undefined && task.archivedAt === null && (
176
+ <Button
177
+ variant="secondary"
178
+ disabled={props.busy === true}
179
+ onClick={() => props.onArchive?.(task)}
180
+ >
181
+ {words.task.archive}
182
+ </Button>
183
+ )}
184
+ {props.onRestore !== undefined && task.archivedAt !== null && (
185
+ <Button
186
+ variant="secondary"
187
+ disabled={props.busy === true}
188
+ onClick={() => props.onRestore?.(task)}
189
+ >
190
+ {words.task.restore}
191
+ </Button>
192
+ )}
193
+ </div>
194
+ )}
157
195
  </header>
158
196
  <div className="sc-task-body">
159
197
  <div className="sc-task-main">
@@ -185,7 +223,6 @@ export function TaskView(props: TaskViewProps) {
185
223
  <section className="sc-task-section" aria-label={words.task.raiseDecision}>
186
224
  <h2 className="sc-section-title">{words.task.raiseDecision}</h2>
187
225
  <DecisionForm
188
- tasks={props.tasks}
189
226
  taskId={task.id}
190
227
  busy={props.busy}
191
228
  onSubmit={props.onRaiseDecision}
@@ -289,12 +326,18 @@ export function TaskView(props: TaskViewProps) {
289
326
  </dd>
290
327
  </div>
291
328
  <div>
292
- <dt>{words.task.assignee}</dt>
329
+ <dt>{words.task.workers}</dt>
293
330
  <dd>
294
- {task.assignee === null ? (
331
+ {task.workers.length === 0 ? (
295
332
  <span className="sc-muted">{words.common.nobody}</span>
296
333
  ) : (
297
- <PersonChip person={task.assignee} />
334
+ <ul className="sc-worker-list">
335
+ {task.workers.map((worker) => (
336
+ <li key={worker.id}>
337
+ <PersonChip person={worker} />
338
+ </li>
339
+ ))}
340
+ </ul>
298
341
  )}
299
342
  </dd>
300
343
  </div>
@@ -346,34 +389,6 @@ export function TaskView(props: TaskViewProps) {
346
389
  </dd>
347
390
  </div>
348
391
  </dl>
349
- {props.formHref !== undefined && (
350
- <div className="sc-task-actions">
351
- <a className="sc-button sc-button-secondary" href={props.formHref("edit")}>
352
- {words.common.edit}
353
- </a>
354
- <a className="sc-button sc-button-secondary" href={props.formHref("ask")}>
355
- {words.task.raiseDecision}
356
- </a>
357
- {props.onArchive !== undefined && task.archivedAt === null && (
358
- <Button
359
- variant="secondary"
360
- disabled={props.busy === true}
361
- onClick={() => props.onArchive?.(task)}
362
- >
363
- {words.task.archive}
364
- </Button>
365
- )}
366
- {props.onRestore !== undefined && task.archivedAt !== null && (
367
- <Button
368
- variant="secondary"
369
- disabled={props.busy === true}
370
- onClick={() => props.onRestore?.(task)}
371
- >
372
- {words.task.restore}
373
- </Button>
374
- )}
375
- </div>
376
- )}
377
392
  </aside>
378
393
  </div>
379
394
  </div>