@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.
Files changed (299) hide show
  1. package/README.md +19 -2
  2. package/SKILLCDN.md +7 -5
  3. package/dist/api.d.ts +5 -4
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +5 -4
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +569 -48
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts +34 -3
  11. package/dist/cli/format.d.ts.map +1 -1
  12. package/dist/cli/format.js +283 -7
  13. package/dist/cli/format.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +44 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +26 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +396 -0
  20. package/dist/cli/plugin-check.js.map +1 -0
  21. package/dist/cli/plugin-cli.d.ts +20 -0
  22. package/dist/cli/plugin-cli.d.ts.map +1 -0
  23. package/dist/cli/plugin-cli.js +189 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +37 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +206 -0
  28. package/dist/cli/plugin-init.js.map +1 -0
  29. package/dist/cli/plugin-main.d.ts +3 -0
  30. package/dist/cli/plugin-main.d.ts.map +1 -0
  31. package/dist/cli/plugin-main.js +28 -0
  32. package/dist/cli/plugin-main.js.map +1 -0
  33. package/dist/client.d.ts +100 -3
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +79 -7
  36. package/dist/client.js.map +1 -1
  37. package/dist/compat.d.ts +1 -1
  38. package/dist/compat.js +2 -2
  39. package/dist/components/board.d.ts +8 -0
  40. package/dist/components/board.d.ts.map +1 -1
  41. package/dist/components/board.js +59 -15
  42. package/dist/components/board.js.map +1 -1
  43. package/dist/components/comments.d.ts.map +1 -1
  44. package/dist/components/comments.js +1 -1
  45. package/dist/components/comments.js.map +1 -1
  46. package/dist/components/decision-form.d.ts +2 -1
  47. package/dist/components/decision-form.d.ts.map +1 -1
  48. package/dist/components/decision-form.js +36 -4
  49. package/dist/components/decision-form.js.map +1 -1
  50. package/dist/components/decision-list.d.ts +4 -0
  51. package/dist/components/decision-list.d.ts.map +1 -1
  52. package/dist/components/decision-list.js +29 -3
  53. package/dist/components/decision-list.js.map +1 -1
  54. package/dist/components/event-feed.d.ts +1 -1
  55. package/dist/components/event-feed.d.ts.map +1 -1
  56. package/dist/components/event-feed.js +33 -1
  57. package/dist/components/event-feed.js.map +1 -1
  58. package/dist/components/my-decisions.d.ts +2 -0
  59. package/dist/components/my-decisions.d.ts.map +1 -1
  60. package/dist/components/my-decisions.js +2 -2
  61. package/dist/components/my-decisions.js.map +1 -1
  62. package/dist/components/plugin-frame.d.ts +52 -0
  63. package/dist/components/plugin-frame.d.ts.map +1 -0
  64. package/dist/components/plugin-frame.js +314 -0
  65. package/dist/components/plugin-frame.js.map +1 -0
  66. package/dist/components/plugin-settings.d.ts +38 -0
  67. package/dist/components/plugin-settings.d.ts.map +1 -0
  68. package/dist/components/plugin-settings.js +348 -0
  69. package/dist/components/plugin-settings.js.map +1 -0
  70. package/dist/components/shell.d.ts +2 -0
  71. package/dist/components/shell.d.ts.map +1 -1
  72. package/dist/components/shell.js +26 -1
  73. package/dist/components/shell.js.map +1 -1
  74. package/dist/components/task-view.d.ts +2 -1
  75. package/dist/components/task-view.d.ts.map +1 -1
  76. package/dist/components/task-view.js.map +1 -1
  77. package/dist/components/workspace.d.ts +7 -1
  78. package/dist/components/workspace.d.ts.map +1 -1
  79. package/dist/components/workspace.js +1 -1
  80. package/dist/components/workspace.js.map +1 -1
  81. package/dist/console.css +330 -1
  82. package/dist/console.d.ts.map +1 -1
  83. package/dist/console.js +152 -41
  84. package/dist/console.js.map +1 -1
  85. package/dist/data.d.ts +9 -2
  86. package/dist/data.d.ts.map +1 -1
  87. package/dist/data.js +45 -9
  88. package/dist/data.js.map +1 -1
  89. package/dist/i18n/en.d.ts +155 -0
  90. package/dist/i18n/en.d.ts.map +1 -1
  91. package/dist/i18n/en.js +166 -0
  92. package/dist/i18n/en.js.map +1 -1
  93. package/dist/i18n/ko.d.ts.map +1 -1
  94. package/dist/i18n/ko.js +150 -0
  95. package/dist/i18n/ko.js.map +1 -1
  96. package/dist/index.d.ts +3 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +3 -1
  99. package/dist/index.js.map +1 -1
  100. package/dist/limits.d.ts +2 -0
  101. package/dist/limits.d.ts.map +1 -1
  102. package/dist/limits.js +2 -0
  103. package/dist/limits.js.map +1 -1
  104. package/dist/plugin/args.d.ts +148 -0
  105. package/dist/plugin/args.d.ts.map +1 -0
  106. package/dist/plugin/args.js +369 -0
  107. package/dist/plugin/args.js.map +1 -0
  108. package/dist/plugin/bridge.d.ts +146 -0
  109. package/dist/plugin/bridge.d.ts.map +1 -0
  110. package/dist/plugin/bridge.js +125 -0
  111. package/dist/plugin/bridge.js.map +1 -0
  112. package/dist/plugin/capabilities.d.ts +20 -0
  113. package/dist/plugin/capabilities.d.ts.map +1 -0
  114. package/dist/plugin/capabilities.js +133 -0
  115. package/dist/plugin/capabilities.js.map +1 -0
  116. package/dist/plugin/context.d.ts +380 -0
  117. package/dist/plugin/context.d.ts.map +1 -0
  118. package/dist/plugin/context.js +2 -0
  119. package/dist/plugin/context.js.map +1 -0
  120. package/dist/plugin/define.d.ts +208 -0
  121. package/dist/plugin/define.d.ts.map +1 -0
  122. package/dist/plugin/define.js +482 -0
  123. package/dist/plugin/define.js.map +1 -0
  124. package/dist/plugin/definition-error.d.ts +11 -0
  125. package/dist/plugin/definition-error.d.ts.map +1 -0
  126. package/dist/plugin/definition-error.js +17 -0
  127. package/dist/plugin/definition-error.js.map +1 -0
  128. package/dist/plugin/limits.d.ts +67 -0
  129. package/dist/plugin/limits.d.ts.map +1 -0
  130. package/dist/plugin/limits.js +70 -0
  131. package/dist/plugin/limits.js.map +1 -0
  132. package/dist/plugin/load-worker.d.ts +7 -0
  133. package/dist/plugin/load-worker.d.ts.map +1 -0
  134. package/dist/plugin/load-worker.js +41 -0
  135. package/dist/plugin/load-worker.js.map +1 -0
  136. package/dist/plugin/load.d.ts +18 -0
  137. package/dist/plugin/load.d.ts.map +1 -0
  138. package/dist/plugin/load.js +62 -0
  139. package/dist/plugin/load.js.map +1 -0
  140. package/dist/plugin/manifest.d.ts +237 -0
  141. package/dist/plugin/manifest.d.ts.map +1 -0
  142. package/dist/plugin/manifest.js +411 -0
  143. package/dist/plugin/manifest.js.map +1 -0
  144. package/dist/plugin/refusals.d.ts +49 -0
  145. package/dist/plugin/refusals.d.ts.map +1 -0
  146. package/dist/plugin/refusals.js +79 -0
  147. package/dist/plugin/refusals.js.map +1 -0
  148. package/dist/plugin/rest-schemas.d.ts +556 -0
  149. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  150. package/dist/plugin/rest-schemas.js +262 -0
  151. package/dist/plugin/rest-schemas.js.map +1 -0
  152. package/dist/plugin/sandbox.d.ts +23 -0
  153. package/dist/plugin/sandbox.d.ts.map +1 -0
  154. package/dist/plugin/sandbox.js +99 -0
  155. package/dist/plugin/sandbox.js.map +1 -0
  156. package/dist/plugin/testing.d.ts +45 -0
  157. package/dist/plugin/testing.d.ts.map +1 -0
  158. package/dist/plugin/testing.js +420 -0
  159. package/dist/plugin/testing.js.map +1 -0
  160. package/dist/plugin/web-react.d.ts +11 -0
  161. package/dist/plugin/web-react.d.ts.map +1 -0
  162. package/dist/plugin/web-react.js +34 -0
  163. package/dist/plugin/web-react.js.map +1 -0
  164. package/dist/plugin/web.d.ts +95 -0
  165. package/dist/plugin/web.d.ts.map +1 -0
  166. package/dist/plugin/web.js +245 -0
  167. package/dist/plugin/web.js.map +1 -0
  168. package/dist/plugin-nav.d.ts +29 -0
  169. package/dist/plugin-nav.d.ts.map +1 -0
  170. package/dist/plugin-nav.js +92 -0
  171. package/dist/plugin-nav.js.map +1 -0
  172. package/dist/plugin-testing.d.ts +2 -0
  173. package/dist/plugin-testing.d.ts.map +1 -0
  174. package/dist/plugin-testing.js +4 -0
  175. package/dist/plugin-testing.js.map +1 -0
  176. package/dist/plugin-web-react.d.ts +3 -0
  177. package/dist/plugin-web-react.d.ts.map +1 -0
  178. package/dist/plugin-web-react.js +3 -0
  179. package/dist/plugin-web-react.js.map +1 -0
  180. package/dist/plugin-web.d.ts +4 -0
  181. package/dist/plugin-web.d.ts.map +1 -0
  182. package/dist/plugin-web.js +5 -0
  183. package/dist/plugin-web.js.map +1 -0
  184. package/dist/plugin.d.ts +11 -0
  185. package/dist/plugin.d.ts.map +1 -0
  186. package/dist/plugin.js +15 -0
  187. package/dist/plugin.js.map +1 -0
  188. package/dist/router.d.ts +28 -0
  189. package/dist/router.d.ts.map +1 -1
  190. package/dist/router.js +47 -0
  191. package/dist/router.js.map +1 -1
  192. package/dist/routes.d.ts +4 -0
  193. package/dist/routes.d.ts.map +1 -1
  194. package/dist/routes.js +6 -0
  195. package/dist/routes.js.map +1 -1
  196. package/dist/schemas.d.ts +194 -9
  197. package/dist/schemas.d.ts.map +1 -1
  198. package/dist/schemas.js +98 -17
  199. package/dist/schemas.js.map +1 -1
  200. package/dist/tokens.d.ts +2 -0
  201. package/dist/tokens.d.ts.map +1 -0
  202. package/dist/tokens.js +101 -0
  203. package/dist/tokens.js.map +1 -0
  204. package/dist/vocabulary.d.ts +11 -1
  205. package/dist/vocabulary.d.ts.map +1 -1
  206. package/dist/vocabulary.js +24 -0
  207. package/dist/vocabulary.js.map +1 -1
  208. package/dist/web.d.ts.map +1 -1
  209. package/dist/web.js +2 -0
  210. package/dist/web.js.map +1 -1
  211. package/docs/specs/cli.md +12 -5
  212. package/docs/specs/plugins.md +488 -0
  213. package/docs/specs/rest.md +11 -9
  214. package/package.json +26 -8
  215. package/skills/building-a-console/SKILL.md +25 -26
  216. package/skills/building-a-console/references/configuring.md +40 -0
  217. package/skills/building-a-console/references/the-default-console.md +1 -1
  218. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  219. package/skills/building-a-plugin/SKILL.md +137 -0
  220. package/skills/deploying-a-console/SKILL.md +24 -26
  221. package/skills/deploying-a-console/references/access.md +13 -9
  222. package/skills/deploying-a-console/references/resources.md +25 -10
  223. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  224. package/skills/deploying-a-console/references/sizing.md +2 -0
  225. package/skills/planning-the-board/SKILL.md +96 -0
  226. package/skills/planning-the-board/references/a-task.md +36 -0
  227. package/skills/planning-the-board/references/setting-up.md +44 -0
  228. package/skills/working-the-board/SKILL.md +43 -34
  229. package/skills/working-the-board/references/the-pack.md +19 -0
  230. package/src/api.ts +12 -0
  231. package/src/cli/cli.ts +737 -55
  232. package/src/cli/format.ts +368 -10
  233. package/src/cli/help.ts +50 -6
  234. package/src/cli/plugin-check.ts +497 -0
  235. package/src/cli/plugin-cli.ts +237 -0
  236. package/src/cli/plugin-init.ts +257 -0
  237. package/src/cli/plugin-main.ts +30 -0
  238. package/src/client.ts +322 -9
  239. package/src/compat.ts +2 -2
  240. package/src/components/board.tsx +62 -14
  241. package/src/components/comments.tsx +1 -0
  242. package/src/components/decision-form.tsx +92 -11
  243. package/src/components/decision-list.tsx +71 -2
  244. package/src/components/event-feed.tsx +42 -1
  245. package/src/components/my-decisions.tsx +4 -1
  246. package/src/components/plugin-frame.tsx +437 -0
  247. package/src/components/plugin-settings.tsx +1111 -0
  248. package/src/components/shell.tsx +82 -14
  249. package/src/components/task-view.tsx +2 -1
  250. package/src/components/workspace.tsx +18 -0
  251. package/src/console.tsx +304 -31
  252. package/src/data.ts +59 -11
  253. package/src/i18n/en.ts +175 -0
  254. package/src/i18n/ko.ts +155 -0
  255. package/src/index.ts +12 -0
  256. package/src/limits.ts +2 -0
  257. package/src/plugin/args.ts +526 -0
  258. package/src/plugin/bridge.ts +314 -0
  259. package/src/plugin/capabilities.ts +150 -0
  260. package/src/plugin/context.ts +421 -0
  261. package/src/plugin/define.ts +792 -0
  262. package/src/plugin/definition-error.ts +19 -0
  263. package/src/plugin/limits.ts +70 -0
  264. package/src/plugin/load-worker.ts +50 -0
  265. package/src/plugin/load.ts +82 -0
  266. package/src/plugin/manifest.ts +561 -0
  267. package/src/plugin/refusals.ts +94 -0
  268. package/src/plugin/rest-schemas.ts +354 -0
  269. package/src/plugin/sandbox.ts +123 -0
  270. package/src/plugin/testing.ts +612 -0
  271. package/src/plugin/web-react.ts +45 -0
  272. package/src/plugin/web.ts +403 -0
  273. package/src/plugin-nav.ts +132 -0
  274. package/src/plugin-testing.ts +10 -0
  275. package/src/plugin-web-react.ts +4 -0
  276. package/src/plugin-web.ts +35 -0
  277. package/src/plugin.ts +126 -0
  278. package/src/router.ts +73 -0
  279. package/src/routes.ts +7 -0
  280. package/src/schemas.ts +115 -18
  281. package/src/styles/console.css +330 -1
  282. package/src/tokens.ts +101 -0
  283. package/src/vocabulary.ts +30 -0
  284. package/src/web.ts +2 -0
  285. package/template/plugin/.gitignore +2 -0
  286. package/template/plugin/README.md +26 -0
  287. package/template/plugin/SKILLCDN.md +13 -0
  288. package/template/plugin/build.mjs +40 -0
  289. package/template/plugin/console-plugin.json +50 -0
  290. package/template/plugin/package.json +27 -0
  291. package/template/plugin/skills/__key__/SKILL.md +23 -0
  292. package/template/plugin/src/check.test.ts +34 -0
  293. package/template/plugin/src/server/index.test.ts +41 -0
  294. package/template/plugin/src/server/index.ts +72 -0
  295. package/template/plugin/src/web/index.css +51 -0
  296. package/template/plugin/src/web/index.ts +115 -0
  297. package/template/plugin/tsconfig.json +17 -0
  298. package/skills/working-the-board/references/setting-up.md +0 -3
  299. /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
- readonly onSubmit: (input: RestDecisionInput) => void;
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
- question: question.trim(),
71
- body,
72
- options: labels.map((label) => ({ label })),
73
- taskId: props.taskId ?? about?.id ?? null,
74
- ...(decider === undefined || decider.id === props.me?.id ? {} : { deciderId: decider.id }),
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 { Badge, Button, cx, PersonChip, personName, Time } from "./ui.js";
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 source={decision.body} docHref={props.docHref} />
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)