@skillcdn/console 0.4.0 → 0.4.2

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 (308) hide show
  1. package/README.md +20 -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 +579 -45
  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 +277 -6
  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 +60 -7
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/plugin-check.d.ts +21 -0
  18. package/dist/cli/plugin-check.d.ts.map +1 -0
  19. package/dist/cli/plugin-check.js +374 -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 +123 -0
  24. package/dist/cli/plugin-cli.js.map +1 -0
  25. package/dist/cli/plugin-init.d.ts +20 -0
  26. package/dist/cli/plugin-init.d.ts.map +1 -0
  27. package/dist/cli/plugin-init.js +74 -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 +109 -2
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +67 -2
  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/event-feed.d.ts +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +33 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/markdown.d.ts.map +1 -1
  48. package/dist/components/markdown.js +6 -3
  49. package/dist/components/markdown.js.map +1 -1
  50. package/dist/components/pack-connect.d.ts +3 -1
  51. package/dist/components/pack-connect.d.ts.map +1 -1
  52. package/dist/components/pack-connect.js +4 -4
  53. package/dist/components/pack-connect.js.map +1 -1
  54. package/dist/components/pack-token.d.ts +3 -1
  55. package/dist/components/pack-token.d.ts.map +1 -1
  56. package/dist/components/pack-token.js +1 -1
  57. package/dist/components/pack-token.js.map +1 -1
  58. package/dist/components/people.d.ts.map +1 -1
  59. package/dist/components/people.js +14 -3
  60. package/dist/components/people.js.map +1 -1
  61. package/dist/components/plugin-frame.d.ts +52 -0
  62. package/dist/components/plugin-frame.d.ts.map +1 -0
  63. package/dist/components/plugin-frame.js +314 -0
  64. package/dist/components/plugin-frame.js.map +1 -0
  65. package/dist/components/plugin-settings.d.ts +38 -0
  66. package/dist/components/plugin-settings.d.ts.map +1 -0
  67. package/dist/components/plugin-settings.js +348 -0
  68. package/dist/components/plugin-settings.js.map +1 -0
  69. package/dist/components/remote-pictures.d.ts +6 -0
  70. package/dist/components/remote-pictures.d.ts.map +1 -0
  71. package/dist/components/remote-pictures.js +8 -0
  72. package/dist/components/remote-pictures.js.map +1 -0
  73. package/dist/components/shell.d.ts +2 -0
  74. package/dist/components/shell.d.ts.map +1 -1
  75. package/dist/components/shell.js +26 -1
  76. package/dist/components/shell.js.map +1 -1
  77. package/dist/components/sign-in.d.ts.map +1 -1
  78. package/dist/components/sign-in.js +49 -2
  79. package/dist/components/sign-in.js.map +1 -1
  80. package/dist/components/ui.d.ts +3 -2
  81. package/dist/components/ui.d.ts.map +1 -1
  82. package/dist/components/ui.js +6 -3
  83. package/dist/components/ui.js.map +1 -1
  84. package/dist/components/workspace.d.ts +7 -1
  85. package/dist/components/workspace.d.ts.map +1 -1
  86. package/dist/components/workspace.js +1 -1
  87. package/dist/components/workspace.js.map +1 -1
  88. package/dist/console.css +384 -5
  89. package/dist/console.d.ts.map +1 -1
  90. package/dist/console.js +146 -29
  91. package/dist/console.js.map +1 -1
  92. package/dist/data.d.ts +11 -2
  93. package/dist/data.d.ts.map +1 -1
  94. package/dist/data.js +35 -21
  95. package/dist/data.js.map +1 -1
  96. package/dist/i18n/en.d.ts +163 -1
  97. package/dist/i18n/en.d.ts.map +1 -1
  98. package/dist/i18n/en.js +174 -1
  99. package/dist/i18n/en.js.map +1 -1
  100. package/dist/i18n/ko.d.ts.map +1 -1
  101. package/dist/i18n/ko.js +154 -1
  102. package/dist/i18n/ko.js.map +1 -1
  103. package/dist/index.d.ts +3 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +3 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/limits.d.ts +14 -0
  108. package/dist/limits.d.ts.map +1 -1
  109. package/dist/limits.js +14 -0
  110. package/dist/limits.js.map +1 -1
  111. package/dist/plugin/args.d.ts +148 -0
  112. package/dist/plugin/args.d.ts.map +1 -0
  113. package/dist/plugin/args.js +369 -0
  114. package/dist/plugin/args.js.map +1 -0
  115. package/dist/plugin/bridge.d.ts +146 -0
  116. package/dist/plugin/bridge.d.ts.map +1 -0
  117. package/dist/plugin/bridge.js +125 -0
  118. package/dist/plugin/bridge.js.map +1 -0
  119. package/dist/plugin/capabilities.d.ts +20 -0
  120. package/dist/plugin/capabilities.d.ts.map +1 -0
  121. package/dist/plugin/capabilities.js +133 -0
  122. package/dist/plugin/capabilities.js.map +1 -0
  123. package/dist/plugin/context.d.ts +374 -0
  124. package/dist/plugin/context.d.ts.map +1 -0
  125. package/dist/plugin/context.js +2 -0
  126. package/dist/plugin/context.js.map +1 -0
  127. package/dist/plugin/define.d.ts +208 -0
  128. package/dist/plugin/define.d.ts.map +1 -0
  129. package/dist/plugin/define.js +482 -0
  130. package/dist/plugin/define.js.map +1 -0
  131. package/dist/plugin/definition-error.d.ts +11 -0
  132. package/dist/plugin/definition-error.d.ts.map +1 -0
  133. package/dist/plugin/definition-error.js +17 -0
  134. package/dist/plugin/definition-error.js.map +1 -0
  135. package/dist/plugin/limits.d.ts +67 -0
  136. package/dist/plugin/limits.d.ts.map +1 -0
  137. package/dist/plugin/limits.js +70 -0
  138. package/dist/plugin/limits.js.map +1 -0
  139. package/dist/plugin/load-worker.d.ts +7 -0
  140. package/dist/plugin/load-worker.d.ts.map +1 -0
  141. package/dist/plugin/load-worker.js +41 -0
  142. package/dist/plugin/load-worker.js.map +1 -0
  143. package/dist/plugin/load.d.ts +18 -0
  144. package/dist/plugin/load.d.ts.map +1 -0
  145. package/dist/plugin/load.js +62 -0
  146. package/dist/plugin/load.js.map +1 -0
  147. package/dist/plugin/manifest.d.ts +237 -0
  148. package/dist/plugin/manifest.d.ts.map +1 -0
  149. package/dist/plugin/manifest.js +411 -0
  150. package/dist/plugin/manifest.js.map +1 -0
  151. package/dist/plugin/refusals.d.ts +49 -0
  152. package/dist/plugin/refusals.d.ts.map +1 -0
  153. package/dist/plugin/refusals.js +79 -0
  154. package/dist/plugin/refusals.js.map +1 -0
  155. package/dist/plugin/rest-schemas.d.ts +556 -0
  156. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  157. package/dist/plugin/rest-schemas.js +262 -0
  158. package/dist/plugin/rest-schemas.js.map +1 -0
  159. package/dist/plugin/sandbox.d.ts +23 -0
  160. package/dist/plugin/sandbox.d.ts.map +1 -0
  161. package/dist/plugin/sandbox.js +99 -0
  162. package/dist/plugin/sandbox.js.map +1 -0
  163. package/dist/plugin/testing.d.ts +45 -0
  164. package/dist/plugin/testing.d.ts.map +1 -0
  165. package/dist/plugin/testing.js +420 -0
  166. package/dist/plugin/testing.js.map +1 -0
  167. package/dist/plugin/web-react.d.ts +11 -0
  168. package/dist/plugin/web-react.d.ts.map +1 -0
  169. package/dist/plugin/web-react.js +34 -0
  170. package/dist/plugin/web-react.js.map +1 -0
  171. package/dist/plugin/web.d.ts +95 -0
  172. package/dist/plugin/web.d.ts.map +1 -0
  173. package/dist/plugin/web.js +245 -0
  174. package/dist/plugin/web.js.map +1 -0
  175. package/dist/plugin-nav.d.ts +29 -0
  176. package/dist/plugin-nav.d.ts.map +1 -0
  177. package/dist/plugin-nav.js +92 -0
  178. package/dist/plugin-nav.js.map +1 -0
  179. package/dist/plugin-testing.d.ts +2 -0
  180. package/dist/plugin-testing.d.ts.map +1 -0
  181. package/dist/plugin-testing.js +4 -0
  182. package/dist/plugin-testing.js.map +1 -0
  183. package/dist/plugin-web-react.d.ts +3 -0
  184. package/dist/plugin-web-react.d.ts.map +1 -0
  185. package/dist/plugin-web-react.js +3 -0
  186. package/dist/plugin-web-react.js.map +1 -0
  187. package/dist/plugin-web.d.ts +4 -0
  188. package/dist/plugin-web.d.ts.map +1 -0
  189. package/dist/plugin-web.js +5 -0
  190. package/dist/plugin-web.js.map +1 -0
  191. package/dist/plugin.d.ts +11 -0
  192. package/dist/plugin.d.ts.map +1 -0
  193. package/dist/plugin.js +15 -0
  194. package/dist/plugin.js.map +1 -0
  195. package/dist/router.d.ts +28 -0
  196. package/dist/router.d.ts.map +1 -1
  197. package/dist/router.js +47 -0
  198. package/dist/router.js.map +1 -1
  199. package/dist/routes.d.ts +6 -0
  200. package/dist/routes.d.ts.map +1 -1
  201. package/dist/routes.js +8 -0
  202. package/dist/routes.js.map +1 -1
  203. package/dist/schemas.d.ts +147 -1
  204. package/dist/schemas.d.ts.map +1 -1
  205. package/dist/schemas.js +55 -5
  206. package/dist/schemas.js.map +1 -1
  207. package/dist/tokens.d.ts +2 -0
  208. package/dist/tokens.d.ts.map +1 -0
  209. package/dist/tokens.js +101 -0
  210. package/dist/tokens.js.map +1 -0
  211. package/dist/vocabulary.d.ts +11 -1
  212. package/dist/vocabulary.d.ts.map +1 -1
  213. package/dist/vocabulary.js +24 -0
  214. package/dist/vocabulary.js.map +1 -1
  215. package/dist/web.d.ts.map +1 -1
  216. package/dist/web.js +5 -2
  217. package/dist/web.js.map +1 -1
  218. package/docs/specs/cli.md +16 -9
  219. package/docs/specs/plugins.md +487 -0
  220. package/docs/specs/rest.md +25 -13
  221. package/package.json +21 -3
  222. package/skills/building-a-console/SKILL.md +25 -26
  223. package/skills/building-a-console/references/configuring.md +40 -0
  224. package/skills/building-a-console/references/the-default-console.md +1 -1
  225. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  226. package/skills/building-a-plugin/SKILL.md +137 -0
  227. package/skills/deploying-a-console/SKILL.md +24 -26
  228. package/skills/deploying-a-console/references/access.md +13 -9
  229. package/skills/deploying-a-console/references/resources.md +25 -10
  230. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  231. package/skills/deploying-a-console/references/sizing.md +2 -0
  232. package/skills/planning-the-board/SKILL.md +96 -0
  233. package/skills/planning-the-board/references/a-task.md +36 -0
  234. package/skills/planning-the-board/references/setting-up.md +44 -0
  235. package/skills/working-the-board/SKILL.md +43 -34
  236. package/skills/working-the-board/references/the-pack.md +19 -0
  237. package/src/api.ts +16 -0
  238. package/src/cli/cli.ts +736 -45
  239. package/src/cli/format.ts +356 -9
  240. package/src/cli/help.ts +71 -6
  241. package/src/cli/plugin-check.ts +470 -0
  242. package/src/cli/plugin-cli.ts +148 -0
  243. package/src/cli/plugin-init.ts +90 -0
  244. package/src/cli/plugin-main.ts +30 -0
  245. package/src/client.ts +324 -1
  246. package/src/compat.ts +2 -2
  247. package/src/components/board.tsx +62 -14
  248. package/src/components/event-feed.tsx +42 -1
  249. package/src/components/markdown.tsx +6 -3
  250. package/src/components/pack-connect.tsx +10 -7
  251. package/src/components/pack-token.tsx +4 -1
  252. package/src/components/people.tsx +17 -7
  253. package/src/components/plugin-frame.tsx +437 -0
  254. package/src/components/plugin-settings.tsx +1111 -0
  255. package/src/components/remote-pictures.ts +19 -0
  256. package/src/components/shell.tsx +82 -14
  257. package/src/components/sign-in.tsx +114 -13
  258. package/src/components/ui.tsx +6 -3
  259. package/src/components/workspace.tsx +18 -0
  260. package/src/console.tsx +309 -23
  261. package/src/data.ts +47 -22
  262. package/src/i18n/en.ts +183 -1
  263. package/src/i18n/ko.ts +159 -1
  264. package/src/index.ts +12 -0
  265. package/src/limits.ts +14 -0
  266. package/src/plugin/args.ts +526 -0
  267. package/src/plugin/bridge.ts +314 -0
  268. package/src/plugin/capabilities.ts +150 -0
  269. package/src/plugin/context.ts +416 -0
  270. package/src/plugin/define.ts +792 -0
  271. package/src/plugin/definition-error.ts +19 -0
  272. package/src/plugin/limits.ts +70 -0
  273. package/src/plugin/load-worker.ts +50 -0
  274. package/src/plugin/load.ts +82 -0
  275. package/src/plugin/manifest.ts +561 -0
  276. package/src/plugin/refusals.ts +94 -0
  277. package/src/plugin/rest-schemas.ts +354 -0
  278. package/src/plugin/sandbox.ts +123 -0
  279. package/src/plugin/testing.ts +612 -0
  280. package/src/plugin/web-react.ts +45 -0
  281. package/src/plugin/web.ts +403 -0
  282. package/src/plugin-nav.ts +132 -0
  283. package/src/plugin-testing.ts +10 -0
  284. package/src/plugin-web-react.ts +4 -0
  285. package/src/plugin-web.ts +35 -0
  286. package/src/plugin.ts +126 -0
  287. package/src/router.ts +73 -0
  288. package/src/routes.ts +9 -0
  289. package/src/schemas.ts +60 -4
  290. package/src/styles/console.css +384 -5
  291. package/src/tokens.ts +101 -0
  292. package/src/vocabulary.ts +30 -0
  293. package/src/web.ts +5 -2
  294. package/template/plugin/.gitignore +2 -0
  295. package/template/plugin/README.md +26 -0
  296. package/template/plugin/SKILLCDN.md +13 -0
  297. package/template/plugin/build.mjs +40 -0
  298. package/template/plugin/console-plugin.json +50 -0
  299. package/template/plugin/package.json +27 -0
  300. package/template/plugin/skills/__key__/SKILL.md +23 -0
  301. package/template/plugin/src/check.test.ts +34 -0
  302. package/template/plugin/src/server/index.test.ts +41 -0
  303. package/template/plugin/src/server/index.ts +72 -0
  304. package/template/plugin/src/web/index.css +51 -0
  305. package/template/plugin/src/web/index.ts +115 -0
  306. package/template/plugin/tsconfig.json +17 -0
  307. package/skills/working-the-board/references/setting-up.md +0 -3
  308. /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
