@skillcdn/console 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/api.d.ts +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 +120 -33
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +4 -2
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +38 -10
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +22 -15
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +10 -1
- package/dist/cli/plugin-check.d.ts.map +1 -1
- package/dist/cli/plugin-check.js +45 -4
- package/dist/cli/plugin-check.js.map +1 -1
- package/dist/cli/plugin-cli.d.ts +1 -1
- package/dist/cli/plugin-cli.d.ts.map +1 -1
- package/dist/cli/plugin-cli.js +80 -5
- package/dist/cli/plugin-cli.js.map +1 -1
- package/dist/cli/plugin-init.d.ts +19 -0
- package/dist/cli/plugin-init.d.ts.map +1 -1
- package/dist/cli/plugin-init.js +141 -3
- package/dist/cli/plugin-init.js.map +1 -1
- package/dist/client.d.ts +7 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +18 -5
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +3 -3
- package/dist/components/board.js.map +1 -1
- package/dist/components/comments.d.ts.map +1 -1
- package/dist/components/comments.js +1 -1
- package/dist/components/comments.js.map +1 -1
- package/dist/components/decision-form.d.ts +8 -1
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +57 -7
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +9 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +53 -7
- 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 +7 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +2 -2
- package/dist/components/goals.js.map +1 -1
- package/dist/components/home.d.ts +35 -0
- package/dist/components/home.d.ts.map +1 -0
- package/dist/components/home.js +326 -0
- package/dist/components/home.js.map +1 -0
- package/dist/components/my-decisions.d.ts +2 -0
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +2 -2
- package/dist/components/my-decisions.js.map +1 -1
- package/dist/components/pack-connect.d.ts.map +1 -1
- package/dist/components/pack-connect.js +5 -4
- package/dist/components/pack-connect.js.map +1 -1
- package/dist/components/people.d.ts +2 -0
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +15 -2
- package/dist/components/people.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +3 -12
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +53 -20
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +151 -26
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts +0 -8
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +1 -13
- package/dist/components/projects.js.map +1 -1
- package/dist/components/task-view.d.ts +2 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +2 -1
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +5 -2
- package/dist/components/ui.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +5 -6
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +764 -192
- package/dist/console.d.ts +10 -3
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +49 -28
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +6 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +31 -7
- package/dist/data.js.map +1 -1
- package/dist/docs-page.d.ts.map +1 -1
- package/dist/docs-page.js +6 -5
- package/dist/docs-page.js.map +1 -1
- package/dist/home-tiles.d.ts +92 -0
- package/dist/home-tiles.d.ts.map +1 -0
- package/dist/home-tiles.js +294 -0
- package/dist/home-tiles.js.map +1 -0
- package/dist/i18n/en.d.ts +180 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +181 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/index.d.ts +4 -2
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +10 -11
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +176 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +8 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +8 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/bridge.d.ts +44 -3
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +26 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +7 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +14 -1
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +24 -1
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +1 -0
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +1 -1
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +5 -2
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +5 -2
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +83 -6
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +111 -7
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +76 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +23 -2
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/sandbox.d.ts +4 -3
- package/dist/plugin/sandbox.d.ts.map +1 -1
- package/dist/plugin/sandbox.js +33 -6
- package/dist/plugin/sandbox.js.map +1 -1
- package/dist/plugin/semver.d.ts +26 -0
- package/dist/plugin/semver.d.ts.map +1 -0
- package/dist/plugin/semver.js +76 -0
- package/dist/plugin/semver.js.map +1 -0
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +2 -1
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +10 -1
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +34 -0
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +3 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -2
- package/dist/plugin.js.map +1 -1
- package/dist/rules.d.ts +2 -2
- package/dist/rules.js +3 -3
- package/dist/schemas.d.ts +574 -11
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +154 -25
- package/dist/schemas.js.map +1 -1
- package/dist/use-action.d.ts +8 -2
- package/dist/use-action.d.ts.map +1 -1
- package/dist/use-action.js +13 -6
- package/dist/use-action.js.map +1 -1
- package/dist/vocabulary.d.ts +3 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +3 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +4 -3
- package/docs/specs/plugins.md +49 -35
- package/docs/specs/rest.md +20 -18
- package/package.json +7 -7
- package/skills/building-a-plugin/SKILL.md +10 -9
- package/skills/planning-the-board/references/setting-up.md +2 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +10 -0
- package/src/cli/cli.ts +162 -39
- package/src/cli/format.ts +54 -10
- package/src/cli/help.ts +23 -14
- package/src/cli/plugin-check.ts +68 -2
- package/src/cli/plugin-cli.ts +104 -6
- package/src/cli/plugin-init.ts +178 -3
- package/src/client.ts +36 -9
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +3 -3
- package/src/components/comments.tsx +1 -0
- package/src/components/decision-form.tsx +166 -29
- package/src/components/decision-list.tsx +116 -15
- package/src/components/event-feed.tsx +7 -1
- package/src/components/goals.tsx +4 -1
- package/src/components/home.tsx +615 -0
- package/src/components/my-decisions.tsx +4 -1
- package/src/components/pack-connect.tsx +5 -4
- package/src/components/people.tsx +136 -54
- package/src/components/plugin-frame.tsx +62 -63
- package/src/components/plugin-settings.tsx +428 -87
- package/src/components/projects.tsx +0 -55
- package/src/components/task-view.tsx +9 -1
- package/src/components/ui.tsx +6 -2
- package/src/connect-page.tsx +5 -6
- package/src/console.tsx +98 -42
- package/src/data.ts +41 -11
- package/src/docs-page.tsx +6 -5
- package/src/home-tiles.ts +438 -0
- package/src/i18n/en.ts +185 -0
- package/src/i18n/index.ts +17 -14
- package/src/i18n/ko.ts +181 -3
- package/src/index.ts +7 -5
- package/src/limits.ts +8 -0
- package/src/plugin/bridge.ts +77 -1
- package/src/plugin/capabilities.ts +17 -1
- package/src/plugin/context.ts +24 -2
- package/src/plugin/define.ts +4 -1
- package/src/plugin/limits.ts +5 -2
- package/src/plugin/manifest.ts +154 -10
- package/src/plugin/rest-schemas.ts +27 -1
- package/src/plugin/sandbox.ts +36 -8
- package/src/plugin/semver.ts +89 -0
- package/src/plugin/testing.ts +2 -1
- package/src/plugin/web.ts +49 -0
- package/src/plugin.ts +18 -0
- package/src/rules.ts +3 -3
- package/src/schemas.ts +190 -28
- package/src/styles/console.css +764 -192
- package/src/use-action.ts +16 -7
- package/src/vocabulary.ts +4 -0
- package/template/plugin/console-plugin.json +3 -1
- package/template/plugin/package.json +3 -3
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { type FormEvent, useMemo, useState } from "react";
|
|
1
|
+
import { type ClipboardEvent, type FormEvent, useMemo, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
MAX_BODY_LENGTH,
|
|
4
|
+
MAX_DECIDERS,
|
|
5
|
+
MAX_FILES_PER_DECISION,
|
|
4
6
|
MAX_OPTION_LABEL_LENGTH,
|
|
5
7
|
MAX_OPTIONS,
|
|
6
8
|
MAX_QUESTION_LENGTH,
|
|
@@ -10,14 +12,17 @@ import {
|
|
|
10
12
|
type RestTask,
|
|
11
13
|
} from "../api.js";
|
|
12
14
|
import { useWords } from "../i18n/index.js";
|
|
15
|
+
import { isPicture, placing, unusedName } from "./files.js";
|
|
13
16
|
import { Picker, type PickerSearch } from "./picker.js";
|
|
14
17
|
import { type TaskSearch, taskLabel } from "./task-form.js";
|
|
15
|
-
import { Button, PersonChip, personName } from "./ui.js";
|
|
18
|
+
import { Button, formatBytes, PersonChip, personName } from "./ui.js";
|
|
16
19
|
|
|
17
20
|
// Raising a decision: the question, what a person needs to know, the options to choose from,
|
|
18
21
|
// and who is to answer (ADR-0042): the person raising it, unless they name another of the
|
|
19
22
|
// project. The task it is about is the page's, when raised from a task; otherwise it is found
|
|
20
|
-
// by searching the board a page at a time (ADR-0037), never picked from the board whole.
|
|
23
|
+
// by searching the board a page at a time (ADR-0037), never picked from the board whole. The
|
|
24
|
+
// files put on the decision (ADR-0055) are chosen or pasted here, a pasted picture placed in
|
|
25
|
+
// the context where pictures are read, as a comment's are.
|
|
21
26
|
|
|
22
27
|
export interface DecisionFormProps {
|
|
23
28
|
/** The task it is about from the start, when raised from a task's page; the field is not shown then. */
|
|
@@ -30,10 +35,28 @@ export interface DecisionFormProps {
|
|
|
30
35
|
readonly me?: RestPerson | undefined;
|
|
31
36
|
readonly busy?: boolean | undefined;
|
|
32
37
|
readonly error?: string | undefined;
|
|
33
|
-
|
|
38
|
+
/** Called with the decision as the API takes it, and the files put on it (ADR-0055), none when none were chosen. */
|
|
39
|
+
readonly onSubmit: (input: RestDecisionInput, files: readonly File[]) => void;
|
|
34
40
|
readonly onCancel?: (() => void) | undefined;
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
/**
|
|
44
|
+
* What the form sends of whom the decision is for: nothing for the raiser alone, `deciderId` for
|
|
45
|
+
* one other person, `deciderIds` for several (ADR-0057), the first among them repeated as
|
|
46
|
+
* `deciderId` for a console of before.
|
|
47
|
+
*/
|
|
48
|
+
export function decidersInput(
|
|
49
|
+
deciders: readonly RestPerson[],
|
|
50
|
+
me: RestPerson | undefined,
|
|
51
|
+
): Pick<RestDecisionInput, "deciderId" | "deciderIds"> {
|
|
52
|
+
const ids = deciders.map((person) => person.id);
|
|
53
|
+
if (ids.length === 0 || (ids.length === 1 && ids[0] === me?.id)) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
const first = ids[0] ?? "";
|
|
57
|
+
return ids.length === 1 ? { deciderId: first } : { deciderId: first, deciderIds: ids };
|
|
58
|
+
}
|
|
59
|
+
|
|
37
60
|
/** The people of the project found by the words typed, a page at a time, as a picker searches. */
|
|
38
61
|
export function peopleSearch(people: readonly RestPerson[]): PickerSearch<RestPerson> {
|
|
39
62
|
return async (query, page) => {
|
|
@@ -58,21 +81,73 @@ export function DecisionForm(props: DecisionFormProps) {
|
|
|
58
81
|
const [body, setBody] = useState("");
|
|
59
82
|
const [about, setAbout] = useState<RestTask | undefined>(undefined);
|
|
60
83
|
const [options, setOptions] = useState<readonly string[]>(["", ""]);
|
|
61
|
-
|
|
62
|
-
const
|
|
84
|
+
// Whom it is for (ADR-0042, ADR-0057): the raiser, until others are named in their place.
|
|
85
|
+
const [deciders, setDeciders] = useState<readonly RestPerson[]>(
|
|
86
|
+
props.me === undefined ? [] : [props.me],
|
|
87
|
+
);
|
|
88
|
+
const [files, setFiles] = useState<readonly File[]>([]);
|
|
89
|
+
const findDeciders = useMemo(
|
|
90
|
+
() =>
|
|
91
|
+
peopleSearch(
|
|
92
|
+
(props.deciders ?? []).filter(
|
|
93
|
+
(person) => !deciders.some((chosen) => chosen.id === person.id),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
[props.deciders, deciders],
|
|
97
|
+
);
|
|
63
98
|
|
|
64
99
|
const labels = options.map((option) => option.trim()).filter((option) => option.length > 0);
|
|
65
100
|
const ready = question.trim().length > 0 && labels.length >= MIN_OPTIONS;
|
|
66
101
|
|
|
102
|
+
const add = (chosen: readonly File[]): File[] => {
|
|
103
|
+
const added: File[] = [];
|
|
104
|
+
let held = files;
|
|
105
|
+
for (const file of chosen) {
|
|
106
|
+
if (held.length >= MAX_FILES_PER_DECISION) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
const name = unusedName(
|
|
110
|
+
file.name,
|
|
111
|
+
held.map((kept) => kept.name),
|
|
112
|
+
);
|
|
113
|
+
const kept = name === file.name ? file : new File([file], name, { type: file.type });
|
|
114
|
+
held = [...held, kept];
|
|
115
|
+
added.push(kept);
|
|
116
|
+
}
|
|
117
|
+
setFiles(held);
|
|
118
|
+
return added;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// A picture pasted into the context goes with the decision, placed where pictures are read.
|
|
122
|
+
const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
|
|
123
|
+
const pasted = [...event.clipboardData.files].filter((file) => isPicture(file.type));
|
|
124
|
+
if (pasted.length === 0) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
const added = add(pasted);
|
|
129
|
+
if (added.length > 0) {
|
|
130
|
+
setBody((current) =>
|
|
131
|
+
placing(
|
|
132
|
+
current,
|
|
133
|
+
added.map((file) => file.name),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
67
139
|
const submit = (event: FormEvent) => {
|
|
68
140
|
event.preventDefault();
|
|
69
|
-
props.onSubmit(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
141
|
+
props.onSubmit(
|
|
142
|
+
{
|
|
143
|
+
question: question.trim(),
|
|
144
|
+
body,
|
|
145
|
+
options: labels.map((label) => ({ label })),
|
|
146
|
+
taskId: props.taskId ?? about?.id ?? null,
|
|
147
|
+
...decidersInput(deciders, props.me),
|
|
148
|
+
},
|
|
149
|
+
files,
|
|
150
|
+
);
|
|
76
151
|
};
|
|
77
152
|
|
|
78
153
|
return (
|
|
@@ -96,8 +171,43 @@ export function DecisionForm(props: DecisionFormProps) {
|
|
|
96
171
|
rows={4}
|
|
97
172
|
placeholder={form.contextPlaceholder}
|
|
98
173
|
onChange={(event) => setBody(event.target.value)}
|
|
174
|
+
onPaste={paste}
|
|
99
175
|
/>
|
|
100
176
|
</label>
|
|
177
|
+
<div className="sc-field">
|
|
178
|
+
<span className="sc-field-label">{form.files}</span>
|
|
179
|
+
{files.length > 0 && (
|
|
180
|
+
<ul className="sc-chosen-files" aria-label={form.files}>
|
|
181
|
+
{files.map((file) => (
|
|
182
|
+
<li key={file.name} className="sc-chosen-file">
|
|
183
|
+
<span>{file.name}</span> <span className="sc-muted">{formatBytes(file.size)}</span>{" "}
|
|
184
|
+
<Button
|
|
185
|
+
variant="ghost"
|
|
186
|
+
size="sm"
|
|
187
|
+
aria-label={form.removeFile(file.name)}
|
|
188
|
+
onClick={() => setFiles((current) => current.filter((held) => held !== file))}
|
|
189
|
+
>
|
|
190
|
+
×
|
|
191
|
+
</Button>
|
|
192
|
+
</li>
|
|
193
|
+
))}
|
|
194
|
+
</ul>
|
|
195
|
+
)}
|
|
196
|
+
<label className="sc-button sc-button-ghost sc-button-sm sc-file-choice">
|
|
197
|
+
{form.chooseFiles}
|
|
198
|
+
<input
|
|
199
|
+
className="sc-file-input"
|
|
200
|
+
type="file"
|
|
201
|
+
multiple
|
|
202
|
+
disabled={props.busy === true || files.length >= MAX_FILES_PER_DECISION}
|
|
203
|
+
onChange={(event) => {
|
|
204
|
+
add([...(event.target.files ?? [])]);
|
|
205
|
+
event.target.value = "";
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
</label>
|
|
209
|
+
<p className="sc-field-hint">{form.filesHint}</p>
|
|
210
|
+
</div>
|
|
101
211
|
{props.taskId === undefined && props.findTasks !== undefined && (
|
|
102
212
|
<div className="sc-field">
|
|
103
213
|
<span className="sc-field-label">{form.about}</span>
|
|
@@ -114,24 +224,51 @@ export function DecisionForm(props: DecisionFormProps) {
|
|
|
114
224
|
</div>
|
|
115
225
|
)}
|
|
116
226
|
{props.deciders !== undefined && (
|
|
117
|
-
<
|
|
118
|
-
<
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
render={(person) => (
|
|
124
|
-
<>
|
|
125
|
-
<PersonChip person={person} size="sm" />
|
|
227
|
+
<fieldset className="sc-field sc-workers-field">
|
|
228
|
+
<legend className="sc-field-label">{form.decider}</legend>
|
|
229
|
+
<div className="sc-chips">
|
|
230
|
+
{deciders.map((person) => (
|
|
231
|
+
<span key={person.id} className="sc-chip">
|
|
232
|
+
<PersonChip person={person} />
|
|
126
233
|
{person.id === props.me?.id && <span className="sc-muted"> {form.me}</span>}
|
|
127
|
-
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
className="sc-chip-remove"
|
|
237
|
+
aria-label={`${words.common.remove}: ${personName(person)}`}
|
|
238
|
+
disabled={props.busy}
|
|
239
|
+
onClick={() =>
|
|
240
|
+
setDeciders((current) => current.filter((chosen) => chosen.id !== person.id))
|
|
241
|
+
}
|
|
242
|
+
>
|
|
243
|
+
<span aria-hidden="true">×</span>
|
|
244
|
+
</button>
|
|
245
|
+
</span>
|
|
246
|
+
))}
|
|
247
|
+
{deciders.length === 0 && <span className="sc-muted">{form.deciderPlaceholder}</span>}
|
|
248
|
+
{deciders.length < MAX_DECIDERS && (
|
|
249
|
+
<Picker
|
|
250
|
+
value={undefined}
|
|
251
|
+
search={findDeciders}
|
|
252
|
+
keyOf={(person) => person.id}
|
|
253
|
+
render={(person) => (
|
|
254
|
+
<>
|
|
255
|
+
<PersonChip person={person} size="sm" />
|
|
256
|
+
{person.id === props.me?.id && <span className="sc-muted"> {form.me}</span>}
|
|
257
|
+
</>
|
|
258
|
+
)}
|
|
259
|
+
label={form.addDecider}
|
|
260
|
+
placeholder={form.addDecider}
|
|
261
|
+
disabled={props.busy}
|
|
262
|
+
onChange={(person) => {
|
|
263
|
+
if (person !== undefined) {
|
|
264
|
+
setDeciders((current) => [...current, person]);
|
|
265
|
+
}
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
128
268
|
)}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
onChange={(person) => setDecider(person ?? props.me)}
|
|
133
|
-
/>
|
|
134
|
-
</div>
|
|
269
|
+
</div>
|
|
270
|
+
{deciders.length > 1 && <p className="sc-field-hint">{form.decidersHint}</p>}
|
|
271
|
+
</fieldset>
|
|
135
272
|
)}
|
|
136
273
|
<fieldset className="sc-field sc-options">
|
|
137
274
|
<legend className="sc-field-label">{form.options}</legend>
|
|
@@ -6,13 +6,17 @@ import {
|
|
|
6
6
|
MAX_OPTION_LABEL_LENGTH,
|
|
7
7
|
type RestAnswerInput,
|
|
8
8
|
type RestDecision,
|
|
9
|
+
type RestDecisionFile,
|
|
9
10
|
type RestDecisionPatch,
|
|
11
|
+
type RestPerson,
|
|
10
12
|
type RestTask,
|
|
11
13
|
} from "../api.js";
|
|
12
14
|
import { useWords } from "../i18n/index.js";
|
|
15
|
+
import { isPicture, namedFile, placed } from "./files.js";
|
|
13
16
|
import { Markdown } from "./markdown.js";
|
|
14
17
|
import { Pager, pageOf, RECORD_PAGE_SIZE } from "./paged.js";
|
|
15
|
-
import {
|
|
18
|
+
import { Lightbox, PictureThumb, usePicture } from "./pictures.js";
|
|
19
|
+
import { Badge, Button, cx, formatBytes, PersonChip, personName, Time } from "./ui.js";
|
|
16
20
|
|
|
17
21
|
// The decisions: the ones that wait, each a card with the way to answer it, since they are what
|
|
18
22
|
// a person came for; and the ones answered, the record, a row each, since they are read back
|
|
@@ -31,6 +35,8 @@ export interface DecisionCardProps {
|
|
|
31
35
|
readonly href?: string | undefined;
|
|
32
36
|
/** The project the decision is in, named where a page shows the decisions of several. */
|
|
33
37
|
readonly project?: React.ReactNode;
|
|
38
|
+
/** Where a file put on the decision is read, by its id (ADR-0055); left out, the files are named and not linked. */
|
|
39
|
+
readonly fileHref?: ((fileId: string) => string) | undefined;
|
|
34
40
|
/** Called with the person's answer. Left out, a waiting decision cannot be answered here. */
|
|
35
41
|
readonly onAnswer?: ((decision: RestDecision, input: RestAnswerInput) => void) | undefined;
|
|
36
42
|
/** Called to grow the record, with what followed. Left out, it cannot be written here. */
|
|
@@ -38,13 +44,34 @@ export interface DecisionCardProps {
|
|
|
38
44
|
readonly busy?: boolean | undefined;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
/** The word beside who answered: through their agent, when the answer came that way (ADR-0056). */
|
|
48
|
+
function answeredWord(words: ReturnType<typeof useWords>, agent: string | null | undefined) {
|
|
49
|
+
return agent === null || agent === undefined
|
|
50
|
+
? words.decisions.answeredBy
|
|
51
|
+
: words.decisions.answeredThrough(agentLabel(agent));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Everyone a decision is addressed to (ADR-0057): `deciders` where the console answers it, else the one decider. */
|
|
55
|
+
export function decidersOf(decision: RestDecision): readonly RestPerson[] {
|
|
56
|
+
return decision.deciders !== undefined && decision.deciders.length > 0
|
|
57
|
+
? decision.deciders
|
|
58
|
+
: [decision.decider];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The names of everyone a decision is addressed to, one after another. */
|
|
62
|
+
export function deciderNames(decision: RestDecision): string {
|
|
63
|
+
return decidersOf(decision).map(personName).join(", ");
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
/**
|
|
42
67
|
* Who asked, and when: the person, through their agent when a run asked; and whom the decision
|
|
43
|
-
* is for, where that is
|
|
68
|
+
* is for, where that is not the asker alone (ADR-0042, ADR-0057).
|
|
44
69
|
*/
|
|
45
70
|
function Asked(props: { readonly decision: RestDecision; readonly day?: boolean }) {
|
|
46
71
|
const words = useWords();
|
|
47
72
|
const { decision } = props;
|
|
73
|
+
const deciders = decidersOf(decision);
|
|
74
|
+
const addressed = deciders.some((person) => person.id !== decision.raisedBy.id);
|
|
48
75
|
return (
|
|
49
76
|
<>
|
|
50
77
|
<PersonChip person={decision.raisedBy} />{" "}
|
|
@@ -52,18 +79,17 @@ function Asked(props: { readonly decision: RestDecision; readonly day?: boolean
|
|
|
52
79
|
? words.decisions.asked
|
|
53
80
|
: words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
|
|
54
81
|
<Time iso={decision.createdAt} precision={props.day === true ? "day" : "minute"} />
|
|
55
|
-
{
|
|
82
|
+
{addressed && (
|
|
56
83
|
<span className="sc-decision-for">
|
|
57
84
|
<span className="sc-visually-hidden">
|
|
58
|
-
{words.decisions.peopleLabel(
|
|
59
|
-
personName(decision.raisedBy),
|
|
60
|
-
personName(decision.decider),
|
|
61
|
-
)}
|
|
85
|
+
{words.decisions.peopleLabel(personName(decision.raisedBy), deciderNames(decision))}
|
|
62
86
|
</span>
|
|
63
87
|
<span className="sc-decision-arrow" aria-hidden="true">
|
|
64
88
|
→
|
|
65
89
|
</span>{" "}
|
|
66
|
-
|
|
90
|
+
{deciders.map((person) => (
|
|
91
|
+
<PersonChip key={person.id} person={person} />
|
|
92
|
+
))}
|
|
67
93
|
</span>
|
|
68
94
|
)}
|
|
69
95
|
</>
|
|
@@ -228,7 +254,20 @@ function Outcome(props: {
|
|
|
228
254
|
{decision.outcome === null ? (
|
|
229
255
|
<p className="sc-muted">{words.decisions.nothingWritten}</p>
|
|
230
256
|
) : (
|
|
231
|
-
|
|
257
|
+
<>
|
|
258
|
+
<Markdown source={decision.outcome} docHref={props.docHref} />
|
|
259
|
+
{decision.outcomeBy != null && decision.outcomeAt != null && (
|
|
260
|
+
<p className="sc-decision-meta">
|
|
261
|
+
<PersonChip person={decision.outcomeBy} />{" "}
|
|
262
|
+
{decision.outcomeAgent == null
|
|
263
|
+
? words.decisions.wroteWhatFollowed
|
|
264
|
+
: words.decisions.wroteWhatFollowedThrough(
|
|
265
|
+
agentLabel(decision.outcomeAgent),
|
|
266
|
+
)}{" "}
|
|
267
|
+
<Time iso={decision.outcomeAt} />
|
|
268
|
+
</p>
|
|
269
|
+
)}
|
|
270
|
+
</>
|
|
232
271
|
)}
|
|
233
272
|
{props.onUpdate !== undefined && (
|
|
234
273
|
<Button
|
|
@@ -255,6 +294,20 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
255
294
|
const words = useWords();
|
|
256
295
|
const { decision, task } = props;
|
|
257
296
|
const answered = decision.answer !== null;
|
|
297
|
+
// The files put on the decision (ADR-0055): a picture the context names by its file name is
|
|
298
|
+
// shown where the text puts it; the rest follow the context, pictures in place and
|
|
299
|
+
// everything else as a link.
|
|
300
|
+
const files = decision.files ?? [];
|
|
301
|
+
const pictures = usePicture();
|
|
302
|
+
const hrefOf = (file: RestDecisionFile) => props.fileHref?.(file.id);
|
|
303
|
+
const named = (source: string): string | undefined => {
|
|
304
|
+
const name = namedFile(source);
|
|
305
|
+
const file = files.find(
|
|
306
|
+
(candidate) => candidate.file.name === name && isPicture(candidate.file.contentType),
|
|
307
|
+
);
|
|
308
|
+
return file === undefined ? undefined : hrefOf(file);
|
|
309
|
+
};
|
|
310
|
+
const following = files.filter((file) => !placed(decision.body, file.file.name));
|
|
258
311
|
|
|
259
312
|
return (
|
|
260
313
|
<article id={decision.id} className={cx("sc-decision", answered && "sc-decision-answered")}>
|
|
@@ -289,7 +342,53 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
289
342
|
{decision.body.length > 0 && (
|
|
290
343
|
<section className="sc-decision-section" aria-label={words.decisions.context}>
|
|
291
344
|
<h4 className="sc-decision-label">{words.decisions.context}</h4>
|
|
292
|
-
<Markdown
|
|
345
|
+
<Markdown
|
|
346
|
+
source={decision.body}
|
|
347
|
+
docHref={props.docHref}
|
|
348
|
+
fileSrc={named}
|
|
349
|
+
onOpenPicture={pictures.open}
|
|
350
|
+
/>
|
|
351
|
+
</section>
|
|
352
|
+
)}
|
|
353
|
+
{following.length > 0 && (
|
|
354
|
+
<section className="sc-decision-section" aria-label={words.decisions.files}>
|
|
355
|
+
<h4 className="sc-decision-label">{words.decisions.files}</h4>
|
|
356
|
+
<ul className="sc-decision-files">
|
|
357
|
+
{following.map((attached) => {
|
|
358
|
+
const href = hrefOf(attached);
|
|
359
|
+
const { file } = attached;
|
|
360
|
+
if (href !== undefined && isPicture(file.contentType)) {
|
|
361
|
+
return (
|
|
362
|
+
<li key={attached.id} className="sc-decision-picture">
|
|
363
|
+
<PictureThumb
|
|
364
|
+
picture={{
|
|
365
|
+
src: href,
|
|
366
|
+
alt: words.decisions.picture(file.name),
|
|
367
|
+
name: file.name,
|
|
368
|
+
size: file.size,
|
|
369
|
+
}}
|
|
370
|
+
onOpen={pictures.open}
|
|
371
|
+
/>
|
|
372
|
+
<span className="sc-muted">
|
|
373
|
+
{file.name}, {formatBytes(file.size)}
|
|
374
|
+
</span>
|
|
375
|
+
</li>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return (
|
|
379
|
+
<li key={attached.id}>
|
|
380
|
+
{href === undefined ? (
|
|
381
|
+
<span>{file.name}</span>
|
|
382
|
+
) : (
|
|
383
|
+
<a href={href} rel="nofollow ugc">
|
|
384
|
+
{file.name}
|
|
385
|
+
</a>
|
|
386
|
+
)}{" "}
|
|
387
|
+
<span className="sc-muted">{formatBytes(file.size)}</span>
|
|
388
|
+
</li>
|
|
389
|
+
);
|
|
390
|
+
})}
|
|
391
|
+
</ul>
|
|
293
392
|
</section>
|
|
294
393
|
)}
|
|
295
394
|
{decision.answer !== null ? (
|
|
@@ -303,14 +402,12 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
303
402
|
</div>
|
|
304
403
|
)}
|
|
305
404
|
<p className="sc-decision-meta">
|
|
306
|
-
<PersonChip person={decision.answer.by} /> {words.
|
|
405
|
+
<PersonChip person={decision.answer.by} /> {answeredWord(words, decision.answer.agent)}{" "}
|
|
307
406
|
<Time iso={decision.answer.at} />
|
|
308
407
|
</p>
|
|
309
408
|
</div>
|
|
310
409
|
) : props.onAnswer === undefined ? (
|
|
311
|
-
<p className="sc-decision-waiting">
|
|
312
|
-
{words.decisions.waitingFor(personName(decision.decider))}
|
|
313
|
-
</p>
|
|
410
|
+
<p className="sc-decision-waiting">{words.decisions.waitingFor(deciderNames(decision))}</p>
|
|
314
411
|
) : (
|
|
315
412
|
<AnswerForm decision={decision} onAnswer={props.onAnswer} busy={props.busy} />
|
|
316
413
|
)}
|
|
@@ -322,6 +419,7 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
322
419
|
busy={props.busy}
|
|
323
420
|
/>
|
|
324
421
|
)}
|
|
422
|
+
<Lightbox picture={pictures.shown} onClose={pictures.close} />
|
|
325
423
|
</article>
|
|
326
424
|
);
|
|
327
425
|
}
|
|
@@ -332,6 +430,8 @@ export interface DecisionListProps {
|
|
|
332
430
|
readonly tasks: ReadonlyMap<string, RestTask>;
|
|
333
431
|
readonly taskHref?: ((task: RestTask) => string) | undefined;
|
|
334
432
|
readonly docHref?: ((path: string) => string) | undefined;
|
|
433
|
+
/** Where a file put on a decision is read, by its id (ADR-0055). */
|
|
434
|
+
readonly fileHref?: ((fileId: string) => string) | undefined;
|
|
335
435
|
/** Where each decision's own page is. */
|
|
336
436
|
readonly decisionHref?: ((decision: RestDecision) => string) | undefined;
|
|
337
437
|
/** The page of the record shown, from 1, and where each page is (ADR-0010); left out, the record turns its pages in place. */
|
|
@@ -406,7 +506,7 @@ function RecordRow(props: {
|
|
|
406
506
|
</div>
|
|
407
507
|
)}
|
|
408
508
|
<p className="sc-decision-record-meta">
|
|
409
|
-
<PersonChip person={answer.by} /> {words.
|
|
509
|
+
<PersonChip person={answer.by} /> {answeredWord(words, answer.agent)}{" "}
|
|
410
510
|
<Time iso={answer.at} precision="day" />
|
|
411
511
|
</p>
|
|
412
512
|
</div>
|
|
@@ -465,6 +565,7 @@ export function DecisionList(props: DecisionListProps) {
|
|
|
465
565
|
task={decision.taskId === null ? undefined : props.tasks.get(decision.taskId)}
|
|
466
566
|
taskHref={props.taskHref}
|
|
467
567
|
docHref={props.docHref}
|
|
568
|
+
fileHref={props.fileHref}
|
|
468
569
|
href={props.decisionHref?.(decision)}
|
|
469
570
|
onAnswer={props.onAnswer}
|
|
470
571
|
onUpdate={props.onUpdate}
|
|
@@ -63,6 +63,10 @@ function sentenceOf(event: RestEvent, words: Messages): string {
|
|
|
63
63
|
return feed.event.personDeactivated(login);
|
|
64
64
|
case "person.reactivated":
|
|
65
65
|
return feed.event.personReactivated(login);
|
|
66
|
+
case "person.manager_changed":
|
|
67
|
+
return data.manager === null || data.manager === undefined
|
|
68
|
+
? feed.event.managerCleared(login)
|
|
69
|
+
: feed.event.managerChanged(login, data.manager);
|
|
66
70
|
case "workspace.updated":
|
|
67
71
|
return feed.event.workspaceUpdated(fields);
|
|
68
72
|
case "project.created":
|
|
@@ -136,7 +140,9 @@ function sentenceOf(event: RestEvent, words: Messages): string {
|
|
|
136
140
|
case "plugin.disabled":
|
|
137
141
|
return feed.event.pluginDisabled(plugin);
|
|
138
142
|
case "plugin.configured":
|
|
139
|
-
return
|
|
143
|
+
return data.capabilities === undefined
|
|
144
|
+
? feed.event.pluginConfigured(fields, plugin)
|
|
145
|
+
: feed.event.pluginConsented(plugin);
|
|
140
146
|
case "plugin.granted":
|
|
141
147
|
return feed.event.pluginGranted(data.role ?? feed.role.other, grantee, plugin);
|
|
142
148
|
case "plugin.revoked":
|
package/src/components/goals.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
agentLabel,
|
|
3
4
|
GOAL_STATES,
|
|
4
5
|
type GoalState,
|
|
5
6
|
MAX_BODY_LENGTH,
|
|
@@ -355,7 +356,9 @@ export function GoalView(props: GoalViewProps) {
|
|
|
355
356
|
<div>
|
|
356
357
|
<dt>{roadmap.setBy}</dt>
|
|
357
358
|
<dd>
|
|
358
|
-
<PersonChip person={goal.createdBy} />
|
|
359
|
+
<PersonChip person={goal.createdBy} />{" "}
|
|
360
|
+
{goal.agent != null && <>{roadmap.setThrough(agentLabel(goal.agent))} </>}
|
|
361
|
+
<Time iso={goal.createdAt} />
|
|
359
362
|
</dd>
|
|
360
363
|
</div>
|
|
361
364
|
{props.editHref !== undefined && (
|