@skillcdn/console 0.4.1 → 0.4.3
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 +19 -2
- package/SKILLCDN.md +7 -5
- package/dist/api.d.ts +5 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +569 -48
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +34 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +283 -7
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +44 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +26 -0
- package/dist/cli/plugin-check.d.ts.map +1 -0
- package/dist/cli/plugin-check.js +396 -0
- package/dist/cli/plugin-check.js.map +1 -0
- package/dist/cli/plugin-cli.d.ts +20 -0
- package/dist/cli/plugin-cli.d.ts.map +1 -0
- package/dist/cli/plugin-cli.js +189 -0
- package/dist/cli/plugin-cli.js.map +1 -0
- package/dist/cli/plugin-init.d.ts +37 -0
- package/dist/cli/plugin-init.d.ts.map +1 -0
- package/dist/cli/plugin-init.js +206 -0
- package/dist/cli/plugin-init.js.map +1 -0
- package/dist/cli/plugin-main.d.ts +3 -0
- package/dist/cli/plugin-main.d.ts.map +1 -0
- package/dist/cli/plugin-main.js +28 -0
- package/dist/cli/plugin-main.js.map +1 -0
- package/dist/client.d.ts +100 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +79 -7
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +2 -2
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +59 -15
- 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 +2 -1
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +36 -4
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +4 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +29 -3
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +33 -1
- package/dist/components/event-feed.js.map +1 -1
- 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/plugin-frame.d.ts +52 -0
- package/dist/components/plugin-frame.d.ts.map +1 -0
- package/dist/components/plugin-frame.js +314 -0
- package/dist/components/plugin-frame.js.map +1 -0
- package/dist/components/plugin-settings.d.ts +38 -0
- package/dist/components/plugin-settings.d.ts.map +1 -0
- package/dist/components/plugin-settings.js +348 -0
- package/dist/components/plugin-settings.js.map +1 -0
- package/dist/components/shell.d.ts +2 -0
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +26 -1
- package/dist/components/shell.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.map +1 -1
- package/dist/components/workspace.d.ts +7 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +1 -1
- package/dist/components/workspace.js.map +1 -1
- package/dist/console.css +330 -1
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +152 -41
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +9 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +45 -9
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +155 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +166 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +150 -0
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/args.d.ts +148 -0
- package/dist/plugin/args.d.ts.map +1 -0
- package/dist/plugin/args.js +369 -0
- package/dist/plugin/args.js.map +1 -0
- package/dist/plugin/bridge.d.ts +146 -0
- package/dist/plugin/bridge.d.ts.map +1 -0
- package/dist/plugin/bridge.js +125 -0
- package/dist/plugin/bridge.js.map +1 -0
- package/dist/plugin/capabilities.d.ts +20 -0
- package/dist/plugin/capabilities.d.ts.map +1 -0
- package/dist/plugin/capabilities.js +133 -0
- package/dist/plugin/capabilities.js.map +1 -0
- package/dist/plugin/context.d.ts +380 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +2 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +208 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +482 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/definition-error.d.ts +11 -0
- package/dist/plugin/definition-error.d.ts.map +1 -0
- package/dist/plugin/definition-error.js +17 -0
- package/dist/plugin/definition-error.js.map +1 -0
- package/dist/plugin/limits.d.ts +67 -0
- package/dist/plugin/limits.d.ts.map +1 -0
- package/dist/plugin/limits.js +70 -0
- package/dist/plugin/limits.js.map +1 -0
- package/dist/plugin/load-worker.d.ts +7 -0
- package/dist/plugin/load-worker.d.ts.map +1 -0
- package/dist/plugin/load-worker.js +41 -0
- package/dist/plugin/load-worker.js.map +1 -0
- package/dist/plugin/load.d.ts +18 -0
- package/dist/plugin/load.d.ts.map +1 -0
- package/dist/plugin/load.js +62 -0
- package/dist/plugin/load.js.map +1 -0
- package/dist/plugin/manifest.d.ts +237 -0
- package/dist/plugin/manifest.d.ts.map +1 -0
- package/dist/plugin/manifest.js +411 -0
- package/dist/plugin/manifest.js.map +1 -0
- package/dist/plugin/refusals.d.ts +49 -0
- package/dist/plugin/refusals.d.ts.map +1 -0
- package/dist/plugin/refusals.js +79 -0
- package/dist/plugin/refusals.js.map +1 -0
- package/dist/plugin/rest-schemas.d.ts +556 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -0
- package/dist/plugin/rest-schemas.js +262 -0
- package/dist/plugin/rest-schemas.js.map +1 -0
- package/dist/plugin/sandbox.d.ts +23 -0
- package/dist/plugin/sandbox.d.ts.map +1 -0
- package/dist/plugin/sandbox.js +99 -0
- package/dist/plugin/sandbox.js.map +1 -0
- package/dist/plugin/testing.d.ts +45 -0
- package/dist/plugin/testing.d.ts.map +1 -0
- package/dist/plugin/testing.js +420 -0
- package/dist/plugin/testing.js.map +1 -0
- package/dist/plugin/web-react.d.ts +11 -0
- package/dist/plugin/web-react.d.ts.map +1 -0
- package/dist/plugin/web-react.js +34 -0
- package/dist/plugin/web-react.js.map +1 -0
- package/dist/plugin/web.d.ts +95 -0
- package/dist/plugin/web.d.ts.map +1 -0
- package/dist/plugin/web.js +245 -0
- package/dist/plugin/web.js.map +1 -0
- package/dist/plugin-nav.d.ts +29 -0
- package/dist/plugin-nav.d.ts.map +1 -0
- package/dist/plugin-nav.js +92 -0
- package/dist/plugin-nav.js.map +1 -0
- package/dist/plugin-testing.d.ts +2 -0
- package/dist/plugin-testing.d.ts.map +1 -0
- package/dist/plugin-testing.js +4 -0
- package/dist/plugin-testing.js.map +1 -0
- package/dist/plugin-web-react.d.ts +3 -0
- package/dist/plugin-web-react.d.ts.map +1 -0
- package/dist/plugin-web-react.js +3 -0
- package/dist/plugin-web-react.js.map +1 -0
- package/dist/plugin-web.d.ts +4 -0
- package/dist/plugin-web.d.ts.map +1 -0
- package/dist/plugin-web.js +5 -0
- package/dist/plugin-web.js.map +1 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +15 -0
- package/dist/plugin.js.map +1 -0
- package/dist/router.d.ts +28 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +47 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +4 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +6 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +194 -9
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +98 -17
- package/dist/schemas.js.map +1 -1
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +101 -0
- package/dist/tokens.js.map +1 -0
- package/dist/vocabulary.d.ts +11 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +24 -0
- package/dist/vocabulary.js.map +1 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +2 -0
- package/dist/web.js.map +1 -1
- package/docs/specs/cli.md +12 -5
- package/docs/specs/plugins.md +488 -0
- package/docs/specs/rest.md +11 -9
- package/package.json +26 -8
- package/skills/building-a-console/SKILL.md +25 -26
- package/skills/building-a-console/references/configuring.md +40 -0
- package/skills/building-a-console/references/the-default-console.md +1 -1
- package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
- package/skills/building-a-plugin/SKILL.md +137 -0
- package/skills/deploying-a-console/SKILL.md +24 -26
- package/skills/deploying-a-console/references/access.md +13 -9
- package/skills/deploying-a-console/references/resources.md +25 -10
- package/skills/deploying-a-console/references/running-it-again.md +15 -0
- package/skills/deploying-a-console/references/sizing.md +2 -0
- package/skills/planning-the-board/SKILL.md +96 -0
- package/skills/planning-the-board/references/a-task.md +36 -0
- package/skills/planning-the-board/references/setting-up.md +44 -0
- package/skills/working-the-board/SKILL.md +43 -34
- package/skills/working-the-board/references/the-pack.md +19 -0
- package/src/api.ts +12 -0
- package/src/cli/cli.ts +737 -55
- package/src/cli/format.ts +368 -10
- package/src/cli/help.ts +50 -6
- package/src/cli/plugin-check.ts +497 -0
- package/src/cli/plugin-cli.ts +237 -0
- package/src/cli/plugin-init.ts +257 -0
- package/src/cli/plugin-main.ts +30 -0
- package/src/client.ts +322 -9
- package/src/compat.ts +2 -2
- package/src/components/board.tsx +62 -14
- package/src/components/comments.tsx +1 -0
- package/src/components/decision-form.tsx +92 -11
- package/src/components/decision-list.tsx +71 -2
- package/src/components/event-feed.tsx +42 -1
- package/src/components/my-decisions.tsx +4 -1
- package/src/components/plugin-frame.tsx +437 -0
- package/src/components/plugin-settings.tsx +1111 -0
- package/src/components/shell.tsx +82 -14
- package/src/components/task-view.tsx +2 -1
- package/src/components/workspace.tsx +18 -0
- package/src/console.tsx +304 -31
- package/src/data.ts +59 -11
- package/src/i18n/en.ts +175 -0
- package/src/i18n/ko.ts +155 -0
- package/src/index.ts +12 -0
- package/src/limits.ts +2 -0
- package/src/plugin/args.ts +526 -0
- package/src/plugin/bridge.ts +314 -0
- package/src/plugin/capabilities.ts +150 -0
- package/src/plugin/context.ts +421 -0
- package/src/plugin/define.ts +792 -0
- package/src/plugin/definition-error.ts +19 -0
- package/src/plugin/limits.ts +70 -0
- package/src/plugin/load-worker.ts +50 -0
- package/src/plugin/load.ts +82 -0
- package/src/plugin/manifest.ts +561 -0
- package/src/plugin/refusals.ts +94 -0
- package/src/plugin/rest-schemas.ts +354 -0
- package/src/plugin/sandbox.ts +123 -0
- package/src/plugin/testing.ts +612 -0
- package/src/plugin/web-react.ts +45 -0
- package/src/plugin/web.ts +403 -0
- package/src/plugin-nav.ts +132 -0
- package/src/plugin-testing.ts +10 -0
- package/src/plugin-web-react.ts +4 -0
- package/src/plugin-web.ts +35 -0
- package/src/plugin.ts +126 -0
- package/src/router.ts +73 -0
- package/src/routes.ts +7 -0
- package/src/schemas.ts +115 -18
- package/src/styles/console.css +330 -1
- package/src/tokens.ts +101 -0
- package/src/vocabulary.ts +30 -0
- package/src/web.ts +2 -0
- package/template/plugin/.gitignore +2 -0
- package/template/plugin/README.md +26 -0
- package/template/plugin/SKILLCDN.md +13 -0
- package/template/plugin/build.mjs +40 -0
- package/template/plugin/console-plugin.json +50 -0
- package/template/plugin/package.json +27 -0
- package/template/plugin/skills/__key__/SKILL.md +23 -0
- package/template/plugin/src/check.test.ts +34 -0
- package/template/plugin/src/server/index.test.ts +41 -0
- package/template/plugin/src/server/index.ts +72 -0
- package/template/plugin/src/web/index.css +51 -0
- package/template/plugin/src/web/index.ts +115 -0
- package/template/plugin/tsconfig.json +17 -0
- package/skills/working-the-board/references/setting-up.md +0 -3
- /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
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_FILES_PER_DECISION,
|
|
4
5
|
MAX_OPTION_LABEL_LENGTH,
|
|
5
6
|
MAX_OPTIONS,
|
|
6
7
|
MAX_QUESTION_LENGTH,
|
|
@@ -10,14 +11,17 @@ import {
|
|
|
10
11
|
type RestTask,
|
|
11
12
|
} from "../api.js";
|
|
12
13
|
import { useWords } from "../i18n/index.js";
|
|
14
|
+
import { isPicture, placing, unusedName } from "./files.js";
|
|
13
15
|
import { Picker, type PickerSearch } from "./picker.js";
|
|
14
16
|
import { type TaskSearch, taskLabel } from "./task-form.js";
|
|
15
|
-
import { Button, PersonChip, personName } from "./ui.js";
|
|
17
|
+
import { Button, formatBytes, PersonChip, personName } from "./ui.js";
|
|
16
18
|
|
|
17
19
|
// Raising a decision: the question, what a person needs to know, the options to choose from,
|
|
18
20
|
// and who is to answer (ADR-0042): the person raising it, unless they name another of the
|
|
19
21
|
// 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.
|
|
22
|
+
// by searching the board a page at a time (ADR-0037), never picked from the board whole. The
|
|
23
|
+
// files put on the decision (ADR-0055) are chosen or pasted here, a pasted picture placed in
|
|
24
|
+
// the context where pictures are read, as a comment's are.
|
|
21
25
|
|
|
22
26
|
export interface DecisionFormProps {
|
|
23
27
|
/** The task it is about from the start, when raised from a task's page; the field is not shown then. */
|
|
@@ -30,7 +34,8 @@ export interface DecisionFormProps {
|
|
|
30
34
|
readonly me?: RestPerson | undefined;
|
|
31
35
|
readonly busy?: boolean | undefined;
|
|
32
36
|
readonly error?: string | undefined;
|
|
33
|
-
|
|
37
|
+
/** Called with the decision as the API takes it, and the files put on it (ADR-0055), none when none were chosen. */
|
|
38
|
+
readonly onSubmit: (input: RestDecisionInput, files: readonly File[]) => void;
|
|
34
39
|
readonly onCancel?: (() => void) | undefined;
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -59,20 +64,61 @@ export function DecisionForm(props: DecisionFormProps) {
|
|
|
59
64
|
const [about, setAbout] = useState<RestTask | undefined>(undefined);
|
|
60
65
|
const [options, setOptions] = useState<readonly string[]>(["", ""]);
|
|
61
66
|
const [decider, setDecider] = useState<RestPerson | undefined>(props.me);
|
|
67
|
+
const [files, setFiles] = useState<readonly File[]>([]);
|
|
62
68
|
const findDeciders = useMemo(() => peopleSearch(props.deciders ?? []), [props.deciders]);
|
|
63
69
|
|
|
64
70
|
const labels = options.map((option) => option.trim()).filter((option) => option.length > 0);
|
|
65
71
|
const ready = question.trim().length > 0 && labels.length >= MIN_OPTIONS;
|
|
66
72
|
|
|
73
|
+
const add = (chosen: readonly File[]): File[] => {
|
|
74
|
+
const added: File[] = [];
|
|
75
|
+
let held = files;
|
|
76
|
+
for (const file of chosen) {
|
|
77
|
+
if (held.length >= MAX_FILES_PER_DECISION) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
const name = unusedName(
|
|
81
|
+
file.name,
|
|
82
|
+
held.map((kept) => kept.name),
|
|
83
|
+
);
|
|
84
|
+
const kept = name === file.name ? file : new File([file], name, { type: file.type });
|
|
85
|
+
held = [...held, kept];
|
|
86
|
+
added.push(kept);
|
|
87
|
+
}
|
|
88
|
+
setFiles(held);
|
|
89
|
+
return added;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// A picture pasted into the context goes with the decision, placed where pictures are read.
|
|
93
|
+
const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
|
|
94
|
+
const pasted = [...event.clipboardData.files].filter((file) => isPicture(file.type));
|
|
95
|
+
if (pasted.length === 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
const added = add(pasted);
|
|
100
|
+
if (added.length > 0) {
|
|
101
|
+
setBody((current) =>
|
|
102
|
+
placing(
|
|
103
|
+
current,
|
|
104
|
+
added.map((file) => file.name),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
67
110
|
const submit = (event: FormEvent) => {
|
|
68
111
|
event.preventDefault();
|
|
69
|
-
props.onSubmit(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
112
|
+
props.onSubmit(
|
|
113
|
+
{
|
|
114
|
+
question: question.trim(),
|
|
115
|
+
body,
|
|
116
|
+
options: labels.map((label) => ({ label })),
|
|
117
|
+
taskId: props.taskId ?? about?.id ?? null,
|
|
118
|
+
...(decider === undefined || decider.id === props.me?.id ? {} : { deciderId: decider.id }),
|
|
119
|
+
},
|
|
120
|
+
files,
|
|
121
|
+
);
|
|
76
122
|
};
|
|
77
123
|
|
|
78
124
|
return (
|
|
@@ -96,8 +142,43 @@ export function DecisionForm(props: DecisionFormProps) {
|
|
|
96
142
|
rows={4}
|
|
97
143
|
placeholder={form.contextPlaceholder}
|
|
98
144
|
onChange={(event) => setBody(event.target.value)}
|
|
145
|
+
onPaste={paste}
|
|
99
146
|
/>
|
|
100
147
|
</label>
|
|
148
|
+
<div className="sc-field">
|
|
149
|
+
<span className="sc-field-label">{form.files}</span>
|
|
150
|
+
{files.length > 0 && (
|
|
151
|
+
<ul className="sc-chosen-files" aria-label={form.files}>
|
|
152
|
+
{files.map((file) => (
|
|
153
|
+
<li key={file.name} className="sc-chosen-file">
|
|
154
|
+
<span>{file.name}</span> <span className="sc-muted">{formatBytes(file.size)}</span>{" "}
|
|
155
|
+
<Button
|
|
156
|
+
variant="ghost"
|
|
157
|
+
size="sm"
|
|
158
|
+
aria-label={form.removeFile(file.name)}
|
|
159
|
+
onClick={() => setFiles((current) => current.filter((held) => held !== file))}
|
|
160
|
+
>
|
|
161
|
+
×
|
|
162
|
+
</Button>
|
|
163
|
+
</li>
|
|
164
|
+
))}
|
|
165
|
+
</ul>
|
|
166
|
+
)}
|
|
167
|
+
<label className="sc-button sc-button-ghost sc-button-sm sc-file-choice">
|
|
168
|
+
{form.chooseFiles}
|
|
169
|
+
<input
|
|
170
|
+
className="sc-file-input"
|
|
171
|
+
type="file"
|
|
172
|
+
multiple
|
|
173
|
+
disabled={props.busy === true || files.length >= MAX_FILES_PER_DECISION}
|
|
174
|
+
onChange={(event) => {
|
|
175
|
+
add([...(event.target.files ?? [])]);
|
|
176
|
+
event.target.value = "";
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
</label>
|
|
180
|
+
<p className="sc-field-hint">{form.filesHint}</p>
|
|
181
|
+
</div>
|
|
101
182
|
{props.taskId === undefined && props.findTasks !== undefined && (
|
|
102
183
|
<div className="sc-field">
|
|
103
184
|
<span className="sc-field-label">{form.about}</span>
|
|
@@ -6,13 +6,16 @@ import {
|
|
|
6
6
|
MAX_OPTION_LABEL_LENGTH,
|
|
7
7
|
type RestAnswerInput,
|
|
8
8
|
type RestDecision,
|
|
9
|
+
type RestDecisionFile,
|
|
9
10
|
type RestDecisionPatch,
|
|
10
11
|
type RestTask,
|
|
11
12
|
} from "../api.js";
|
|
12
13
|
import { useWords } from "../i18n/index.js";
|
|
14
|
+
import { isPicture, namedFile, placed } from "./files.js";
|
|
13
15
|
import { Markdown } from "./markdown.js";
|
|
14
16
|
import { Pager, pageOf, RECORD_PAGE_SIZE } from "./paged.js";
|
|
15
|
-
import {
|
|
17
|
+
import { Lightbox, PictureThumb, usePicture } from "./pictures.js";
|
|
18
|
+
import { Badge, Button, cx, formatBytes, PersonChip, personName, Time } from "./ui.js";
|
|
16
19
|
|
|
17
20
|
// The decisions: the ones that wait, each a card with the way to answer it, since they are what
|
|
18
21
|
// a person came for; and the ones answered, the record, a row each, since they are read back
|
|
@@ -31,6 +34,8 @@ export interface DecisionCardProps {
|
|
|
31
34
|
readonly href?: string | undefined;
|
|
32
35
|
/** The project the decision is in, named where a page shows the decisions of several. */
|
|
33
36
|
readonly project?: React.ReactNode;
|
|
37
|
+
/** Where a file put on the decision is read, by its id (ADR-0055); left out, the files are named and not linked. */
|
|
38
|
+
readonly fileHref?: ((fileId: string) => string) | undefined;
|
|
34
39
|
/** Called with the person's answer. Left out, a waiting decision cannot be answered here. */
|
|
35
40
|
readonly onAnswer?: ((decision: RestDecision, input: RestAnswerInput) => void) | undefined;
|
|
36
41
|
/** Called to grow the record, with what followed. Left out, it cannot be written here. */
|
|
@@ -255,6 +260,20 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
255
260
|
const words = useWords();
|
|
256
261
|
const { decision, task } = props;
|
|
257
262
|
const answered = decision.answer !== null;
|
|
263
|
+
// The files put on the decision (ADR-0055): a picture the context names by its file name is
|
|
264
|
+
// shown where the text puts it; the rest follow the context, pictures in place and
|
|
265
|
+
// everything else as a link.
|
|
266
|
+
const files = decision.files ?? [];
|
|
267
|
+
const pictures = usePicture();
|
|
268
|
+
const hrefOf = (file: RestDecisionFile) => props.fileHref?.(file.id);
|
|
269
|
+
const named = (source: string): string | undefined => {
|
|
270
|
+
const name = namedFile(source);
|
|
271
|
+
const file = files.find(
|
|
272
|
+
(candidate) => candidate.file.name === name && isPicture(candidate.file.contentType),
|
|
273
|
+
);
|
|
274
|
+
return file === undefined ? undefined : hrefOf(file);
|
|
275
|
+
};
|
|
276
|
+
const following = files.filter((file) => !placed(decision.body, file.file.name));
|
|
258
277
|
|
|
259
278
|
return (
|
|
260
279
|
<article id={decision.id} className={cx("sc-decision", answered && "sc-decision-answered")}>
|
|
@@ -289,7 +308,53 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
289
308
|
{decision.body.length > 0 && (
|
|
290
309
|
<section className="sc-decision-section" aria-label={words.decisions.context}>
|
|
291
310
|
<h4 className="sc-decision-label">{words.decisions.context}</h4>
|
|
292
|
-
<Markdown
|
|
311
|
+
<Markdown
|
|
312
|
+
source={decision.body}
|
|
313
|
+
docHref={props.docHref}
|
|
314
|
+
fileSrc={named}
|
|
315
|
+
onOpenPicture={pictures.open}
|
|
316
|
+
/>
|
|
317
|
+
</section>
|
|
318
|
+
)}
|
|
319
|
+
{following.length > 0 && (
|
|
320
|
+
<section className="sc-decision-section" aria-label={words.decisions.files}>
|
|
321
|
+
<h4 className="sc-decision-label">{words.decisions.files}</h4>
|
|
322
|
+
<ul className="sc-decision-files">
|
|
323
|
+
{following.map((attached) => {
|
|
324
|
+
const href = hrefOf(attached);
|
|
325
|
+
const { file } = attached;
|
|
326
|
+
if (href !== undefined && isPicture(file.contentType)) {
|
|
327
|
+
return (
|
|
328
|
+
<li key={attached.id} className="sc-decision-picture">
|
|
329
|
+
<PictureThumb
|
|
330
|
+
picture={{
|
|
331
|
+
src: href,
|
|
332
|
+
alt: words.decisions.picture(file.name),
|
|
333
|
+
name: file.name,
|
|
334
|
+
size: file.size,
|
|
335
|
+
}}
|
|
336
|
+
onOpen={pictures.open}
|
|
337
|
+
/>
|
|
338
|
+
<span className="sc-muted">
|
|
339
|
+
{file.name}, {formatBytes(file.size)}
|
|
340
|
+
</span>
|
|
341
|
+
</li>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return (
|
|
345
|
+
<li key={attached.id}>
|
|
346
|
+
{href === undefined ? (
|
|
347
|
+
<span>{file.name}</span>
|
|
348
|
+
) : (
|
|
349
|
+
<a href={href} rel="nofollow ugc">
|
|
350
|
+
{file.name}
|
|
351
|
+
</a>
|
|
352
|
+
)}{" "}
|
|
353
|
+
<span className="sc-muted">{formatBytes(file.size)}</span>
|
|
354
|
+
</li>
|
|
355
|
+
);
|
|
356
|
+
})}
|
|
357
|
+
</ul>
|
|
293
358
|
</section>
|
|
294
359
|
)}
|
|
295
360
|
{decision.answer !== null ? (
|
|
@@ -322,6 +387,7 @@ export function DecisionCard(props: DecisionCardProps) {
|
|
|
322
387
|
busy={props.busy}
|
|
323
388
|
/>
|
|
324
389
|
)}
|
|
390
|
+
<Lightbox picture={pictures.shown} onClose={pictures.close} />
|
|
325
391
|
</article>
|
|
326
392
|
);
|
|
327
393
|
}
|
|
@@ -332,6 +398,8 @@ export interface DecisionListProps {
|
|
|
332
398
|
readonly tasks: ReadonlyMap<string, RestTask>;
|
|
333
399
|
readonly taskHref?: ((task: RestTask) => string) | undefined;
|
|
334
400
|
readonly docHref?: ((path: string) => string) | undefined;
|
|
401
|
+
/** Where a file put on a decision is read, by its id (ADR-0055). */
|
|
402
|
+
readonly fileHref?: ((fileId: string) => string) | undefined;
|
|
335
403
|
/** Where each decision's own page is. */
|
|
336
404
|
readonly decisionHref?: ((decision: RestDecision) => string) | undefined;
|
|
337
405
|
/** The page of the record shown, from 1, and where each page is (ADR-0010); left out, the record turns its pages in place. */
|
|
@@ -465,6 +533,7 @@ export function DecisionList(props: DecisionListProps) {
|
|
|
465
533
|
task={decision.taskId === null ? undefined : props.tasks.get(decision.taskId)}
|
|
466
534
|
taskHref={props.taskHref}
|
|
467
535
|
docHref={props.docHref}
|
|
536
|
+
fileHref={props.fileHref}
|
|
468
537
|
href={props.decisionHref?.(decision)}
|
|
469
538
|
onAnswer={props.onAnswer}
|
|
470
539
|
onUpdate={props.onUpdate}
|
|
@@ -10,8 +10,15 @@ import { formatInstant, PersonChip } from "./ui.js";
|
|
|
10
10
|
// as which agent when through one, and what. It takes its data as props and nothing from the
|
|
11
11
|
// network; the sentence is the language's (ADR-0012).
|
|
12
12
|
|
|
13
|
-
/** What an event says, without its actor: the words after the name, in the words given. */
|
|
13
|
+
/** What an event says, without its actor: the words after the name, in the words given; a board's event a plugin caused says so (ADR-0052). */
|
|
14
14
|
export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
15
|
+
const sentence = sentenceOf(event, words);
|
|
16
|
+
return event.plugin !== null && !event.kind.startsWith("plugin.")
|
|
17
|
+
? `${sentence} ${words.feed.throughPlugin(event.plugin)}`
|
|
18
|
+
: sentence;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sentenceOf(event: RestEvent, words: Messages): string {
|
|
15
22
|
const { data } = event;
|
|
16
23
|
const { feed, vocabulary } = words;
|
|
17
24
|
const task =
|
|
@@ -34,6 +41,15 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
|
34
41
|
const question = data.question ?? feed.aQuestion;
|
|
35
42
|
const goal = data.title ?? feed.aGoal;
|
|
36
43
|
const category = data.name ?? data.key ?? feed.aCategory;
|
|
44
|
+
const plugin = event.plugin ?? feed.aPlugin;
|
|
45
|
+
const grantee =
|
|
46
|
+
data.everyone === true
|
|
47
|
+
? feed.everyone
|
|
48
|
+
: data.login !== undefined
|
|
49
|
+
? data.login
|
|
50
|
+
: data.key !== undefined
|
|
51
|
+
? feed.membersOf(data.name ?? data.key)
|
|
52
|
+
: feed.someone;
|
|
37
53
|
switch (event.kind) {
|
|
38
54
|
case "person.joined":
|
|
39
55
|
return feed.event.personJoined();
|
|
@@ -115,6 +131,31 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
|
115
131
|
return feed.event.documentRestored(page);
|
|
116
132
|
case "document.file_attached":
|
|
117
133
|
return feed.event.fileAttached(data.label ?? feed.aFile, page);
|
|
134
|
+
case "plugin.enabled":
|
|
135
|
+
return feed.event.pluginEnabled(plugin);
|
|
136
|
+
case "plugin.disabled":
|
|
137
|
+
return feed.event.pluginDisabled(plugin);
|
|
138
|
+
case "plugin.configured":
|
|
139
|
+
return feed.event.pluginConfigured(fields, plugin);
|
|
140
|
+
case "plugin.granted":
|
|
141
|
+
return feed.event.pluginGranted(data.role ?? feed.role.other, grantee, plugin);
|
|
142
|
+
case "plugin.revoked":
|
|
143
|
+
return feed.event.pluginRevoked(data.role ?? feed.role.other, grantee, plugin);
|
|
144
|
+
case "plugin.upgraded":
|
|
145
|
+
return feed.event.pluginUpgraded(
|
|
146
|
+
plugin,
|
|
147
|
+
data.version === undefined ? feed.something : String(data.version),
|
|
148
|
+
);
|
|
149
|
+
case "plugin.command":
|
|
150
|
+
return feed.event.pluginCommand(
|
|
151
|
+
data.command ?? feed.something,
|
|
152
|
+
plugin,
|
|
153
|
+
feed.outcome[data.outcome ?? ""] ?? data.outcome ?? feed.something,
|
|
154
|
+
);
|
|
155
|
+
case "plugin.purged":
|
|
156
|
+
return feed.event.pluginPurged(plugin);
|
|
157
|
+
case "plugin.failed":
|
|
158
|
+
return feed.event.pluginFailed(plugin, data.problem ?? feed.something);
|
|
118
159
|
}
|
|
119
160
|
}
|
|
120
161
|
|
|
@@ -27,6 +27,8 @@ export interface MyDecisionsProps {
|
|
|
27
27
|
/** The decisions the person asked of someone else: waiting and answered. */
|
|
28
28
|
readonly raised: DecisionsPage;
|
|
29
29
|
readonly decisionHref: (projectKey: string, decisionId: string) => string;
|
|
30
|
+
/** Where a file put on a decision is read, by its project's key and its id (ADR-0055). */
|
|
31
|
+
readonly fileHref?: ((projectKey: string, fileId: string) => string) | undefined;
|
|
30
32
|
/** Called with the person's answer on a decision of theirs, in its project. Left out, a waiting decision is answered on its own page. */
|
|
31
33
|
readonly onAnswer?:
|
|
32
34
|
| ((projectKey: string, decision: RestDecision, input: RestAnswerInput) => void)
|
|
@@ -67,7 +69,7 @@ function More(props: {
|
|
|
67
69
|
export function MyDecisions(props: MyDecisionsProps) {
|
|
68
70
|
const words = useWords();
|
|
69
71
|
const own = words.myDecisions;
|
|
70
|
-
const { onAnswer } = props;
|
|
72
|
+
const { onAnswer, fileHref } = props;
|
|
71
73
|
const waiting = props.addressed.items.filter(({ decision }) => decision.answer === null);
|
|
72
74
|
const asked = props.raised.items.filter(({ decision }) => decision.answer === null);
|
|
73
75
|
const record = [...props.addressed.items, ...props.raised.items]
|
|
@@ -80,6 +82,7 @@ export function MyDecisions(props: MyDecisionsProps) {
|
|
|
80
82
|
decision={item.decision}
|
|
81
83
|
href={props.decisionHref(item.project.key, item.decision.id)}
|
|
82
84
|
project={<InProject project={item.project} />}
|
|
85
|
+
fileHref={fileHref === undefined ? undefined : (fileId) => fileHref(item.project.key, fileId)}
|
|
83
86
|
onAnswer={
|
|
84
87
|
answerable && onAnswer !== undefined
|
|
85
88
|
? (decision, input) => onAnswer(item.project.key, decision, input)
|