package/src/console.tsx CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  type RestDecisionPatch,
24
24
  type RestEvent,
25
25
  type RestMyDecisions,
26
+ type RestPluginDetail,
26
27
  type RestProject,
27
28
  type RestProjectInput,
28
29
  type RestProjectPatch,
@@ -50,6 +51,8 @@ import { MyDecisions } from "./components/my-decisions.js";
50
51
  import { PackTokenWays } from "./components/pack-token.js";
51
52
  import { PAGE_SIZE } from "./components/paged.js";
52
53
  import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
54
+ import { PluginFrame, PluginWidgets } from "./components/plugin-frame.js";
55
+ import { PluginSettingsList, PluginSettingsView } from "./components/plugin-settings.js";
53
56
  import {
54
57
  MemberList,
55
58
  type MemberListProps,
@@ -58,6 +61,7 @@ import {
58
61
  type ProjectListProps,
59
62
  ProjectMark,
60
63
  } from "./components/projects.js";
64
+ import { type RemotePictureSrc, RemotePicturesContext } from "./components/remote-pictures.js";
61
65
  import { GoalTimeline } from "./components/roadmap.js";
62
66
  import { RulesForm, RulesView } from "./components/rules.js";
63
67
  import {
@@ -103,6 +107,7 @@ import {
103
107
  type Messages,
104
108
  useWords,
105
109
  } from "./i18n/index.js";
110
+ import { pluginNavItems, pluginPagesAt, pluginWordOf } from "./plugin-nav.js";
106
111
  import {
107
112
  boardHref,
108
113
  calendarHref,
@@ -120,6 +125,8 @@ import {
120
125
  PATHS,
121
126
  personAgentsHref,
122
127
  personWorkHref,
128
+ pluginHref,
129
+ pluginSettingsHref,
123
130
  projectHref,
124
131
  projectSettingsHref,
125
132
  type Route,
@@ -392,24 +399,45 @@ function ProjectsPage(props: {
392
399
  readonly data: ConsoleData;
393
400
  readonly components: ConsoleComponents;
394
401
  readonly navigation: Navigation;
402
+ readonly client: ConsoleClient;
403
+ readonly language: string;
395
404
  /** Whether the form for a new project is open: the page is at its address. */
396
405
  readonly making: boolean;
397
406
  }) {
398
407
  const words = useWords().projects;
399
- const { data, components, navigation, making } = props;
408
+ const { data, components, navigation, client, language } = props;
409
+ const me = data.me;
410
+ /** The plugins' widgets at a place of this page (ADR-0054), for the person's roles. */
411
+ const widgets = (place: "home:top" | "home:bottom") =>
412
+ me === undefined ? null : (
413
+ <PluginWidgets
414
+ plugins={data.plugins}
415
+ place={place}
416
+ me={me}
417
+ language={language}
418
+ client={client}
419
+ navigation={navigation}
420
+ nameOf={(name) => pluginWordOf(name, language)}
421
+ />
422
+ );
400
423
  const { busy, error, act } = useAction();
401
424
  const href = (project: RestProject) => projectHref(project.key);
425
+ // A project is made by an administrator (ADR-0045): a member sees the list alone, the form's
426
+ // address included.
427
+ const administrator = data.me?.person?.role === "admin";
428
+ const making = props.making && administrator;
402
429
  return (
403
430
  <>
404
431
  <div className="sc-page-head">
405
432
  <h1 className="sc-page-title">{words.title}</h1>
406
- {!making && (
433
+ {administrator && !making && (
407
434
  <LinkButton href={PATHS.newProject} variant="primary">
408
435
  {words.newProject}
409
436
  </LinkButton>
410
437
  )}
411
438
  </div>
412
439
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
440
+ {widgets("home:top")}
413
441
  {making && (
414
442
  <section className="sc-panel sc-panel-form" aria-label={words.form.aria}>
415
443
  <ProjectForm
@@ -436,15 +464,159 @@ function ProjectsPage(props: {
436
464
  empty={
437
465
  <EmptyState
438
466
  title={words.empty.title}
439
- body={words.empty.body}
440
- action={<LinkButton href={PATHS.newProject}>{words.newProject}</LinkButton>}
467
+ body={administrator ? words.empty.body : words.empty.bodyMember}
468
+ action={
469
+ administrator ? (
470
+ <LinkButton href={PATHS.newProject}>{words.newProject}</LinkButton>
471
+ ) : undefined
472
+ }
441
473
  />
442
474
  }
443
475
  />
476
+ {widgets("home:bottom")}
444
477
  </>
445
478
  );
446
479
  }
447
480
 
481
+ /**
482
+ * A plugin's page (ADR-0054): the sub-tabs of its pages at the level, where it has several, above
483
+ * the frame of the one shown, the first when the address names none. Nothing for a plugin the
484
+ * person has no page of, or that is not on the project.
485
+ */
486
+ function PluginPage(props: {
487
+ readonly route: Extract<Route, { name: "plugin" }>;
488
+ readonly data: ConsoleData;
489
+ readonly project: RestProject | undefined;
490
+ readonly client: ConsoleClient;
491
+ readonly language: string;
492
+ readonly navigation: Navigation;
493
+ }) {
494
+ const words = useWords();
495
+ const { route, data, project, client, language, navigation } = props;
496
+ const plugin = data.plugins.find(
497
+ (candidate) => candidate.key === route.plugin && candidate.state === "enabled",
498
+ );
499
+ const bound =
500
+ route.level === "workspace" ||
501
+ data.projectPlugins.some((candidate) => candidate.key === route.plugin);
502
+ const pages = plugin === undefined ? [] : pluginPagesAt(plugin, route.level);
503
+ const page = route.page === undefined ? pages[0] : pages.find((p) => p.key === route.page);
504
+ if (plugin === undefined || !bound || page === undefined || data.me === undefined) {
505
+ return <EmptyState title={words.plugins.noPage.title} body={words.plugins.noPage.body} />;
506
+ }
507
+ const projectKey = route.level === "project" ? route.project : undefined;
508
+ return (
509
+ <>
510
+ {pages.length > 1 && (
511
+ <nav className="sc-subtabs" aria-label={pluginWordOf(plugin.name, language)}>
512
+ {pages.map((candidate) => (
513
+ <a
514
+ key={candidate.key}
515
+ href={pluginHref(plugin.key, candidate.key, projectKey)}
516
+ className={candidate.key === page.key ? "sc-tab sc-tab-current" : "sc-tab"}
517
+ aria-current={candidate.key === page.key ? "page" : undefined}
518
+ >
519
+ {pluginWordOf(candidate.name, language)}
520
+ </a>
521
+ ))}
522
+ </nav>
523
+ )}
524
+ <PluginFrame
525
+ key={`${plugin.key}:${page.key}:${projectKey ?? ""}`}
526
+ plugin={plugin}
527
+ page={page.key}
528
+ project={project}
529
+ path={route.rest}
530
+ me={data.me}
531
+ language={language}
532
+ client={client}
533
+ navigation={navigation}
534
+ events={project === undefined ? undefined : data.events}
535
+ title={words.plugins.frame(pluginWordOf(page.name, language))}
536
+ />
537
+ </>
538
+ );
539
+ }
540
+
541
+ /**
542
+ * The plugins' settings (docs/specs/plugins.md, "The settings pages"): the list, or one plugin
543
+ * whole, read again after every change; for an administrator.
544
+ */
545
+ function PluginSettingsPage(props: {
546
+ readonly plugin: string | undefined;
547
+ readonly data: ConsoleData;
548
+ readonly client: ConsoleClient;
549
+ readonly language: string;
550
+ readonly navigation: Navigation;
551
+ }) {
552
+ const words = useWords();
553
+ const { data, client, language, navigation } = props;
554
+ const settings = words.plugins.settings;
555
+ const administrator = data.me?.person?.role === "admin";
556
+ const [detail, setDetail] = useState<RestPluginDetail | undefined>(undefined);
557
+ const [failure, setFailure] = useState<string | undefined>(undefined);
558
+ const key = props.plugin;
559
+ const read = useCallback(async () => {
560
+ if (key === undefined) {
561
+ return;
562
+ }
563
+ try {
564
+ setDetail(await client.plugin(key).get());
565
+ setFailure(undefined);
566
+ } catch (error) {
567
+ setFailure(error instanceof ApiError ? error.message : words.common.somethingWentWrong);
568
+ }
569
+ }, [client, key, words.common.somethingWentWrong]);
570
+ useEffect(() => {
571
+ setDetail(undefined);
572
+ void read();
573
+ }, [read]);
574
+ if (!administrator) {
575
+ return <NothingHere />;
576
+ }
577
+ if (key === undefined) {
578
+ return (
579
+ <>
580
+ <div className="sc-page-head">
581
+ <h1 className="sc-page-title">{settings.title}</h1>
582
+ </div>
583
+ <PluginSettingsList
584
+ plugins={data.plugins}
585
+ language={language}
586
+ href={(plugin) => pluginSettingsHref(plugin.key)}
587
+ />
588
+ </>
589
+ );
590
+ }
591
+ if (failure !== undefined) {
592
+ return <EmptyState title={words.common.somethingWentWrong} body={failure} />;
593
+ }
594
+ if (detail === undefined) {
595
+ return (
596
+ <div className="sc-loading">
597
+ <Spinner label={words.common.loading} />
598
+ </div>
599
+ );
600
+ }
601
+ return (
602
+ <PluginSettingsView
603
+ detail={detail}
604
+ projects={data.projects}
605
+ people={data.people}
606
+ language={language}
607
+ client={client}
608
+ onChanged={async () => {
609
+ await read();
610
+ await data.actions.refreshPlugins();
611
+ }}
612
+ onPurged={() => {
613
+ void data.actions.refreshPlugins();
614
+ navigation.replace(pluginSettingsHref());
615
+ }}
616
+ />
617
+ );
618
+ }
619
+
448
620
  /**
449
621
  * The archive of the board (ADR-0029): the tasks put away, newest first, a page at a time from
450
622
  * the server, each brought back from here by whoever may work in the project.
@@ -738,6 +910,8 @@ function ProjectPage(props: {
738
910
  readonly data: ConsoleData;
739
911
  readonly components: ConsoleComponents;
740
912
  readonly navigation: Navigation;
913
+ readonly client: ConsoleClient;
914
+ readonly language: string;
741
915
  }) {
742
916
  const words = useWords();
743
917
  const { route, project, data, components, navigation } = props;
@@ -1304,6 +1478,18 @@ function ProjectPage(props: {
1304
1478
  <ProjectSettingsPage route={route} project={project} data={data} navigation={navigation} />
1305
1479
  );
1306
1480
  }
1481
+ if (route.name === "plugin") {
1482
+ return (
1483
+ <PluginPage
1484
+ route={route}
1485
+ data={data}
1486
+ project={project}
1487
+ client={props.client}
1488
+ language={props.language}
1489
+ navigation={navigation}
1490
+ />
1491
+ );
1492
+ }
1307
1493
  return <NothingHere />;
1308
1494
  }
1309
1495
 
@@ -1432,6 +1618,7 @@ function ProjectSettingsPage(props: {
1432
1618
  busy={busy}
1433
1619
  onStart={data.actions.connectPackToken}
1434
1620
  poll={data.actions.packTokenConnection}
1621
+ claim={data.actions.claimPackTokenConnection}
1435
1622
  onCancelConnect={data.actions.cancelPackTokenConnection}
1436
1623
  onConnected={() => navigation.replace(here)}
1437
1624
  onSubmit={(token) =>
@@ -1568,9 +1755,10 @@ function WorkspaceSettingsPage(props: {
1568
1755
  readonly form: "rules" | "workspace" | "pack-token" | undefined;
1569
1756
  readonly data: ConsoleData;
1570
1757
  readonly navigation: Navigation;
1758
+ readonly language: string;
1571
1759
  }) {
1572
1760
  const words = useWords();
1573
- const { form, data, navigation } = props;
1761
+ const { form, data, navigation, language } = props;
1574
1762
  const { busy, error, act } = useAction();
1575
1763
  const administrator = data.me?.person?.role === "admin";
1576
1764
  const workspace = data.me?.workspace;
@@ -1654,6 +1842,7 @@ function WorkspaceSettingsPage(props: {
1654
1842
  busy={busy}
1655
1843
  onStart={data.actions.connectWorkspacePackToken}
1656
1844
  poll={data.actions.workspacePackTokenConnection}
1845
+ claim={data.actions.claimWorkspacePackTokenConnection}
1657
1846
  onCancelConnect={data.actions.cancelWorkspacePackTokenConnection}
1658
1847
  onConnected={() => navigation.replace(PATHS.settings)}
1659
1848
  onSubmit={(token) =>
@@ -1718,7 +1907,7 @@ function WorkspaceSettingsPage(props: {
1718
1907
  </a>
1719
1908
  )}
1720
1909
  </div>
1721
- <WorkspaceSettingsView workspace={workspace} />
1910
+ <WorkspaceSettingsView workspace={workspace} console={data.me?.console} />
1722
1911
  </section>
1723
1912
  )}
1724
1913
  <section className="sc-panel" aria-label={words.rules.title}>
@@ -1742,6 +1931,20 @@ function WorkspaceSettingsPage(props: {
1742
1931
  />
1743
1932
  )}
1744
1933
  </section>
1934
+ {administrator && (
1935
+ <section className="sc-panel" aria-label={words.plugins.settings.title}>
1936
+ <h2 className="sc-section-title">{words.plugins.settings.title}</h2>
1937
+ {data.plugins.length === 0 ? (
1938
+ <p className="sc-settings-hint">{words.plugins.settings.empty.body}</p>
1939
+ ) : (
1940
+ <PluginSettingsList
1941
+ plugins={data.plugins}
1942
+ language={language}
1943
+ href={(plugin) => pluginSettingsHref(plugin.key)}
1944
+ />
1945
+ )}
1946
+ </section>
1947
+ )}
1745
1948
  <section className="sc-panel" aria-label={words.settings.categories}>
1746
1949
  <h2 className="sc-section-title">{words.settings.categories}</h2>
1747
1950
  {data.categories.length > 0 && (
@@ -2163,11 +2366,36 @@ function WorkspacePage(props: {
2163
2366
  readonly data: ConsoleData;
2164
2367
  readonly components: ConsoleComponents;
2165
2368
  readonly navigation: Navigation;
2369
+ readonly client: ConsoleClient;
2370
+ readonly language: string;
2166
2371
  }) {
2167
2372
  const words = useWords();
2168
- const { route, data, components, navigation } = props;
2373
+ const { route, data, components, navigation, client, language } = props;
2169
2374
  const [fresh, setFresh] = useState<RestTokenCreated | undefined>(undefined);
2170
2375
  const { busy, error, act } = useAction();
2376
+ if (route.name === "plugin" && route.level === "workspace") {
2377
+ return (
2378
+ <PluginPage
2379
+ route={route}
2380
+ data={data}
2381
+ project={undefined}
2382
+ client={client}
2383
+ language={language}
2384
+ navigation={navigation}
2385
+ />
2386
+ );
2387
+ }
2388
+ if (route.name === "plugins-settings") {
2389
+ return (
2390
+ <PluginSettingsPage
2391
+ plugin={route.plugin}
2392
+ data={data}
2393
+ client={client}
2394
+ language={language}
2395
+ navigation={navigation}
2396
+ />
2397
+ );
2398
+ }
2171
2399
 
2172
2400
  if (route.name === "work") {
2173
2401
  return <WorkPage data={data} components={components} week={route.week} />;
@@ -2179,7 +2407,14 @@ function WorkspacePage(props: {
2179
2407
  return <PersonWorkPage id={route.id} data={data} components={components} week={route.week} />;
2180
2408
  }
2181
2409
  if (route.name === "workspace-settings") {
2182
- return <WorkspaceSettingsPage form={route.form} data={data} navigation={navigation} />;
2410
+ return (
2411
+ <WorkspaceSettingsPage
2412
+ form={route.form}
2413
+ data={data}
2414
+ navigation={navigation}
2415
+ language={language}
2416
+ />
2417
+ );
2183
2418
  }
2184
2419
  if (route.name === "categories") {
2185
2420
  const me = data.me?.person ?? undefined;
@@ -2334,6 +2569,8 @@ function WorkspacePage(props: {
2334
2569
  export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2335
2570
  const components: ConsoleComponents = { ...DEFAULT_COMPONENTS, ...config.components };
2336
2571
  const client = config.client ?? createClient({ baseUrl: config.baseUrl ?? "" });
2572
+ /** Where the pages read a picture from elsewhere (ADR-0048): through this client's console. */
2573
+ const pictureSrc: RemotePictureSrc = (url) => client.pictureUrl(url);
2337
2574
  const fallbackTitle = config.title ?? "Console";
2338
2575
  const languages = config.languages ?? DEFAULT_LANGUAGES;
2339
2576
  const pages = config.pages ?? [];
@@ -2440,13 +2677,6 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2440
2677
  current: route.name === "agents" || route.name === "new-token",
2441
2678
  },
2442
2679
  ]),
2443
- // The settings are read by everyone: the rules every agent follows, the categories and
2444
- // what the colours mean, where the skills come from; kept by an administrator.
2445
- {
2446
- href: PATHS.settings,
2447
- label: words.nav.settings,
2448
- current: route.name === "workspace-settings" || route.name === "categories",
2449
- },
2450
2680
  ...pages
2451
2681
  .filter((candidate) => candidate.level === "workspace")
2452
2682
  .map((candidate) => ({
@@ -2457,6 +2687,28 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2457
2687
  route.level === "workspace" &&
2458
2688
  route.page === candidate.key,
2459
2689
  })),
2690
+ // The plugins' tabs (ADR-0054): the groups that hold a plugin the person has a page of.
2691
+ ...pluginNavItems({
2692
+ plugins: data.plugins,
2693
+ level: "workspace",
2694
+ current: {
2695
+ plugin:
2696
+ route.name === "plugin" && route.level === "workspace" ? route.plugin : undefined,
2697
+ },
2698
+ words,
2699
+ language: pack.tag,
2700
+ }),
2701
+ // The settings are read by everyone: the rules every agent follows, the categories and
2702
+ // what the colours mean, where the skills come from, the plugins; kept by an
2703
+ // administrator. Last, after the organization's own tabs and the plugins'.
2704
+ {
2705
+ href: PATHS.settings,
2706
+ label: words.nav.settings,
2707
+ current:
2708
+ route.name === "workspace-settings" ||
2709
+ route.name === "categories" ||
2710
+ route.name === "plugins-settings",
2711
+ },
2460
2712
  ];
2461
2713
  const nav: NavItem[] =
2462
2714
  projectKey === undefined
@@ -2508,12 +2760,6 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2508
2760
  label: words.nav.members,
2509
2761
  current: route.name === "members",
2510
2762
  },
2511
- // Read by every member: what the project is, and what its agents are asked (ADR-0023).
2512
- {
2513
- href: projectHref(projectKey, "settings"),
2514
- label: words.nav.settings,
2515
- current: route.name === "settings",
2516
- },
2517
2763
  ...pages
2518
2764
  .filter((candidate) => candidate.level === "project")
2519
2765
  .map((candidate) => ({
@@ -2524,6 +2770,24 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2524
2770
  route.level === "project" &&
2525
2771
  route.page === candidate.key,
2526
2772
  })),
2773
+ ...pluginNavItems({
2774
+ plugins: data.plugins,
2775
+ level: "project",
2776
+ project: { key: projectKey, plugins: data.projectPlugins },
2777
+ current: {
2778
+ plugin:
2779
+ route.name === "plugin" && route.level === "project" ? route.plugin : undefined,
2780
+ },
2781
+ words,
2782
+ language: pack.tag,
2783
+ }),
2784
+ // Read by every member: what the project is, and what its agents are asked
2785
+ // (ADR-0023). Last, after the organization's own tabs and the plugins'.
2786
+ {
2787
+ href: projectHref(projectKey, "settings"),
2788
+ label: words.nav.settings,
2789
+ current: route.name === "settings",
2790
+ },
2527
2791
  ];
2528
2792
  /** A page of the console's own at the address, or nothing there. */
2529
2793
  const customPageOf = (key: string, level: CustomPage["level"]): ReactNode => {
@@ -2549,6 +2813,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2549
2813
  data={data}
2550
2814
  components={components}
2551
2815
  navigation={navigation}
2816
+ client={client}
2817
+ language={pack.tag}
2552
2818
  making={route.name === "new-project"}
2553
2819
  />
2554
2820
  );
@@ -2561,6 +2827,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2561
2827
  data={data}
2562
2828
  components={components}
2563
2829
  navigation={navigation}
2830
+ client={client}
2831
+ language={pack.tag}
2564
2832
  />
2565
2833
  );
2566
2834
  } else if (data.projectError !== undefined) {
@@ -2587,6 +2855,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2587
2855
  data={data}
2588
2856
  components={components}
2589
2857
  navigation={navigation}
2858
+ client={client}
2859
+ language={pack.tag}
2590
2860
  />
2591
2861
  );
2592
2862
  }
@@ -2619,7 +2889,17 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2619
2889
  route.name === "calendar" ||
2620
2890
  (route.name === "goal" && route.form === undefined) ||
2621
2891
  (route.name === "custom" &&
2622
- pages.some((candidate) => candidate.key === route.page && candidate.wide === true))
2892
+ pages.some((candidate) => candidate.key === route.page && candidate.wide === true)) ||
2893
+ (route.name === "plugin" &&
2894
+ data.plugins.some(
2895
+ (candidate) =>
2896
+ candidate.key === route.plugin &&
2897
+ pluginPagesAt(candidate, route.level).some(
2898
+ (candidate, index) =>
2899
+ candidate.wide &&
2900
+ (route.page === undefined ? index === 0 : candidate.key === route.page),
2901
+ ),
2902
+ ))
2623
2903
  }
2624
2904
  onNavigate={(href) => navigation.go(href)}
2625
2905
  onSignOut={viaToken ? undefined : () => void data.actions.signOut()}
@@ -2628,7 +2908,13 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2628
2908
  </components.Shell>
2629
2909
  );
2630
2910
  }
2631
- return <LanguageContext.Provider value={pack}>{content}</LanguageContext.Provider>;
2911
+ return (
2912
+ <LanguageContext.Provider value={pack}>
2913
+ <RemotePicturesContext.Provider value={pictureSrc}>
2914
+ {content}
2915
+ </RemotePicturesContext.Provider>
2916
+ </LanguageContext.Provider>
2917
+ );
2632
2918
  }
2633
2919
 
2634
2920
  return {