@skillcdn/console 0.4.4 → 0.4.5
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 +3 -3
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +162 -15
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +16 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +68 -19
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +26 -5
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +28 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +46 -15
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +0 -3
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +3 -15
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +30 -10
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/my-decisions.d.ts +20 -3
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +14 -1
- package/dist/components/my-decisions.js.map +1 -1
- package/dist/components/people.d.ts +43 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +117 -9
- package/dist/components/people.js.map +1 -1
- package/dist/components/picker.d.ts +8 -0
- package/dist/components/picker.d.ts.map +1 -1
- package/dist/components/picker.js +2 -2
- package/dist/components/picker.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +0 -1
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +22 -1
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts +5 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +143 -30
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -1
- package/dist/components/projects.js.map +1 -1
- package/dist/components/searches.d.ts +11 -0
- package/dist/components/searches.d.ts.map +1 -0
- package/dist/components/searches.js +30 -0
- package/dist/components/searches.js.map +1 -0
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +6 -5
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/ui.d.ts +16 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +10 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/console.css +362 -67
- package/dist/console.d.ts +2 -1
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +90 -14
- 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 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +89 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +87 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +87 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/args.d.ts +6 -1
- package/dist/plugin/args.d.ts.map +1 -1
- package/dist/plugin/args.js +16 -11
- package/dist/plugin/args.js.map +1 -1
- package/dist/plugin/bridge.d.ts +20 -1
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +28 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +1 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +10 -0
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +158 -13
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +13 -1
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +17 -1
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +4 -0
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +4 -0
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +15 -0
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +61 -4
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +44 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +55 -3
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/testing.d.ts +15 -1
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +40 -4
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +6 -0
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +10 -1
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/router.d.ts +19 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +18 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +11 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +14 -1
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +367 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +62 -2
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +1 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +6 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +7 -4
- package/docs/specs/plugins.md +47 -39
- package/docs/specs/rest.md +22 -5
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +8 -6
- package/skills/working-the-board/SKILL.md +1 -1
- package/src/api.ts +17 -0
- package/src/cli/cli.ts +200 -12
- package/src/cli/format.ts +82 -19
- package/src/cli/help.ts +30 -5
- package/src/client.ts +70 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +83 -42
- package/src/components/decision-form.tsx +13 -32
- package/src/components/event-feed.tsx +36 -9
- package/src/components/my-decisions.tsx +50 -5
- package/src/components/people.tsx +394 -37
- package/src/components/picker.tsx +11 -2
- package/src/components/plugin-frame.tsx +23 -0
- package/src/components/plugin-settings.tsx +346 -137
- package/src/components/projects.tsx +14 -14
- package/src/components/searches.ts +47 -0
- package/src/components/task-form.tsx +23 -28
- package/src/components/ui.tsx +36 -0
- package/src/console.tsx +229 -40
- package/src/data.ts +53 -1
- package/src/i18n/en.ts +98 -2
- package/src/i18n/ko.ts +95 -3
- package/src/index.ts +11 -0
- package/src/limits.ts +6 -0
- package/src/plugin/args.ts +16 -11
- package/src/plugin/bridge.ts +45 -0
- package/src/plugin/capabilities.ts +11 -0
- package/src/plugin/context.ts +170 -10
- package/src/plugin/define.ts +34 -0
- package/src/plugin/limits.ts +4 -0
- package/src/plugin/manifest.ts +74 -3
- package/src/plugin/rest-schemas.ts +62 -5
- package/src/plugin/testing.ts +61 -5
- package/src/plugin/web.ts +17 -0
- package/src/plugin.ts +1 -0
- package/src/router.ts +25 -0
- package/src/routes.ts +17 -2
- package/src/schemas.ts +81 -2
- package/src/styles/console.css +362 -67
- package/src/vocabulary.ts +6 -0
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
type RestProjectPatch,
|
|
24
24
|
} from "../api.js";
|
|
25
25
|
import { useWords } from "../i18n/index.js";
|
|
26
|
+
import { Picker } from "./picker.js";
|
|
27
|
+
import { peopleSearch } from "./searches.js";
|
|
26
28
|
import { Badge, Button, cx, PersonChip, personName, Time } from "./ui.js";
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -462,21 +464,19 @@ export function MemberList(props: MemberListProps) {
|
|
|
462
464
|
}}
|
|
463
465
|
>
|
|
464
466
|
<div className="sc-field-row">
|
|
465
|
-
<
|
|
467
|
+
<div className="sc-field">
|
|
466
468
|
<span className="sc-field-label">{members.person}</span>
|
|
467
|
-
<
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
{
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
</select>
|
|
479
|
-
</label>
|
|
469
|
+
<Picker
|
|
470
|
+
value={candidates.find((person) => person.id === personId)}
|
|
471
|
+
search={peopleSearch(candidates)}
|
|
472
|
+
keyOf={(person) => person.id}
|
|
473
|
+
render={(person) => <PersonChip person={person} size="sm" />}
|
|
474
|
+
label={members.person}
|
|
475
|
+
placeholder={members.chooseSomeone}
|
|
476
|
+
disabled={props.busy === true}
|
|
477
|
+
onChange={(person) => setPersonId(person?.id ?? "")}
|
|
478
|
+
/>
|
|
479
|
+
</div>
|
|
480
480
|
<label className="sc-field">
|
|
481
481
|
<span className="sc-field-label">{members.as}</span>
|
|
482
482
|
<select
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RestGoal, RestPerson, RestProject } from "../api.js";
|
|
2
|
+
import type { PickerSearch } from "./picker.js";
|
|
3
|
+
import { personName } from "./ui.js";
|
|
4
|
+
|
|
5
|
+
// What a picker asks for, answered from a listing the page already holds (ADR-0037): the people,
|
|
6
|
+
// the projects or the goals the page was given, found by the words typed, a page at a time, so
|
|
7
|
+
// that choosing one among many is the same wherever one is chosen, and the board's filters, a
|
|
8
|
+
// setting, a manager or a worker never offer a list too long to read.
|
|
9
|
+
|
|
10
|
+
/** A page of `items` found by the words typed, as a picker searches; everything while the words are empty. */
|
|
11
|
+
export function listSearch<T>(
|
|
12
|
+
items: readonly T[],
|
|
13
|
+
matches: (item: T, words: string) => boolean,
|
|
14
|
+
): PickerSearch<T> {
|
|
15
|
+
return async (query, page) => {
|
|
16
|
+
const words = query.trim().toLowerCase();
|
|
17
|
+
const found = words.length === 0 ? items : items.filter((item) => matches(item, words));
|
|
18
|
+
return {
|
|
19
|
+
items: found.slice(page.offset, page.offset + page.limit),
|
|
20
|
+
more: found.length > page.offset + page.limit,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The people given, found by name or login. */
|
|
26
|
+
export function peopleSearch(people: readonly RestPerson[]): PickerSearch<RestPerson> {
|
|
27
|
+
return listSearch(
|
|
28
|
+
people,
|
|
29
|
+
(person, words) =>
|
|
30
|
+
personName(person).toLowerCase().includes(words) ||
|
|
31
|
+
person.login.toLowerCase().includes(words),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The projects given, found by name or key. */
|
|
36
|
+
export function projectsSearch(projects: readonly RestProject[]): PickerSearch<RestProject> {
|
|
37
|
+
return listSearch(
|
|
38
|
+
projects,
|
|
39
|
+
(project, words) =>
|
|
40
|
+
project.name.toLowerCase().includes(words) || project.key.toLowerCase().includes(words),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The goals given, found by title. */
|
|
45
|
+
export function goalsSearch(goals: readonly RestGoal[]): PickerSearch<RestGoal> {
|
|
46
|
+
return listSearch(goals, (goal, words) => goal.title.toLowerCase().includes(words));
|
|
47
|
+
}
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
} from "../api.js";
|
|
17
17
|
import { useWords } from "../i18n/index.js";
|
|
18
18
|
import { Picker, type PickerSearch } from "./picker.js";
|
|
19
|
-
import {
|
|
19
|
+
import { peopleSearch } from "./searches.js";
|
|
20
|
+
import { Button, Chip, GoalStateBadge, PersonChip, personName, StateBadge } from "./ui.js";
|
|
20
21
|
|
|
21
22
|
// Writing a task, or changing one. The bounds are the package's, said before a request is made.
|
|
22
23
|
// The plan is two days (ADR-0019); what happened to the task is the board's to keep, not a
|
|
@@ -222,37 +223,31 @@ export function TaskForm(props: TaskFormProps) {
|
|
|
222
223
|
<legend className="sc-field-label">{form.workers}</legend>
|
|
223
224
|
<div className="sc-chips">
|
|
224
225
|
{chosenWorkers.map((person) => (
|
|
225
|
-
<
|
|
226
|
+
<Chip
|
|
227
|
+
key={person.id}
|
|
228
|
+
onRemove={() => setWorkerIds(workerIds.filter((id) => id !== person.id))}
|
|
229
|
+
removeLabel={`${words.common.remove}: ${personName(person)}`}
|
|
230
|
+
>
|
|
226
231
|
<PersonChip person={person} />
|
|
227
|
-
|
|
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">×</span>
|
|
234
|
-
</button>
|
|
235
|
-
</span>
|
|
232
|
+
</Chip>
|
|
236
233
|
))}
|
|
237
|
-
{
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
234
|
+
{otherPeople.length > 0 && workerIds.length < MAX_WORKERS ? (
|
|
235
|
+
<Picker
|
|
236
|
+
className="sc-picker-add"
|
|
237
|
+
value={undefined}
|
|
238
|
+
search={peopleSearch(otherPeople)}
|
|
239
|
+
keyOf={(person) => person.id}
|
|
240
|
+
render={(person) => <PersonChip person={person} size="sm" />}
|
|
241
|
+
label={form.addWorker}
|
|
242
|
+
placeholder={form.addWorker}
|
|
243
|
+
onChange={(person) => {
|
|
244
|
+
if (person !== undefined) {
|
|
245
|
+
setWorkerIds([...workerIds, person.id]);
|
|
246
246
|
}
|
|
247
247
|
}}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<option key={person.id} value={person.id}>
|
|
252
|
-
{personName(person)}
|
|
253
|
-
</option>
|
|
254
|
-
))}
|
|
255
|
-
</select>
|
|
248
|
+
/>
|
|
249
|
+
) : (
|
|
250
|
+
chosenWorkers.length === 0 && <span className="sc-muted">{words.common.nobody}</span>
|
|
256
251
|
)}
|
|
257
252
|
</div>
|
|
258
253
|
</fieldset>
|
package/src/components/ui.tsx
CHANGED
|
@@ -164,6 +164,42 @@ export function PersonChip(props: { readonly person: RestPerson; readonly size?:
|
|
|
164
164
|
);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* A thing named among others in a row (`sc-chips`): a person (a `PersonChip` inside), a team, an
|
|
169
|
+
* item of a list, a holder of a role. Given `onRemove`, it carries the way to take it out, named
|
|
170
|
+
* for a screen reader by `removeLabel`; left out, the chip is read, not changed. Every row of
|
|
171
|
+
* things a person adds to and takes from is made of these, so that they read alike everywhere.
|
|
172
|
+
*/
|
|
173
|
+
export function Chip(props: {
|
|
174
|
+
readonly children: ReactNode;
|
|
175
|
+
readonly onRemove?: (() => void) | undefined;
|
|
176
|
+
/** What the way out says to a screen reader: the word for removing, and what the chip names. */
|
|
177
|
+
readonly removeLabel?: string | undefined;
|
|
178
|
+
readonly disabled?: boolean | undefined;
|
|
179
|
+
readonly title?: string | undefined;
|
|
180
|
+
/** A class of the chip's own beside `sc-chip`. */
|
|
181
|
+
readonly className?: string | undefined;
|
|
182
|
+
}) {
|
|
183
|
+
const words = useWords().common;
|
|
184
|
+
return (
|
|
185
|
+
<span className={cx("sc-chip", props.className)} title={props.title}>
|
|
186
|
+
<span className="sc-chip-label">{props.children}</span>
|
|
187
|
+
{props.onRemove !== undefined && (
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
className="sc-chip-remove"
|
|
191
|
+
aria-label={props.removeLabel ?? words.remove}
|
|
192
|
+
title={props.removeLabel ?? words.remove}
|
|
193
|
+
disabled={props.disabled}
|
|
194
|
+
onClick={props.onRemove}
|
|
195
|
+
>
|
|
196
|
+
<span aria-hidden="true">×</span>
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
</span>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
167
203
|
/** How many of a task's workers are shown as pictures before the rest are a count. */
|
|
168
204
|
const WORKERS_SHOWN = 3;
|
|
169
205
|
|
package/src/console.tsx
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type RestProjectInput,
|
|
30
30
|
type RestProjectPatch,
|
|
31
31
|
type RestProjectRules,
|
|
32
|
+
type RestReachedDecision,
|
|
32
33
|
type RestRules,
|
|
33
34
|
type RestTask,
|
|
34
35
|
type RestTaskInput,
|
|
@@ -49,10 +50,17 @@ import { DecisionCard, DecisionList, type DecisionListProps } from "./components
|
|
|
49
50
|
import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
|
|
50
51
|
import { GoalForm, GoalList, type GoalListProps, GoalView, goalMonth } from "./components/goals.js";
|
|
51
52
|
import { HomeGrid, type HomeGridProps } from "./components/home.js";
|
|
52
|
-
import { MyDecisions } from "./components/my-decisions.js";
|
|
53
|
+
import { MyDecisions, ReachedDecision } from "./components/my-decisions.js";
|
|
53
54
|
import { PackTokenWays } from "./components/pack-token.js";
|
|
54
55
|
import { PAGE_SIZE } from "./components/paged.js";
|
|
55
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
ListingForm,
|
|
58
|
+
PeopleList,
|
|
59
|
+
type PeopleListProps,
|
|
60
|
+
TeamForm,
|
|
61
|
+
TeamList,
|
|
62
|
+
type TeamListProps,
|
|
63
|
+
} from "./components/people.js";
|
|
56
64
|
import { PluginFrame } from "./components/plugin-frame.js";
|
|
57
65
|
import { PluginSettingsList, PluginSettingsView } from "./components/plugin-settings.js";
|
|
58
66
|
import {
|
|
@@ -81,6 +89,7 @@ import {
|
|
|
81
89
|
Button,
|
|
82
90
|
Callout,
|
|
83
91
|
CategoryBadge,
|
|
92
|
+
cx,
|
|
84
93
|
EmptyState,
|
|
85
94
|
LinkButton,
|
|
86
95
|
personName,
|
|
@@ -119,6 +128,7 @@ import {
|
|
|
119
128
|
type GoalTaskView,
|
|
120
129
|
goalHref,
|
|
121
130
|
matchRoute,
|
|
131
|
+
myDecisionHref,
|
|
122
132
|
type Navigation,
|
|
123
133
|
newDecisionHref,
|
|
124
134
|
newGoalHref,
|
|
@@ -157,6 +167,7 @@ export interface ConsoleComponents {
|
|
|
157
167
|
readonly SignIn: ComponentType<SignInProps>;
|
|
158
168
|
readonly TokenList: ComponentType<TokenListProps>;
|
|
159
169
|
readonly PeopleList: ComponentType<PeopleListProps>;
|
|
170
|
+
readonly TeamList: ComponentType<TeamListProps>;
|
|
160
171
|
readonly SkillList: ComponentType<SkillListProps>;
|
|
161
172
|
readonly HomeGrid: ComponentType<HomeGridProps>;
|
|
162
173
|
readonly MemberList: ComponentType<MemberListProps>;
|
|
@@ -175,6 +186,7 @@ export const DEFAULT_COMPONENTS: ConsoleComponents = {
|
|
|
175
186
|
SignIn,
|
|
176
187
|
TokenList,
|
|
177
188
|
PeopleList,
|
|
189
|
+
TeamList,
|
|
178
190
|
SkillList,
|
|
179
191
|
HomeGrid,
|
|
180
192
|
MemberList,
|
|
@@ -644,6 +656,8 @@ function PluginSettingsPage(props: {
|
|
|
644
656
|
detail={detail}
|
|
645
657
|
projects={data.projects}
|
|
646
658
|
people={data.people}
|
|
659
|
+
plugins={data.plugins}
|
|
660
|
+
teams={data.teams}
|
|
647
661
|
language={language}
|
|
648
662
|
client={client}
|
|
649
663
|
onChanged={async () => {
|
|
@@ -2242,6 +2256,70 @@ function PlatePage(props: {
|
|
|
2242
2256
|
);
|
|
2243
2257
|
}
|
|
2244
2258
|
|
|
2259
|
+
/**
|
|
2260
|
+
* One decision wherever it is (ADR-0068): in a project the person may see, or one that reaches
|
|
2261
|
+
* them from elsewhere, addressed to them, raised by them, or a plugin's addressed to someone
|
|
2262
|
+
* they manage; answered here where they may. The address a plugin's page sends a person to,
|
|
2263
|
+
* who may not be in the project the decision waits in.
|
|
2264
|
+
*/
|
|
2265
|
+
function MyDecisionPage(props: { readonly id: string; readonly data: ConsoleData }) {
|
|
2266
|
+
const words = useWords();
|
|
2267
|
+
const { id, data } = props;
|
|
2268
|
+
const { reachDecision, answerReachedDecision } = data.actions;
|
|
2269
|
+
const [reached, setReached] = useState<RestReachedDecision | undefined>(undefined);
|
|
2270
|
+
const [problem, setProblem] = useState<string | undefined>(undefined);
|
|
2271
|
+
const { busy, error, act } = useAction();
|
|
2272
|
+
const couldNotLoad = words.myDecisions.couldNotLoad;
|
|
2273
|
+
useEffect(() => {
|
|
2274
|
+
const controller = new AbortController();
|
|
2275
|
+
setReached(undefined);
|
|
2276
|
+
setProblem(undefined);
|
|
2277
|
+
reachDecision(id, controller.signal).then(
|
|
2278
|
+
(found) => {
|
|
2279
|
+
if (!controller.signal.aborted) {
|
|
2280
|
+
setReached(found);
|
|
2281
|
+
}
|
|
2282
|
+
},
|
|
2283
|
+
(failure: unknown) => {
|
|
2284
|
+
if (!controller.signal.aborted) {
|
|
2285
|
+
setProblem(errorWords(failure, couldNotLoad, words));
|
|
2286
|
+
}
|
|
2287
|
+
},
|
|
2288
|
+
);
|
|
2289
|
+
return () => controller.abort();
|
|
2290
|
+
}, [reachDecision, id, couldNotLoad, words]);
|
|
2291
|
+
const shown = problem ?? error;
|
|
2292
|
+
return (
|
|
2293
|
+
<>
|
|
2294
|
+
<div className="sc-page-head">
|
|
2295
|
+
<h1 className="sc-page-title">{words.decisions.one}</h1>
|
|
2296
|
+
<LinkButton href={PATHS.decisions}>{words.myDecisions.title}</LinkButton>
|
|
2297
|
+
</div>
|
|
2298
|
+
{shown !== undefined && <Callout tone="danger">{shown}</Callout>}
|
|
2299
|
+
{reached === undefined ? (
|
|
2300
|
+
shown === undefined && (
|
|
2301
|
+
<div className="sc-loading">
|
|
2302
|
+
<Spinner label={words.myDecisions.loading} />
|
|
2303
|
+
</div>
|
|
2304
|
+
)
|
|
2305
|
+
) : (
|
|
2306
|
+
<ReachedDecision
|
|
2307
|
+
reached={reached}
|
|
2308
|
+
onAnswer={(_decision, input) =>
|
|
2309
|
+
void act(async () => {
|
|
2310
|
+
const answered = await answerReachedDecision(id, input);
|
|
2311
|
+
setReached((current) =>
|
|
2312
|
+
current === undefined ? current : { ...current, decision: answered },
|
|
2313
|
+
);
|
|
2314
|
+
})
|
|
2315
|
+
}
|
|
2316
|
+
busy={busy}
|
|
2317
|
+
/>
|
|
2318
|
+
)}
|
|
2319
|
+
</>
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2245
2323
|
/** What is on the person's own plate, across their projects. */
|
|
2246
2324
|
/** How many of a person's decisions are read at a time. */
|
|
2247
2325
|
const MY_DECISIONS_PAGE = 50;
|
|
@@ -2307,14 +2385,17 @@ function useMyDecisions(
|
|
|
2307
2385
|
*/
|
|
2308
2386
|
function MyDecisionsPage(props: { readonly data: ConsoleData }) {
|
|
2309
2387
|
const words = useWords();
|
|
2310
|
-
const { myDecisions,
|
|
2388
|
+
const { myDecisions, answerReachedDecision } = props.data.actions;
|
|
2311
2389
|
const addressed = useMyDecisions(myDecisions, false, words.myDecisions.couldNotLoad);
|
|
2312
2390
|
const raised = useMyDecisions(myDecisions, true, words.myDecisions.couldNotLoad);
|
|
2313
2391
|
const { busy, error, act } = useAction();
|
|
2314
2392
|
const problem = addressed.problem ?? raised.problem ?? error;
|
|
2315
|
-
|
|
2393
|
+
// Answered at the decision's own address (ADR-0068), so that one that reaches the person from a
|
|
2394
|
+
// project they are not in, a plugin's up the reporting line or one they were named in before
|
|
2395
|
+
// they left it, is answered here as it is read here.
|
|
2396
|
+
const onAnswer = (_projectKey: string, decision: RestDecision, input: RestAnswerInput) =>
|
|
2316
2397
|
void act(async () => {
|
|
2317
|
-
await
|
|
2398
|
+
await answerReachedDecision(decision.id, input);
|
|
2318
2399
|
addressed.reload();
|
|
2319
2400
|
raised.reload();
|
|
2320
2401
|
});
|
|
@@ -2336,8 +2417,11 @@ function MyDecisionsPage(props: { readonly data: ConsoleData }) {
|
|
|
2336
2417
|
<MyDecisions
|
|
2337
2418
|
addressed={{ items: addressed.items, more: addressed.more, onMore: addressed.readMore }}
|
|
2338
2419
|
raised={{ items: raised.items, more: raised.more, onMore: raised.readMore }}
|
|
2339
|
-
decisionHref={
|
|
2340
|
-
|
|
2420
|
+
decisionHref={(projectKey, decisionId) =>
|
|
2421
|
+
props.data.projects.some((project) => project.key === projectKey)
|
|
2422
|
+
? decisionHref(projectKey, decisionId)
|
|
2423
|
+
: myDecisionHref(decisionId)
|
|
2424
|
+
}
|
|
2341
2425
|
onAnswer={onAnswer}
|
|
2342
2426
|
busy={busy || addressed.busy || raised.busy}
|
|
2343
2427
|
/>
|
|
@@ -2440,6 +2524,9 @@ function WorkspacePage(props: {
|
|
|
2440
2524
|
if (route.name === "my-decisions") {
|
|
2441
2525
|
return <MyDecisionsPage data={data} />;
|
|
2442
2526
|
}
|
|
2527
|
+
if (route.name === "my-decision") {
|
|
2528
|
+
return <MyDecisionPage id={route.id} data={data} />;
|
|
2529
|
+
}
|
|
2443
2530
|
if (route.name === "person-work") {
|
|
2444
2531
|
return <PersonWorkPage id={route.id} data={data} components={components} week={route.week} />;
|
|
2445
2532
|
}
|
|
@@ -2488,26 +2575,106 @@ function WorkspacePage(props: {
|
|
|
2488
2575
|
);
|
|
2489
2576
|
}
|
|
2490
2577
|
|
|
2491
|
-
if (
|
|
2578
|
+
if (
|
|
2579
|
+
route.name === "people" ||
|
|
2580
|
+
route.name === "new-listing" ||
|
|
2581
|
+
route.name === "teams" ||
|
|
2582
|
+
route.name === "new-team"
|
|
2583
|
+
) {
|
|
2492
2584
|
const me = data.me?.person ?? undefined;
|
|
2493
2585
|
const administrator = me?.role === "admin";
|
|
2586
|
+
// The people and the teams (ADR-0073) are two views of one page, each with its form.
|
|
2587
|
+
const onTeams = route.name === "teams" || route.name === "new-team";
|
|
2494
2588
|
const making = route.name === "new-listing" && administrator;
|
|
2589
|
+
const makingTeam = route.name === "new-team" && administrator;
|
|
2495
2590
|
// An owner sees the work of the people in their projects; the console refuses the rest.
|
|
2496
2591
|
const oversees = administrator || data.projects.some((project) => project.role === "owner");
|
|
2497
2592
|
// The list is what the policy shows (ADR-0035): everyone, or the people one shares a project with.
|
|
2498
2593
|
const narrowed = peopleVisibilityOf(data.me?.workspace) === "shared_projects" && !administrator;
|
|
2594
|
+
const views = words.people.views;
|
|
2595
|
+
const inCount = data.people.filter((person) => person.status === "active").length;
|
|
2499
2596
|
return (
|
|
2500
2597
|
<>
|
|
2501
2598
|
<div className="sc-page-head">
|
|
2502
2599
|
<h1 className="sc-page-title">{words.people.title}</h1>
|
|
2503
|
-
{administrator && !making && (
|
|
2600
|
+
{administrator && !onTeams && !making && (
|
|
2504
2601
|
<LinkButton href={PATHS.newListing} variant="primary">
|
|
2505
2602
|
{words.people.letIn}
|
|
2506
2603
|
</LinkButton>
|
|
2507
2604
|
)}
|
|
2605
|
+
{administrator && onTeams && !makingTeam && (
|
|
2606
|
+
<LinkButton href={PATHS.newTeam} variant="primary">
|
|
2607
|
+
{words.teams.newTeam}
|
|
2608
|
+
</LinkButton>
|
|
2609
|
+
)}
|
|
2508
2610
|
</div>
|
|
2611
|
+
<nav className="sc-subtabs sc-people-views" aria-label={views.aria}>
|
|
2612
|
+
<a
|
|
2613
|
+
className={cx("sc-tab", !onTeams && "sc-tab-current")}
|
|
2614
|
+
href={PATHS.people}
|
|
2615
|
+
aria-current={onTeams ? undefined : "page"}
|
|
2616
|
+
>
|
|
2617
|
+
{views.people}
|
|
2618
|
+
<span className="sc-tab-count">{inCount}</span>
|
|
2619
|
+
</a>
|
|
2620
|
+
<a
|
|
2621
|
+
className={cx("sc-tab", onTeams && "sc-tab-current")}
|
|
2622
|
+
href={PATHS.teams}
|
|
2623
|
+
aria-current={onTeams ? "page" : undefined}
|
|
2624
|
+
>
|
|
2625
|
+
{views.teams}
|
|
2626
|
+
<span className="sc-tab-count">{data.teams.length}</span>
|
|
2627
|
+
</a>
|
|
2628
|
+
</nav>
|
|
2509
2629
|
{narrowed && <p className="sc-lead">{words.people.narrowed}</p>}
|
|
2510
2630
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
2631
|
+
{makingTeam && (
|
|
2632
|
+
<section className="sc-panel sc-panel-form" aria-label={words.teams.newAria}>
|
|
2633
|
+
<TeamForm
|
|
2634
|
+
busy={busy}
|
|
2635
|
+
onSubmit={(input) =>
|
|
2636
|
+
void act(async () => {
|
|
2637
|
+
await data.actions.createTeam(input);
|
|
2638
|
+
navigation.replace(PATHS.teams);
|
|
2639
|
+
})
|
|
2640
|
+
}
|
|
2641
|
+
onCancel={() => navigation.back(PATHS.teams)}
|
|
2642
|
+
/>
|
|
2643
|
+
</section>
|
|
2644
|
+
)}
|
|
2645
|
+
{onTeams && (
|
|
2646
|
+
<components.TeamList
|
|
2647
|
+
teams={data.teams}
|
|
2648
|
+
people={data.people}
|
|
2649
|
+
busy={busy}
|
|
2650
|
+
onRename={
|
|
2651
|
+
administrator
|
|
2652
|
+
? (team, name) => void act(() => data.actions.renameTeam(team.key, { name }))
|
|
2653
|
+
: undefined
|
|
2654
|
+
}
|
|
2655
|
+
onRemove={
|
|
2656
|
+
administrator
|
|
2657
|
+
? (team) => {
|
|
2658
|
+
// The roles granted to the team go with it: asked first, as nothing brings them back.
|
|
2659
|
+
if (window.confirm(words.teams.removeAsk(team.name))) {
|
|
2660
|
+
void act(() => data.actions.removeTeam(team.key));
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
: undefined
|
|
2664
|
+
}
|
|
2665
|
+
onAddMember={
|
|
2666
|
+
administrator
|
|
2667
|
+
? (team, person) => void act(() => data.actions.addTeamMember(team.key, person.id))
|
|
2668
|
+
: undefined
|
|
2669
|
+
}
|
|
2670
|
+
onRemoveMember={
|
|
2671
|
+
administrator
|
|
2672
|
+
? (team, person) =>
|
|
2673
|
+
void act(() => data.actions.removeTeamMember(team.key, person.id))
|
|
2674
|
+
: undefined
|
|
2675
|
+
}
|
|
2676
|
+
/>
|
|
2677
|
+
)}
|
|
2511
2678
|
{making && (
|
|
2512
2679
|
<section className="sc-panel sc-panel-form" aria-label={words.people.letInAria}>
|
|
2513
2680
|
<ListingForm
|
|
@@ -2523,36 +2690,56 @@ function WorkspacePage(props: {
|
|
|
2523
2690
|
/>
|
|
2524
2691
|
</section>
|
|
2525
2692
|
)}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2693
|
+
{!onTeams && (
|
|
2694
|
+
<components.PeopleList
|
|
2695
|
+
people={data.people}
|
|
2696
|
+
listed={data.listed}
|
|
2697
|
+
organization={data.organization}
|
|
2698
|
+
me={me}
|
|
2699
|
+
busy={busy}
|
|
2700
|
+
onChangeRole={
|
|
2701
|
+
administrator
|
|
2702
|
+
? (person, role) => void act(() => data.actions.updatePerson(person.id, { role }))
|
|
2703
|
+
: undefined
|
|
2704
|
+
}
|
|
2705
|
+
onChangeStatus={
|
|
2706
|
+
administrator
|
|
2707
|
+
? (person, status) =>
|
|
2708
|
+
void act(() => data.actions.updatePerson(person.id, { status }))
|
|
2709
|
+
: undefined
|
|
2710
|
+
}
|
|
2711
|
+
onChangeManager={
|
|
2712
|
+
administrator
|
|
2713
|
+
? (person, managerId) =>
|
|
2714
|
+
void act(() => data.actions.updatePerson(person.id, { managerId }))
|
|
2715
|
+
: undefined
|
|
2716
|
+
}
|
|
2717
|
+
onChangeEmail={
|
|
2718
|
+
administrator
|
|
2719
|
+
? (person, email) => void act(() => data.actions.updatePerson(person.id, { email }))
|
|
2720
|
+
: undefined
|
|
2721
|
+
}
|
|
2722
|
+
onUnlist={
|
|
2723
|
+
administrator
|
|
2724
|
+
? (listing) => void act(() => data.actions.removeListing(listing.id))
|
|
2725
|
+
: undefined
|
|
2726
|
+
}
|
|
2727
|
+
teams={data.teams}
|
|
2728
|
+
onJoinTeam={
|
|
2729
|
+
administrator
|
|
2730
|
+
? (person, team) => void act(() => data.actions.addTeamMember(team.key, person.id))
|
|
2731
|
+
: undefined
|
|
2732
|
+
}
|
|
2733
|
+
onLeaveTeam={
|
|
2734
|
+
administrator
|
|
2735
|
+
? (person, team) =>
|
|
2736
|
+
void act(() => data.actions.removeTeamMember(team.key, person.id))
|
|
2737
|
+
: undefined
|
|
2738
|
+
}
|
|
2739
|
+
workHref={oversees ? (person) => personWorkHref(person.id) : undefined}
|
|
2740
|
+
agentsHref={administrator ? (person) => personAgentsHref(person.id) : undefined}
|
|
2741
|
+
/>
|
|
2742
|
+
)}
|
|
2556
2743
|
</>
|
|
2557
2744
|
);
|
|
2558
2745
|
}
|
|
@@ -2713,7 +2900,7 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
2713
2900
|
{
|
|
2714
2901
|
href: PATHS.decisions,
|
|
2715
2902
|
label: words.nav.myDecisions,
|
|
2716
|
-
current: route.name === "my-decisions",
|
|
2903
|
+
current: route.name === "my-decisions" || route.name === "my-decision",
|
|
2717
2904
|
count: data.me?.decisions?.waiting,
|
|
2718
2905
|
},
|
|
2719
2906
|
{
|
|
@@ -2722,6 +2909,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
|
|
|
2722
2909
|
current:
|
|
2723
2910
|
route.name === "people" ||
|
|
2724
2911
|
route.name === "new-listing" ||
|
|
2912
|
+
route.name === "teams" ||
|
|
2913
|
+
route.name === "new-team" ||
|
|
2725
2914
|
route.name === "person-agents",
|
|
2726
2915
|
},
|
|
2727
2916
|
...(viaToken
|