@skillcdn/console 0.1.8 → 0.1.10
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 +5 -5
- package/dist/api.d.ts +5 -5
- 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.map +1 -1
- package/dist/cli/cli.js +255 -10
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +9 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +80 -0
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +62 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +31 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -3
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +27 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +75 -7
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +34 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +96 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/categories.d.ts +13 -0
- package/dist/components/categories.d.ts.map +1 -0
- package/dist/components/categories.js +52 -0
- package/dist/components/categories.js.map +1 -0
- package/dist/components/decision-list.d.ts +10 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +14 -1
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +12 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +35 -0
- package/dist/components/goals.d.ts.map +1 -0
- package/dist/components/goals.js +45 -0
- package/dist/components/goals.js.map +1 -0
- package/dist/components/task-form.d.ts +5 -1
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +13 -1
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.d.ts +16 -0
- package/dist/components/task-list.d.ts.map +1 -0
- package/dist/components/task-list.js +14 -0
- package/dist/components/task-list.js.map +1 -0
- package/dist/components/task-view.d.ts +9 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +10 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +30 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +60 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +10 -0
- package/dist/components/work.d.ts.map +1 -0
- package/dist/components/work.js +29 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +511 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +124 -8
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +36 -2
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +39 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/dates.js +120 -0
- package/dist/dates.js.map +1 -0
- package/dist/i18n/en.d.ts +162 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +174 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +169 -1
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +5 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +5 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +50 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +74 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +7 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +9 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +366 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +112 -3
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +15 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +26 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +11 -4
- package/docs/specs/rest.md +33 -8
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +4 -4
- package/skills/building-a-console/references/the-default-console.md +1 -1
- package/skills/deploying-a-console/SKILL.md +11 -10
- package/skills/deploying-a-console/assets/deployer-policy.json +13 -12
- package/skills/deploying-a-console/assets/secrets.example.json +4 -4
- package/skills/deploying-a-console/references/access.md +10 -9
- package/skills/deploying-a-console/references/resources.md +107 -50
- package/skills/deploying-a-console/references/sizing.md +1 -1
- package/skills/deploying-a-console/scripts/store-secrets.mjs +1 -15
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +30 -0
- package/src/cli/cli.ts +311 -10
- package/src/cli/format.ts +88 -0
- package/src/cli/help.ts +69 -7
- package/src/client.ts +70 -2
- package/src/components/board.tsx +205 -12
- package/src/components/calendar.tsx +255 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +62 -5
- package/src/components/event-feed.tsx +12 -0
- package/src/components/goals.tsx +264 -0
- package/src/components/task-form.tsx +78 -1
- package/src/components/task-list.tsx +61 -0
- package/src/components/task-view.tsx +74 -3
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +83 -0
- package/src/console.tsx +337 -4
- package/src/data.ts +63 -8
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +184 -1
- package/src/i18n/ko.ts +175 -1
- package/src/index.ts +58 -1
- package/src/limits.ts +5 -0
- package/src/router.ts +119 -6
- package/src/routes.ts +10 -0
- package/src/schemas.ts +141 -1
- package/src/styles/console.css +511 -0
- package/src/vocabulary.ts +33 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { type FormEvent, type ReactNode, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
GOAL_STATES,
|
|
4
|
+
type GoalState,
|
|
5
|
+
MAX_BODY_LENGTH,
|
|
6
|
+
MAX_TITLE_LENGTH,
|
|
7
|
+
type RestGoal,
|
|
8
|
+
type RestGoalInput,
|
|
9
|
+
type RestTask,
|
|
10
|
+
TASK_STATES,
|
|
11
|
+
} from "../api.js";
|
|
12
|
+
import { useWords } from "../i18n/index.js";
|
|
13
|
+
import { Markdown } from "./markdown.js";
|
|
14
|
+
import { TaskList } from "./task-list.js";
|
|
15
|
+
import { Button, Day, DueBadge, GoalStateBadge, PersonChip, ProgressBar, Time } from "./ui.js";
|
|
16
|
+
|
|
17
|
+
// The roadmap of a project (ADR-0019): its goals, each with the day it is meant to be reached
|
|
18
|
+
// by and how far its tasks are, read from the board as it stands. The list, one goal with its
|
|
19
|
+
// tasks, and the form an owner sets one with. Each takes its data as props and nothing from
|
|
20
|
+
// the network; what people wrote is shown as text, never as HTML.
|
|
21
|
+
|
|
22
|
+
/** A goal's day on the calendar's terms: reached or let go, it is a plain fact; else it may be late. */
|
|
23
|
+
const dueState = (goal: RestGoal) =>
|
|
24
|
+
goal.state === "done" ? "done" : goal.state === "dropped" ? "dropped" : "ready";
|
|
25
|
+
|
|
26
|
+
export interface GoalListProps {
|
|
27
|
+
readonly goals: readonly RestGoal[];
|
|
28
|
+
readonly goalHref: (goal: RestGoal) => string;
|
|
29
|
+
/** The day to count from; the browser's today when left out. */
|
|
30
|
+
readonly today?: string | undefined;
|
|
31
|
+
readonly empty?: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function GoalList(props: GoalListProps) {
|
|
35
|
+
const words = useWords().roadmap;
|
|
36
|
+
if (props.goals.length === 0) {
|
|
37
|
+
return <>{props.empty ?? null}</>;
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
<ul className="sc-goals" aria-label={words.title}>
|
|
41
|
+
{props.goals.map((goal) => (
|
|
42
|
+
<li key={goal.id} className={`sc-goal sc-goal-${goal.state}`}>
|
|
43
|
+
<div className="sc-goal-head">
|
|
44
|
+
<a className="sc-goal-title" href={props.goalHref(goal)}>
|
|
45
|
+
{goal.title}
|
|
46
|
+
</a>
|
|
47
|
+
<GoalStateBadge state={goal.state} />
|
|
48
|
+
{goal.dueOn !== null && (
|
|
49
|
+
<DueBadge dueOn={goal.dueOn} state={dueState(goal)} today={props.today} />
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
<ProgressBar counts={goal.taskCounts} />
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface GoalFormProps {
|
|
60
|
+
/** The goal to change; left out, the form sets a new one. */
|
|
61
|
+
readonly goal?: RestGoal | undefined;
|
|
62
|
+
readonly busy?: boolean | undefined;
|
|
63
|
+
readonly error?: string | undefined;
|
|
64
|
+
readonly onSubmit: (input: RestGoalInput) => void;
|
|
65
|
+
readonly onCancel?: (() => void) | undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function GoalForm(props: GoalFormProps) {
|
|
69
|
+
const words = useWords();
|
|
70
|
+
const form = words.roadmap.form;
|
|
71
|
+
const { goal } = props;
|
|
72
|
+
const [title, setTitle] = useState(goal?.title ?? "");
|
|
73
|
+
const [body, setBody] = useState(goal?.body ?? "");
|
|
74
|
+
const [state, setState] = useState<GoalState>(goal?.state ?? "planned");
|
|
75
|
+
const [dueOn, setDueOn] = useState(goal?.dueOn ?? "");
|
|
76
|
+
|
|
77
|
+
const submit = (event: FormEvent) => {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
props.onSubmit({ title: title.trim(), body, state, dueOn: dueOn === "" ? null : dueOn });
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<form className="sc-form" onSubmit={submit}>
|
|
84
|
+
<label className="sc-field">
|
|
85
|
+
<span className="sc-field-label">{form.title}</span>
|
|
86
|
+
<input
|
|
87
|
+
className="sc-input"
|
|
88
|
+
value={title}
|
|
89
|
+
maxLength={MAX_TITLE_LENGTH}
|
|
90
|
+
required
|
|
91
|
+
onChange={(event) => setTitle(event.target.value)}
|
|
92
|
+
/>
|
|
93
|
+
</label>
|
|
94
|
+
<label className="sc-field">
|
|
95
|
+
<span className="sc-field-label">{form.body}</span>
|
|
96
|
+
<textarea
|
|
97
|
+
className="sc-input sc-textarea"
|
|
98
|
+
value={body}
|
|
99
|
+
maxLength={MAX_BODY_LENGTH}
|
|
100
|
+
rows={4}
|
|
101
|
+
placeholder={form.bodyPlaceholder}
|
|
102
|
+
onChange={(event) => setBody(event.target.value)}
|
|
103
|
+
/>
|
|
104
|
+
</label>
|
|
105
|
+
<div className="sc-field-row">
|
|
106
|
+
<label className="sc-field">
|
|
107
|
+
<span className="sc-field-label">{form.state}</span>
|
|
108
|
+
<select
|
|
109
|
+
className="sc-input"
|
|
110
|
+
value={state}
|
|
111
|
+
onChange={(event) => setState(event.target.value as GoalState)}
|
|
112
|
+
>
|
|
113
|
+
{GOAL_STATES.map((candidate) => (
|
|
114
|
+
<option key={candidate} value={candidate}>
|
|
115
|
+
{words.vocabulary.goalState[candidate]}
|
|
116
|
+
</option>
|
|
117
|
+
))}
|
|
118
|
+
</select>
|
|
119
|
+
</label>
|
|
120
|
+
<label className="sc-field">
|
|
121
|
+
<span className="sc-field-label">{form.dueOn}</span>
|
|
122
|
+
<input
|
|
123
|
+
className="sc-input"
|
|
124
|
+
type="date"
|
|
125
|
+
value={dueOn}
|
|
126
|
+
onChange={(event) => setDueOn(event.target.value)}
|
|
127
|
+
/>
|
|
128
|
+
</label>
|
|
129
|
+
</div>
|
|
130
|
+
{props.error !== undefined && (
|
|
131
|
+
<p className="sc-form-error" role="alert">
|
|
132
|
+
{props.error}
|
|
133
|
+
</p>
|
|
134
|
+
)}
|
|
135
|
+
<div className="sc-form-actions">
|
|
136
|
+
<Button
|
|
137
|
+
type="submit"
|
|
138
|
+
variant="primary"
|
|
139
|
+
disabled={props.busy === true || title.trim() === ""}
|
|
140
|
+
>
|
|
141
|
+
{goal === undefined ? form.set : words.common.save}
|
|
142
|
+
</Button>
|
|
143
|
+
{props.onCancel !== undefined && (
|
|
144
|
+
<Button variant="ghost" onClick={props.onCancel}>
|
|
145
|
+
{words.common.cancel}
|
|
146
|
+
</Button>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</form>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface GoalViewProps {
|
|
154
|
+
readonly goal: RestGoal;
|
|
155
|
+
/** The tasks of the board; those that work towards the goal are shown, by state. */
|
|
156
|
+
readonly tasks: readonly RestTask[];
|
|
157
|
+
readonly taskHref: (task: RestTask) => string;
|
|
158
|
+
/** Where a document of the project is read, by its path, for the links in what was written. */
|
|
159
|
+
readonly docHref?: ((path: string) => string) | undefined;
|
|
160
|
+
readonly today?: string | undefined;
|
|
161
|
+
/** Where the goal is changed; left out, it cannot be changed from here. */
|
|
162
|
+
readonly editHref?: string | undefined;
|
|
163
|
+
/** Called when an owner moves the goal to another state from here. */
|
|
164
|
+
readonly onMove?: ((goal: RestGoal, state: GoalState) => void) | undefined;
|
|
165
|
+
readonly busy?: boolean | undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function GoalView(props: GoalViewProps) {
|
|
169
|
+
const words = useWords();
|
|
170
|
+
const roadmap = words.roadmap;
|
|
171
|
+
const { goal } = props;
|
|
172
|
+
const towards = props.tasks.filter((task) => task.goalId === goal.id);
|
|
173
|
+
return (
|
|
174
|
+
<div className="sc-task sc-goal-view">
|
|
175
|
+
<header className="sc-task-header">
|
|
176
|
+
<h1 className="sc-task-title">{goal.title}</h1>
|
|
177
|
+
<div className="sc-task-badges">
|
|
178
|
+
<GoalStateBadge state={goal.state} />
|
|
179
|
+
{goal.dueOn !== null && (
|
|
180
|
+
<DueBadge dueOn={goal.dueOn} state={dueState(goal)} today={props.today} />
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</header>
|
|
184
|
+
<div className="sc-task-body">
|
|
185
|
+
<div className="sc-task-main">
|
|
186
|
+
<ProgressBar counts={goal.taskCounts} />
|
|
187
|
+
{goal.body.length > 0 ? (
|
|
188
|
+
<Markdown source={goal.body} docHref={props.docHref} />
|
|
189
|
+
) : (
|
|
190
|
+
<p className="sc-muted">{roadmap.nothingMore}</p>
|
|
191
|
+
)}
|
|
192
|
+
<section className="sc-task-section" aria-label={roadmap.tasks}>
|
|
193
|
+
<h2 className="sc-section-title">{roadmap.tasks}</h2>
|
|
194
|
+
{towards.length === 0 ? (
|
|
195
|
+
<p className="sc-muted">{roadmap.noTasks}</p>
|
|
196
|
+
) : (
|
|
197
|
+
TASK_STATES.map((state) => {
|
|
198
|
+
const inState = towards.filter((task) => task.state === state);
|
|
199
|
+
return inState.length === 0 ? null : (
|
|
200
|
+
<div key={state} className="sc-goal-tasks">
|
|
201
|
+
<h3 className="sc-goal-tasks-state">
|
|
202
|
+
{words.vocabulary.state[state]}{" "}
|
|
203
|
+
<span className="sc-column-count">{inState.length}</span>
|
|
204
|
+
</h3>
|
|
205
|
+
<TaskList tasks={inState} taskHref={props.taskHref} today={props.today} />
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
})
|
|
209
|
+
)}
|
|
210
|
+
</section>
|
|
211
|
+
</div>
|
|
212
|
+
<aside className="sc-task-aside" aria-label={words.task.facts}>
|
|
213
|
+
<dl className="sc-facts">
|
|
214
|
+
<div>
|
|
215
|
+
<dt>{roadmap.state}</dt>
|
|
216
|
+
<dd>
|
|
217
|
+
{props.onMove === undefined ? (
|
|
218
|
+
words.vocabulary.goalState[goal.state]
|
|
219
|
+
) : (
|
|
220
|
+
<select
|
|
221
|
+
className="sc-input"
|
|
222
|
+
aria-label={roadmap.state}
|
|
223
|
+
value={goal.state}
|
|
224
|
+
disabled={props.busy === true}
|
|
225
|
+
onChange={(event) => props.onMove?.(goal, event.target.value as GoalState)}
|
|
226
|
+
>
|
|
227
|
+
{GOAL_STATES.map((state) => (
|
|
228
|
+
<option key={state} value={state}>
|
|
229
|
+
{words.vocabulary.goalState[state]}
|
|
230
|
+
</option>
|
|
231
|
+
))}
|
|
232
|
+
</select>
|
|
233
|
+
)}
|
|
234
|
+
</dd>
|
|
235
|
+
</div>
|
|
236
|
+
<div>
|
|
237
|
+
<dt>{roadmap.target}</dt>
|
|
238
|
+
<dd>
|
|
239
|
+
{goal.dueOn === null ? (
|
|
240
|
+
<span className="sc-muted">{roadmap.noTarget}</span>
|
|
241
|
+
) : (
|
|
242
|
+
<Day day={goal.dueOn} />
|
|
243
|
+
)}
|
|
244
|
+
</dd>
|
|
245
|
+
</div>
|
|
246
|
+
<div>
|
|
247
|
+
<dt>{roadmap.setBy}</dt>
|
|
248
|
+
<dd>
|
|
249
|
+
<PersonChip person={goal.createdBy} /> <Time iso={goal.createdAt} />
|
|
250
|
+
</dd>
|
|
251
|
+
</div>
|
|
252
|
+
</dl>
|
|
253
|
+
{props.editHref !== undefined && (
|
|
254
|
+
<div className="sc-task-actions">
|
|
255
|
+
<a className="sc-button sc-button-secondary" href={props.editHref}>
|
|
256
|
+
{words.common.edit}
|
|
257
|
+
</a>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
</aside>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
MAX_LINKS,
|
|
6
6
|
MAX_TITLE_LENGTH,
|
|
7
7
|
MAX_URL_LENGTH,
|
|
8
|
+
type RestCategory,
|
|
9
|
+
type RestGoal,
|
|
8
10
|
type RestPerson,
|
|
9
11
|
type RestTask,
|
|
10
12
|
type RestTaskInput,
|
|
@@ -15,6 +17,8 @@ import { useWords } from "../i18n/index.js";
|
|
|
15
17
|
import { Button } from "./ui.js";
|
|
16
18
|
|
|
17
19
|
// Writing a task, or changing one. The bounds are the package's, said before a request is made.
|
|
20
|
+
// The plan is two days (ADR-0019); what happened to the task is the board's to keep, not a
|
|
21
|
+
// field here.
|
|
18
22
|
|
|
19
23
|
export interface TaskFormProps {
|
|
20
24
|
readonly people: readonly RestPerson[];
|
|
@@ -22,6 +26,10 @@ export interface TaskFormProps {
|
|
|
22
26
|
readonly task?: RestTask | undefined;
|
|
23
27
|
/** The tasks a new one may be part of. */
|
|
24
28
|
readonly parents?: readonly RestTask[] | undefined;
|
|
29
|
+
/** The categories of the workspace, to sort the task into one. */
|
|
30
|
+
readonly categories?: readonly RestCategory[] | undefined;
|
|
31
|
+
/** The goals of the project, for the task to work towards one. */
|
|
32
|
+
readonly goals?: readonly RestGoal[] | undefined;
|
|
25
33
|
readonly busy?: boolean | undefined;
|
|
26
34
|
readonly error?: string | undefined;
|
|
27
35
|
readonly onSubmit: (input: RestTaskInput) => void;
|
|
@@ -43,6 +51,10 @@ export function TaskForm(props: TaskFormProps) {
|
|
|
43
51
|
const [priority, setPriority] = useState(task?.priority ?? "normal");
|
|
44
52
|
const [assigneeId, setAssigneeId] = useState(task?.assignee?.id ?? "");
|
|
45
53
|
const [parentId, setParentId] = useState(task?.parentId ?? "");
|
|
54
|
+
const [category, setCategory] = useState(task?.category?.key ?? "");
|
|
55
|
+
const [goalId, setGoalId] = useState(task?.goalId ?? "");
|
|
56
|
+
const [startsOn, setStartsOn] = useState(task?.startsOn ?? "");
|
|
57
|
+
const [dueOn, setDueOn] = useState(task?.dueOn ?? "");
|
|
46
58
|
const [links, setLinks] = useState<readonly LinkDraft[]>(
|
|
47
59
|
task?.links.map((link) => ({ url: link.url, label: link.label ?? "" })) ?? [],
|
|
48
60
|
);
|
|
@@ -56,6 +68,10 @@ export function TaskForm(props: TaskFormProps) {
|
|
|
56
68
|
priority,
|
|
57
69
|
assigneeId: assigneeId === "" ? null : assigneeId,
|
|
58
70
|
parentId: parentId === "" ? null : parentId,
|
|
71
|
+
category: category === "" ? null : category,
|
|
72
|
+
goalId: goalId === "" ? null : goalId,
|
|
73
|
+
startsOn: startsOn === "" ? null : startsOn,
|
|
74
|
+
dueOn: dueOn === "" ? null : dueOn,
|
|
59
75
|
links: links
|
|
60
76
|
.filter((link) => link.url.trim().length > 0)
|
|
61
77
|
.map((link) => ({
|
|
@@ -66,6 +82,12 @@ export function TaskForm(props: TaskFormProps) {
|
|
|
66
82
|
};
|
|
67
83
|
|
|
68
84
|
const parents = (props.parents ?? []).filter((candidate) => candidate.id !== task?.id);
|
|
85
|
+
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
|
+
const datesInOrder = startsOn === "" || dueOn === "" || startsOn <= dueOn;
|
|
69
91
|
|
|
70
92
|
return (
|
|
71
93
|
<form className="sc-form" onSubmit={submit}>
|
|
@@ -134,6 +156,61 @@ export function TaskForm(props: TaskFormProps) {
|
|
|
134
156
|
))}
|
|
135
157
|
</select>
|
|
136
158
|
</label>
|
|
159
|
+
{categories.length > 0 && (
|
|
160
|
+
<label className="sc-field">
|
|
161
|
+
<span className="sc-field-label">{form.category}</span>
|
|
162
|
+
<select
|
|
163
|
+
className="sc-input"
|
|
164
|
+
value={category}
|
|
165
|
+
onChange={(event) => setCategory(event.target.value)}
|
|
166
|
+
>
|
|
167
|
+
<option value="">{form.noCategory}</option>
|
|
168
|
+
{categories.map((candidate) => (
|
|
169
|
+
<option key={candidate.key} value={candidate.key}>
|
|
170
|
+
{candidate.name}
|
|
171
|
+
</option>
|
|
172
|
+
))}
|
|
173
|
+
</select>
|
|
174
|
+
</label>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
<div className="sc-field-row">
|
|
178
|
+
<label className="sc-field">
|
|
179
|
+
<span className="sc-field-label">{form.startsOn}</span>
|
|
180
|
+
<input
|
|
181
|
+
className="sc-input"
|
|
182
|
+
type="date"
|
|
183
|
+
value={startsOn}
|
|
184
|
+
onChange={(event) => setStartsOn(event.target.value)}
|
|
185
|
+
/>
|
|
186
|
+
</label>
|
|
187
|
+
<label className="sc-field">
|
|
188
|
+
<span className="sc-field-label">{form.dueOn}</span>
|
|
189
|
+
<input
|
|
190
|
+
className="sc-input"
|
|
191
|
+
type="date"
|
|
192
|
+
value={dueOn}
|
|
193
|
+
min={startsOn === "" ? undefined : startsOn}
|
|
194
|
+
onChange={(event) => setDueOn(event.target.value)}
|
|
195
|
+
/>
|
|
196
|
+
</label>
|
|
197
|
+
{goals.length > 0 && (
|
|
198
|
+
<label className="sc-field">
|
|
199
|
+
<span className="sc-field-label">{form.goal}</span>
|
|
200
|
+
<select
|
|
201
|
+
className="sc-input"
|
|
202
|
+
value={goalId}
|
|
203
|
+
onChange={(event) => setGoalId(event.target.value)}
|
|
204
|
+
>
|
|
205
|
+
<option value="">{form.noGoal}</option>
|
|
206
|
+
{goals.map((goal) => (
|
|
207
|
+
<option key={goal.id} value={goal.id}>
|
|
208
|
+
{goal.title}
|
|
209
|
+
</option>
|
|
210
|
+
))}
|
|
211
|
+
</select>
|
|
212
|
+
</label>
|
|
213
|
+
)}
|
|
137
214
|
{parents.length > 0 && (
|
|
138
215
|
<label className="sc-field">
|
|
139
216
|
<span className="sc-field-label">{form.partOf}</span>
|
|
@@ -214,7 +291,7 @@ export function TaskForm(props: TaskFormProps) {
|
|
|
214
291
|
<Button
|
|
215
292
|
type="submit"
|
|
216
293
|
variant="primary"
|
|
217
|
-
disabled={props.busy === true || title.trim() === ""}
|
|
294
|
+
disabled={props.busy === true || title.trim() === "" || !datesInOrder}
|
|
218
295
|
>
|
|
219
296
|
{task === undefined ? form.write : words.common.save}
|
|
220
297
|
</Button>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { RestTask } from "../api.js";
|
|
3
|
+
import { useWords } from "../i18n/index.js";
|
|
4
|
+
import { Avatar, Badge, CategoryBadge, DueBadge, StateBadge } from "./ui.js";
|
|
5
|
+
|
|
6
|
+
// A list of tasks as rows, where the board's columns are not the point: the calendar's late
|
|
7
|
+
// and unscheduled tasks, a goal's tasks, a person's own work. Each row says what a card says,
|
|
8
|
+
// in one line. Takes its data as props and nothing from the network.
|
|
9
|
+
|
|
10
|
+
export interface TaskListProps {
|
|
11
|
+
readonly tasks: readonly RestTask[];
|
|
12
|
+
readonly taskHref: (task: RestTask) => string;
|
|
13
|
+
/** Whether each row says the task's state: where the rows are not grouped by it. */
|
|
14
|
+
readonly showState?: boolean | undefined;
|
|
15
|
+
/** What the row says the task belongs to, where tasks of several projects are listed together. */
|
|
16
|
+
readonly projectOf?: ((task: RestTask) => string | undefined) | undefined;
|
|
17
|
+
/** The day to count due days from; the browser's today when left out. */
|
|
18
|
+
readonly today?: string | undefined;
|
|
19
|
+
readonly label?: string | undefined;
|
|
20
|
+
readonly empty?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function TaskList(props: TaskListProps) {
|
|
24
|
+
const words = useWords();
|
|
25
|
+
if (props.tasks.length === 0) {
|
|
26
|
+
return <>{props.empty ?? null}</>;
|
|
27
|
+
}
|
|
28
|
+
return (
|
|
29
|
+
<ul className="sc-task-rows" aria-label={props.label}>
|
|
30
|
+
{props.tasks.map((task) => {
|
|
31
|
+
const project = props.projectOf?.(task);
|
|
32
|
+
return (
|
|
33
|
+
<li key={task.id} className="sc-task-row">
|
|
34
|
+
<a className="sc-task-row-title" href={props.taskHref(task)}>
|
|
35
|
+
<span className="sc-card-number">#{task.number}</span> {task.title}
|
|
36
|
+
</a>
|
|
37
|
+
{project !== undefined && <span className="sc-task-row-project">{project}</span>}
|
|
38
|
+
<span className="sc-task-row-meta">
|
|
39
|
+
{task.category !== null && <CategoryBadge category={task.category} />}
|
|
40
|
+
{props.showState === true && <StateBadge state={task.state} />}
|
|
41
|
+
{task.openDecisions > 0 && (
|
|
42
|
+
<Badge tone="warning" title={words.board.decisionsTitle}>
|
|
43
|
+
{words.board.decisions(task.openDecisions)}
|
|
44
|
+
</Badge>
|
|
45
|
+
)}
|
|
46
|
+
{task.openRuns > 0 && (
|
|
47
|
+
<Badge tone="accent" title={words.board.agentAtWorkTitle}>
|
|
48
|
+
{words.board.agentAtWork}
|
|
49
|
+
</Badge>
|
|
50
|
+
)}
|
|
51
|
+
{task.dueOn !== null && (
|
|
52
|
+
<DueBadge dueOn={task.dueOn} state={task.state} today={props.today} />
|
|
53
|
+
)}
|
|
54
|
+
{task.assignee !== null && <Avatar person={task.assignee} size="sm" />}
|
|
55
|
+
</span>
|
|
56
|
+
</li>
|
|
57
|
+
);
|
|
58
|
+
})}
|
|
59
|
+
</ul>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
RestAnswerInput,
|
|
3
3
|
RestArtifact,
|
|
4
|
+
RestCategory,
|
|
4
5
|
RestDecision,
|
|
5
6
|
RestDecisionInput,
|
|
6
7
|
RestDecisionPatch,
|
|
7
8
|
RestEvent,
|
|
9
|
+
RestGoal,
|
|
8
10
|
RestPerson,
|
|
9
11
|
RestRun,
|
|
10
12
|
RestTask,
|
|
11
13
|
RestTaskInput,
|
|
12
14
|
TaskState,
|
|
13
15
|
} from "../api.js";
|
|
14
|
-
import {
|
|
16
|
+
import { formatDay } from "../dates.js";
|
|
17
|
+
import { useLanguage, useWords } from "../i18n/index.js";
|
|
15
18
|
import type { TaskFormKind } from "../router.js";
|
|
16
19
|
import { TASK_STATES } from "../vocabulary.js";
|
|
17
20
|
import { DecisionForm } from "./decision-form.js";
|
|
@@ -20,12 +23,13 @@ import { EventFeed } from "./event-feed.js";
|
|
|
20
23
|
import { Markdown } from "./markdown.js";
|
|
21
24
|
import { RunList } from "./runs.js";
|
|
22
25
|
import { TaskForm } from "./task-form.js";
|
|
23
|
-
import { PersonChip, PriorityBadge, StateBadge, Time } from "./ui.js";
|
|
26
|
+
import { CategoryBadge, DueBadge, PersonChip, PriorityBadge, StateBadge, Time } from "./ui.js";
|
|
24
27
|
|
|
25
28
|
// One task: what it is, who it is on, what it links to, the agents at work on it, the decisions
|
|
26
29
|
// about it, the tasks that are part of it, everything that happened to it, and the ways to
|
|
27
30
|
// change it. Takes its data as props and nothing from the network; which of its forms is open
|
|
28
|
-
// is told by the page, since each form has an address of its own (ADR-0010).
|
|
31
|
+
// is told by the page, since each form has an address of its own (ADR-0010). The facts beside
|
|
32
|
+
// the text say only what is set: a plan, a category or a goal the task has none of is not a row.
|
|
29
33
|
|
|
30
34
|
export interface TaskViewProps {
|
|
31
35
|
readonly task: RestTask;
|
|
@@ -37,13 +41,21 @@ export interface TaskViewProps {
|
|
|
37
41
|
readonly runs?: readonly RestRun[] | undefined;
|
|
38
42
|
/** Everything that happened to the task, oldest first, when the page has read it. */
|
|
39
43
|
readonly history?: readonly RestEvent[] | undefined;
|
|
44
|
+
/** The categories of the workspace, for the form. */
|
|
45
|
+
readonly categories?: readonly RestCategory[] | undefined;
|
|
46
|
+
/** The goals of the project, for the form and for naming the one the task works towards. */
|
|
47
|
+
readonly goals?: readonly RestGoal[] | undefined;
|
|
40
48
|
readonly taskHref: (task: RestTask) => string;
|
|
41
49
|
/** Where a decision is answered, for a run that waits for one. */
|
|
42
50
|
readonly decisionHref?: ((decisionId: string) => string) | undefined;
|
|
51
|
+
/** Where a goal of the roadmap is read. */
|
|
52
|
+
readonly goalHref?: ((goalId: string) => string) | undefined;
|
|
43
53
|
/** Where a file a run handed in is read. */
|
|
44
54
|
readonly fileHref?: ((artifact: RestArtifact) => string) | undefined;
|
|
45
55
|
/** Where a document of the project is read, by its path, for the links in what was written. */
|
|
46
56
|
readonly docHref?: ((path: string) => string) | undefined;
|
|
57
|
+
/** The day to count due days from; the browser's today when left out. */
|
|
58
|
+
readonly today?: string | undefined;
|
|
47
59
|
/** The form open on the page, if one: the task edited, or a decision raised about it. */
|
|
48
60
|
readonly form?: TaskFormKind | undefined;
|
|
49
61
|
/** Where each form is opened; left out, the forms cannot be opened from here. */
|
|
@@ -65,6 +77,7 @@ export interface TaskViewProps {
|
|
|
65
77
|
|
|
66
78
|
export function TaskView(props: TaskViewProps) {
|
|
67
79
|
const words = useWords();
|
|
80
|
+
const { tag } = useLanguage();
|
|
68
81
|
const { task } = props;
|
|
69
82
|
const parent =
|
|
70
83
|
task.parentId === null
|
|
@@ -74,6 +87,11 @@ export function TaskView(props: TaskViewProps) {
|
|
|
74
87
|
const about = props.decisions.filter((decision) => decision.taskId === task.id);
|
|
75
88
|
const runs = (props.runs ?? []).filter((run) => run.taskId === task.id);
|
|
76
89
|
const tasksById = new Map(props.tasks.map((candidate) => [candidate.id, candidate]));
|
|
90
|
+
const goal =
|
|
91
|
+
task.goalId === null
|
|
92
|
+
? undefined
|
|
93
|
+
: props.goals?.find((candidate) => candidate.id === task.goalId);
|
|
94
|
+
const day = (value: string) => formatDay(value, tag);
|
|
77
95
|
|
|
78
96
|
if (props.form === "edit") {
|
|
79
97
|
return (
|
|
@@ -85,6 +103,8 @@ export function TaskView(props: TaskViewProps) {
|
|
|
85
103
|
people={props.people}
|
|
86
104
|
task={task}
|
|
87
105
|
parents={props.tasks}
|
|
106
|
+
categories={props.categories}
|
|
107
|
+
goals={props.goals}
|
|
88
108
|
busy={props.busy}
|
|
89
109
|
error={props.error}
|
|
90
110
|
onSubmit={(input) => props.onChange(task, input)}
|
|
@@ -110,7 +130,11 @@ export function TaskView(props: TaskViewProps) {
|
|
|
110
130
|
</h1>
|
|
111
131
|
<div className="sc-task-badges">
|
|
112
132
|
<StateBadge state={task.state} />
|
|
133
|
+
{task.category !== null && <CategoryBadge category={task.category} />}
|
|
113
134
|
<PriorityBadge priority={task.priority} />
|
|
135
|
+
{task.dueOn !== null && (
|
|
136
|
+
<DueBadge dueOn={task.dueOn} state={task.state} today={props.today} />
|
|
137
|
+
)}
|
|
114
138
|
</div>
|
|
115
139
|
</header>
|
|
116
140
|
<div className="sc-task-body">
|
|
@@ -226,6 +250,26 @@ export function TaskView(props: TaskViewProps) {
|
|
|
226
250
|
<dt>{words.task.form.priority}</dt>
|
|
227
251
|
<dd>{words.vocabulary.priority[task.priority]}</dd>
|
|
228
252
|
</div>
|
|
253
|
+
{task.category !== null && (
|
|
254
|
+
<div>
|
|
255
|
+
<dt>{words.task.category}</dt>
|
|
256
|
+
<dd>
|
|
257
|
+
<CategoryBadge category={task.category} />
|
|
258
|
+
</dd>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
{task.goalId !== null && (
|
|
262
|
+
<div>
|
|
263
|
+
<dt>{words.task.goal}</dt>
|
|
264
|
+
<dd>
|
|
265
|
+
{props.goalHref === undefined ? (
|
|
266
|
+
(goal?.title ?? task.goalId)
|
|
267
|
+
) : (
|
|
268
|
+
<a href={props.goalHref(task.goalId)}>{goal?.title ?? task.goalId}</a>
|
|
269
|
+
)}
|
|
270
|
+
</dd>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
229
273
|
<div>
|
|
230
274
|
<dt>{words.task.owner}</dt>
|
|
231
275
|
<dd>
|
|
@@ -242,6 +286,33 @@ export function TaskView(props: TaskViewProps) {
|
|
|
242
286
|
)}
|
|
243
287
|
</dd>
|
|
244
288
|
</div>
|
|
289
|
+
{(task.startsOn !== null || task.dueOn !== null) && (
|
|
290
|
+
<div>
|
|
291
|
+
<dt>{words.task.plan}</dt>
|
|
292
|
+
<dd>
|
|
293
|
+
{words.task.planned(
|
|
294
|
+
task.startsOn === null ? undefined : day(task.startsOn),
|
|
295
|
+
task.dueOn === null ? undefined : day(task.dueOn),
|
|
296
|
+
)}
|
|
297
|
+
</dd>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
{task.startedAt !== null && (
|
|
301
|
+
<div>
|
|
302
|
+
<dt>{words.task.started}</dt>
|
|
303
|
+
<dd>
|
|
304
|
+
<Time iso={task.startedAt} />
|
|
305
|
+
</dd>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
{task.doneAt !== null && (
|
|
309
|
+
<div>
|
|
310
|
+
<dt>{words.task.done}</dt>
|
|
311
|
+
<dd>
|
|
312
|
+
<Time iso={task.doneAt} />
|
|
313
|
+
</dd>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
245
316
|
<div>
|
|
246
317
|
<dt>{words.task.written}</dt>
|
|
247
318
|
<dd>
|