@skillcdn/console 0.4.1 → 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 (271) hide show
  1. package/README.md +19 -2
  2. package/SKILLCDN.md +7 -5
  3. package/dist/api.d.ts +4 -3
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +4 -3
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +543 -34
  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 +35 -2
  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 +97 -1
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +63 -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/plugin-frame.d.ts +52 -0
  48. package/dist/components/plugin-frame.d.ts.map +1 -0
  49. package/dist/components/plugin-frame.js +314 -0
  50. package/dist/components/plugin-frame.js.map +1 -0
  51. package/dist/components/plugin-settings.d.ts +38 -0
  52. package/dist/components/plugin-settings.d.ts.map +1 -0
  53. package/dist/components/plugin-settings.js +348 -0
  54. package/dist/components/plugin-settings.js.map +1 -0
  55. package/dist/components/shell.d.ts +2 -0
  56. package/dist/components/shell.d.ts.map +1 -1
  57. package/dist/components/shell.js +26 -1
  58. package/dist/components/shell.js.map +1 -1
  59. package/dist/components/workspace.d.ts +7 -1
  60. package/dist/components/workspace.d.ts.map +1 -1
  61. package/dist/components/workspace.js +1 -1
  62. package/dist/components/workspace.js.map +1 -1
  63. package/dist/console.css +327 -0
  64. package/dist/console.d.ts.map +1 -1
  65. package/dist/console.js +136 -26
  66. package/dist/console.js.map +1 -1
  67. package/dist/data.d.ts +7 -1
  68. package/dist/data.d.ts.map +1 -1
  69. package/dist/data.js +19 -2
  70. package/dist/data.js.map +1 -1
  71. package/dist/i18n/en.d.ts +149 -0
  72. package/dist/i18n/en.d.ts.map +1 -1
  73. package/dist/i18n/en.js +160 -0
  74. package/dist/i18n/en.js.map +1 -1
  75. package/dist/i18n/ko.d.ts.map +1 -1
  76. package/dist/i18n/ko.js +144 -0
  77. package/dist/i18n/ko.js.map +1 -1
  78. package/dist/index.d.ts +3 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +3 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/plugin/args.d.ts +148 -0
  83. package/dist/plugin/args.d.ts.map +1 -0
  84. package/dist/plugin/args.js +369 -0
  85. package/dist/plugin/args.js.map +1 -0
  86. package/dist/plugin/bridge.d.ts +146 -0
  87. package/dist/plugin/bridge.d.ts.map +1 -0
  88. package/dist/plugin/bridge.js +125 -0
  89. package/dist/plugin/bridge.js.map +1 -0
  90. package/dist/plugin/capabilities.d.ts +20 -0
  91. package/dist/plugin/capabilities.d.ts.map +1 -0
  92. package/dist/plugin/capabilities.js +133 -0
  93. package/dist/plugin/capabilities.js.map +1 -0
  94. package/dist/plugin/context.d.ts +374 -0
  95. package/dist/plugin/context.d.ts.map +1 -0
  96. package/dist/plugin/context.js +2 -0
  97. package/dist/plugin/context.js.map +1 -0
  98. package/dist/plugin/define.d.ts +208 -0
  99. package/dist/plugin/define.d.ts.map +1 -0
  100. package/dist/plugin/define.js +482 -0
  101. package/dist/plugin/define.js.map +1 -0
  102. package/dist/plugin/definition-error.d.ts +11 -0
  103. package/dist/plugin/definition-error.d.ts.map +1 -0
  104. package/dist/plugin/definition-error.js +17 -0
  105. package/dist/plugin/definition-error.js.map +1 -0
  106. package/dist/plugin/limits.d.ts +67 -0
  107. package/dist/plugin/limits.d.ts.map +1 -0
  108. package/dist/plugin/limits.js +70 -0
  109. package/dist/plugin/limits.js.map +1 -0
  110. package/dist/plugin/load-worker.d.ts +7 -0
  111. package/dist/plugin/load-worker.d.ts.map +1 -0
  112. package/dist/plugin/load-worker.js +41 -0
  113. package/dist/plugin/load-worker.js.map +1 -0
  114. package/dist/plugin/load.d.ts +18 -0
  115. package/dist/plugin/load.d.ts.map +1 -0
  116. package/dist/plugin/load.js +62 -0
  117. package/dist/plugin/load.js.map +1 -0
  118. package/dist/plugin/manifest.d.ts +237 -0
  119. package/dist/plugin/manifest.d.ts.map +1 -0
  120. package/dist/plugin/manifest.js +411 -0
  121. package/dist/plugin/manifest.js.map +1 -0
  122. package/dist/plugin/refusals.d.ts +49 -0
  123. package/dist/plugin/refusals.d.ts.map +1 -0
  124. package/dist/plugin/refusals.js +79 -0
  125. package/dist/plugin/refusals.js.map +1 -0
  126. package/dist/plugin/rest-schemas.d.ts +556 -0
  127. package/dist/plugin/rest-schemas.d.ts.map +1 -0
  128. package/dist/plugin/rest-schemas.js +262 -0
  129. package/dist/plugin/rest-schemas.js.map +1 -0
  130. package/dist/plugin/sandbox.d.ts +23 -0
  131. package/dist/plugin/sandbox.d.ts.map +1 -0
  132. package/dist/plugin/sandbox.js +99 -0
  133. package/dist/plugin/sandbox.js.map +1 -0
  134. package/dist/plugin/testing.d.ts +45 -0
  135. package/dist/plugin/testing.d.ts.map +1 -0
  136. package/dist/plugin/testing.js +420 -0
  137. package/dist/plugin/testing.js.map +1 -0
  138. package/dist/plugin/web-react.d.ts +11 -0
  139. package/dist/plugin/web-react.d.ts.map +1 -0
  140. package/dist/plugin/web-react.js +34 -0
  141. package/dist/plugin/web-react.js.map +1 -0
  142. package/dist/plugin/web.d.ts +95 -0
  143. package/dist/plugin/web.d.ts.map +1 -0
  144. package/dist/plugin/web.js +245 -0
  145. package/dist/plugin/web.js.map +1 -0
  146. package/dist/plugin-nav.d.ts +29 -0
  147. package/dist/plugin-nav.d.ts.map +1 -0
  148. package/dist/plugin-nav.js +92 -0
  149. package/dist/plugin-nav.js.map +1 -0
  150. package/dist/plugin-testing.d.ts +2 -0
  151. package/dist/plugin-testing.d.ts.map +1 -0
  152. package/dist/plugin-testing.js +4 -0
  153. package/dist/plugin-testing.js.map +1 -0
  154. package/dist/plugin-web-react.d.ts +3 -0
  155. package/dist/plugin-web-react.d.ts.map +1 -0
  156. package/dist/plugin-web-react.js +3 -0
  157. package/dist/plugin-web-react.js.map +1 -0
  158. package/dist/plugin-web.d.ts +4 -0
  159. package/dist/plugin-web.d.ts.map +1 -0
  160. package/dist/plugin-web.js +5 -0
  161. package/dist/plugin-web.js.map +1 -0
  162. package/dist/plugin.d.ts +11 -0
  163. package/dist/plugin.d.ts.map +1 -0
  164. package/dist/plugin.js +15 -0
  165. package/dist/plugin.js.map +1 -0
  166. package/dist/router.d.ts +28 -0
  167. package/dist/router.d.ts.map +1 -1
  168. package/dist/router.js +47 -0
  169. package/dist/router.js.map +1 -1
  170. package/dist/routes.d.ts +4 -0
  171. package/dist/routes.d.ts.map +1 -1
  172. package/dist/routes.js +6 -0
  173. package/dist/routes.js.map +1 -1
  174. package/dist/schemas.d.ts +117 -0
  175. package/dist/schemas.d.ts.map +1 -1
  176. package/dist/schemas.js +40 -1
  177. package/dist/schemas.js.map +1 -1
  178. package/dist/tokens.d.ts +2 -0
  179. package/dist/tokens.d.ts.map +1 -0
  180. package/dist/tokens.js +101 -0
  181. package/dist/tokens.js.map +1 -0
  182. package/dist/vocabulary.d.ts +11 -1
  183. package/dist/vocabulary.d.ts.map +1 -1
  184. package/dist/vocabulary.js +24 -0
  185. package/dist/vocabulary.js.map +1 -1
  186. package/dist/web.d.ts.map +1 -1
  187. package/dist/web.js +2 -0
  188. package/dist/web.js.map +1 -1
  189. package/docs/specs/cli.md +11 -4
  190. package/docs/specs/plugins.md +487 -0
  191. package/docs/specs/rest.md +3 -2
  192. package/package.json +20 -2
  193. package/skills/building-a-console/SKILL.md +25 -26
  194. package/skills/building-a-console/references/configuring.md +40 -0
  195. package/skills/building-a-console/references/the-default-console.md +1 -1
  196. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  197. package/skills/building-a-plugin/SKILL.md +137 -0
  198. package/skills/deploying-a-console/SKILL.md +24 -26
  199. package/skills/deploying-a-console/references/access.md +13 -9
  200. package/skills/deploying-a-console/references/resources.md +25 -10
  201. package/skills/deploying-a-console/references/running-it-again.md +15 -0
  202. package/skills/deploying-a-console/references/sizing.md +2 -0
  203. package/skills/planning-the-board/SKILL.md +96 -0
  204. package/skills/planning-the-board/references/a-task.md +36 -0
  205. package/skills/planning-the-board/references/setting-up.md +44 -0
  206. package/skills/working-the-board/SKILL.md +43 -34
  207. package/skills/working-the-board/references/the-pack.md +19 -0
  208. package/src/api.ts +8 -0
  209. package/src/cli/cli.ts +697 -33
  210. package/src/cli/format.ts +356 -9
  211. package/src/cli/help.ts +41 -2
  212. package/src/cli/plugin-check.ts +470 -0
  213. package/src/cli/plugin-cli.ts +148 -0
  214. package/src/cli/plugin-init.ts +90 -0
  215. package/src/cli/plugin-main.ts +30 -0
  216. package/src/client.ts +294 -0
  217. package/src/compat.ts +2 -2
  218. package/src/components/board.tsx +62 -14
  219. package/src/components/event-feed.tsx +42 -1
  220. package/src/components/plugin-frame.tsx +437 -0
  221. package/src/components/plugin-settings.tsx +1111 -0
  222. package/src/components/shell.tsx +82 -14
  223. package/src/components/workspace.tsx +18 -0
  224. package/src/console.tsx +286 -19
  225. package/src/data.ts +27 -2
  226. package/src/i18n/en.ts +168 -0
  227. package/src/i18n/ko.ts +148 -0
  228. package/src/index.ts +12 -0
  229. package/src/plugin/args.ts +526 -0
  230. package/src/plugin/bridge.ts +314 -0
  231. package/src/plugin/capabilities.ts +150 -0
  232. package/src/plugin/context.ts +416 -0
  233. package/src/plugin/define.ts +792 -0
  234. package/src/plugin/definition-error.ts +19 -0
  235. package/src/plugin/limits.ts +70 -0
  236. package/src/plugin/load-worker.ts +50 -0
  237. package/src/plugin/load.ts +82 -0
  238. package/src/plugin/manifest.ts +561 -0
  239. package/src/plugin/refusals.ts +94 -0
  240. package/src/plugin/rest-schemas.ts +354 -0
  241. package/src/plugin/sandbox.ts +123 -0
  242. package/src/plugin/testing.ts +612 -0
  243. package/src/plugin/web-react.ts +45 -0
  244. package/src/plugin/web.ts +403 -0
  245. package/src/plugin-nav.ts +132 -0
  246. package/src/plugin-testing.ts +10 -0
  247. package/src/plugin-web-react.ts +4 -0
  248. package/src/plugin-web.ts +35 -0
  249. package/src/plugin.ts +126 -0
  250. package/src/router.ts +73 -0
  251. package/src/routes.ts +7 -0
  252. package/src/schemas.ts +43 -0
  253. package/src/styles/console.css +327 -0
  254. package/src/tokens.ts +101 -0
  255. package/src/vocabulary.ts +30 -0
  256. package/src/web.ts +2 -0
  257. package/template/plugin/.gitignore +2 -0
  258. package/template/plugin/README.md +26 -0
  259. package/template/plugin/SKILLCDN.md +13 -0
  260. package/template/plugin/build.mjs +40 -0
  261. package/template/plugin/console-plugin.json +50 -0
  262. package/template/plugin/package.json +27 -0
  263. package/template/plugin/skills/__key__/SKILL.md +23 -0
  264. package/template/plugin/src/check.test.ts +34 -0
  265. package/template/plugin/src/server/index.test.ts +41 -0
  266. package/template/plugin/src/server/index.ts +72 -0
  267. package/template/plugin/src/web/index.css +51 -0
  268. package/template/plugin/src/web/index.ts +115 -0
  269. package/template/plugin/tsconfig.json +17 -0
  270. package/skills/working-the-board/references/setting-up.md +0 -3
  271. /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
@@ -13,6 +13,8 @@ export interface NavItem {
13
13
  readonly current: boolean;
14
14
  /** A count beside the label: the decisions waiting, for example. */
15
15
  readonly count?: number | undefined;
16
+ /** The pages the tab opens a list of, as a group of plugins does (ADR-0054); the tab itself leads to the first. */
17
+ readonly children?: readonly NavItem[] | undefined;
16
18
  }
17
19
 
18
20
  /** The addresses of a brand's marks (ADR-0013): shown beside the workspace's name, and above it when signing in. */
@@ -67,6 +69,68 @@ export function isPlainClick(event: {
67
69
  return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
68
70
  }
69
71
 
72
+ /** A tab that opens a list of pages, as a group of several plugins does: a details, as the person's menu is. Closes once used. */
73
+ function TabGroup(props: { readonly item: NavItem; readonly onNavigate: (href: string) => void }) {
74
+ const details = useRef<HTMLDetailsElement>(null);
75
+ useEffect(() => {
76
+ const onClick = (event: MouseEvent) => {
77
+ const element = details.current;
78
+ if (
79
+ element?.open === true &&
80
+ !(event.target instanceof Node && element.contains(event.target))
81
+ ) {
82
+ element.open = false;
83
+ }
84
+ };
85
+ document.addEventListener("click", onClick);
86
+ return () => document.removeEventListener("click", onClick);
87
+ }, []);
88
+ const { item } = props;
89
+ return (
90
+ <details className="sc-tab-group" ref={details}>
91
+ <summary
92
+ className={cx("sc-tab", "sc-tab-summary", item.current && "sc-tab-current")}
93
+ aria-current={item.current ? "page" : undefined}
94
+ >
95
+ {item.label}
96
+ <svg className="sc-menu-caret" viewBox="0 0 16 16" aria-hidden="true">
97
+ <path
98
+ d="M4 6l4 4 4-4"
99
+ fill="none"
100
+ stroke="currentColor"
101
+ strokeWidth="1.5"
102
+ strokeLinecap="round"
103
+ strokeLinejoin="round"
104
+ />
105
+ </svg>
106
+ </summary>
107
+ <div className="sc-menu-panel sc-tab-group-panel">
108
+ <div className="sc-menu-list">
109
+ {(item.children ?? []).map((child) => (
110
+ <a
111
+ key={child.href}
112
+ href={child.href}
113
+ className="sc-menu-link"
114
+ aria-current={child.current ? "page" : undefined}
115
+ onClick={(event) => {
116
+ if (isPlainClick(event)) {
117
+ event.preventDefault();
118
+ if (details.current !== null) {
119
+ details.current.open = false;
120
+ }
121
+ props.onNavigate(child.href);
122
+ }
123
+ }}
124
+ >
125
+ {child.label}
126
+ </a>
127
+ ))}
128
+ </div>
129
+ </div>
130
+ </details>
131
+ );
132
+ }
133
+
70
134
  /** The person's menu: who they are, the workspace's pages, the languages, and the way out. Closes once used. */
71
135
  function PersonMenu(props: {
72
136
  readonly person: RestPerson;
@@ -296,20 +360,24 @@ export function Shell(props: ShellProps) {
296
360
  {props.nav.length > 0 && (
297
361
  <nav ref={tabs} className="sc-tabs" aria-label={words.shell.pages}>
298
362
  <div className="sc-tabs-inner">
299
- {props.nav.map((item) => (
300
- <a
301
- key={item.href}
302
- href={item.href}
303
- className={cx("sc-tab", item.current && "sc-tab-current")}
304
- aria-current={item.current ? "page" : undefined}
305
- onClick={follow(item.href)}
306
- >
307
- {item.label}
308
- {item.count !== undefined && item.count > 0 && (
309
- <span className="sc-nav-count">{item.count}</span>
310
- )}
311
- </a>
312
- ))}
363
+ {props.nav.map((item) =>
364
+ item.children !== undefined && item.children.length > 0 ? (
365
+ <TabGroup key={item.href} item={item} onNavigate={props.onNavigate} />
366
+ ) : (
367
+ <a
368
+ key={item.href}
369
+ href={item.href}
370
+ className={cx("sc-tab", item.current && "sc-tab-current")}
371
+ aria-current={item.current ? "page" : undefined}
372
+ onClick={follow(item.href)}
373
+ >
374
+ {item.label}
375
+ {item.count !== undefined && item.count > 0 && (
376
+ <span className="sc-nav-count">{item.count}</span>
377
+ )}
378
+ </a>
379
+ ),
380
+ )}
313
381
  </div>
314
382
  </nav>
315
383
  )}
@@ -2,6 +2,7 @@ import { type FormEvent, useId, useMemo, useState } from "react";
2
2
  import {
3
3
  PEOPLE_VISIBILITIES,
4
4
  type PeopleVisibility,
5
+ type RestMe,
5
6
  type RestWorkspace,
6
7
  type RestWorkspacePatch,
7
8
  WEEK_STARTS,
@@ -117,6 +118,12 @@ const labelOf = (zone: TimeZoneOption): string =>
117
118
 
118
119
  export interface WorkspaceSettingsViewProps {
119
120
  readonly workspace: RestWorkspace;
121
+ /**
122
+ * The console's version with the range of command versions it takes, as `GET /api/v1/me`
123
+ * says them (ADR-0025): shown first, so that whoever keeps the agents' commands up to date
124
+ * reads here which console they must match. Left out, nothing is said.
125
+ */
126
+ readonly console?: RestMe["console"];
120
127
  }
121
128
 
122
129
  /** The settings as they stand, one row each; where they are changed is the page's business. */
@@ -128,6 +135,17 @@ export function WorkspaceSettingsView(props: WorkspaceSettingsViewProps) {
128
135
  const offset = zone === undefined ? undefined : offsetOf(zone, new Date());
129
136
  return (
130
137
  <dl className="sc-settings-list">
138
+ {props.console !== undefined && (
139
+ <div className="sc-settings-item">
140
+ <dt>{settings.consoleVersion}</dt>
141
+ <dd>
142
+ {props.console.version}
143
+ <span className="sc-settings-note">
144
+ {settings.consoleTakes(props.console.clientRange)}
145
+ </span>
146
+ </dd>
147
+ </div>
148
+ )}
131
149
  <div className="sc-settings-item">
132
150
  <dt>{settings.peopleVisibility}</dt>
133
151
  <dd>{words.vocabulary.peopleVisibility[peopleVisibilityOf(props.workspace)]}</dd>
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,
@@ -104,6 +107,7 @@ import {
104
107
  type Messages,
105
108
  useWords,
106
109
  } from "./i18n/index.js";
110
+ import { pluginNavItems, pluginPagesAt, pluginWordOf } from "./plugin-nav.js";
107
111
  import {
108
112
  boardHref,
109
113
  calendarHref,
@@ -121,6 +125,8 @@ import {
121
125
  PATHS,
122
126
  personAgentsHref,
123
127
  personWorkHref,
128
+ pluginHref,
129
+ pluginSettingsHref,
124
130
  projectHref,
125
131
  projectSettingsHref,
126
132
  type Route,
@@ -393,11 +399,27 @@ function ProjectsPage(props: {
393
399
  readonly data: ConsoleData;
394
400
  readonly components: ConsoleComponents;
395
401
  readonly navigation: Navigation;
402
+ readonly client: ConsoleClient;
403
+ readonly language: string;
396
404
  /** Whether the form for a new project is open: the page is at its address. */
397
405
  readonly making: boolean;
398
406
  }) {
399
407
  const words = useWords().projects;
400
- const { data, components, navigation } = 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
+ );
401
423
  const { busy, error, act } = useAction();
402
424
  const href = (project: RestProject) => projectHref(project.key);
403
425
  // A project is made by an administrator (ADR-0045): a member sees the list alone, the form's
@@ -415,6 +437,7 @@ function ProjectsPage(props: {
415
437
  )}
416
438
  </div>
417
439
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
440
+ {widgets("home:top")}
418
441
  {making && (
419
442
  <section className="sc-panel sc-panel-form" aria-label={words.form.aria}>
420
443
  <ProjectForm
@@ -450,10 +473,150 @@ function ProjectsPage(props: {
450
473
  />
451
474
  }
452
475
  />
476
+ {widgets("home:bottom")}
477
+ </>
478
+ );
479
+ }
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
+ />
453
537
  </>
454
538
  );
455
539
  }
456
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
+
457
620
  /**
458
621
  * The archive of the board (ADR-0029): the tasks put away, newest first, a page at a time from
459
622
  * the server, each brought back from here by whoever may work in the project.
@@ -747,6 +910,8 @@ function ProjectPage(props: {
747
910
  readonly data: ConsoleData;
748
911
  readonly components: ConsoleComponents;
749
912
  readonly navigation: Navigation;
913
+ readonly client: ConsoleClient;
914
+ readonly language: string;
750
915
  }) {
751
916
  const words = useWords();
752
917
  const { route, project, data, components, navigation } = props;
@@ -1313,6 +1478,18 @@ function ProjectPage(props: {
1313
1478
  <ProjectSettingsPage route={route} project={project} data={data} navigation={navigation} />
1314
1479
  );
1315
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
+ }
1316
1493
  return <NothingHere />;
1317
1494
  }
1318
1495
 
@@ -1578,9 +1755,10 @@ function WorkspaceSettingsPage(props: {
1578
1755
  readonly form: "rules" | "workspace" | "pack-token" | undefined;
1579
1756
  readonly data: ConsoleData;
1580
1757
  readonly navigation: Navigation;
1758
+ readonly language: string;
1581
1759
  }) {
1582
1760
  const words = useWords();
1583
- const { form, data, navigation } = props;
1761
+ const { form, data, navigation, language } = props;
1584
1762
  const { busy, error, act } = useAction();
1585
1763
  const administrator = data.me?.person?.role === "admin";
1586
1764
  const workspace = data.me?.workspace;
@@ -1729,7 +1907,7 @@ function WorkspaceSettingsPage(props: {
1729
1907
  </a>
1730
1908
  )}
1731
1909
  </div>
1732
- <WorkspaceSettingsView workspace={workspace} />
1910
+ <WorkspaceSettingsView workspace={workspace} console={data.me?.console} />
1733
1911
  </section>
1734
1912
  )}
1735
1913
  <section className="sc-panel" aria-label={words.rules.title}>
@@ -1753,6 +1931,20 @@ function WorkspaceSettingsPage(props: {
1753
1931
  />
1754
1932
  )}
1755
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
+ )}
1756
1948
  <section className="sc-panel" aria-label={words.settings.categories}>
1757
1949
  <h2 className="sc-section-title">{words.settings.categories}</h2>
1758
1950
  {data.categories.length > 0 && (
@@ -2174,11 +2366,36 @@ function WorkspacePage(props: {
2174
2366
  readonly data: ConsoleData;
2175
2367
  readonly components: ConsoleComponents;
2176
2368
  readonly navigation: Navigation;
2369
+ readonly client: ConsoleClient;
2370
+ readonly language: string;
2177
2371
  }) {
2178
2372
  const words = useWords();
2179
- const { route, data, components, navigation } = props;
2373
+ const { route, data, components, navigation, client, language } = props;
2180
2374
  const [fresh, setFresh] = useState<RestTokenCreated | undefined>(undefined);
2181
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
+ }
2182
2399
 
2183
2400
  if (route.name === "work") {
2184
2401
  return <WorkPage data={data} components={components} week={route.week} />;
@@ -2190,7 +2407,14 @@ function WorkspacePage(props: {
2190
2407
  return <PersonWorkPage id={route.id} data={data} components={components} week={route.week} />;
2191
2408
  }
2192
2409
  if (route.name === "workspace-settings") {
2193
- 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
+ );
2194
2418
  }
2195
2419
  if (route.name === "categories") {
2196
2420
  const me = data.me?.person ?? undefined;
@@ -2453,13 +2677,6 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2453
2677
  current: route.name === "agents" || route.name === "new-token",
2454
2678
  },
2455
2679
  ]),
2456
- // The settings are read by everyone: the rules every agent follows, the categories and
2457
- // what the colours mean, where the skills come from; kept by an administrator.
2458
- {
2459
- href: PATHS.settings,
2460
- label: words.nav.settings,
2461
- current: route.name === "workspace-settings" || route.name === "categories",
2462
- },
2463
2680
  ...pages
2464
2681
  .filter((candidate) => candidate.level === "workspace")
2465
2682
  .map((candidate) => ({
@@ -2470,6 +2687,28 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2470
2687
  route.level === "workspace" &&
2471
2688
  route.page === candidate.key,
2472
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
+ },
2473
2712
  ];
2474
2713
  const nav: NavItem[] =
2475
2714
  projectKey === undefined
@@ -2521,12 +2760,6 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2521
2760
  label: words.nav.members,
2522
2761
  current: route.name === "members",
2523
2762
  },
2524
- // Read by every member: what the project is, and what its agents are asked (ADR-0023).
2525
- {
2526
- href: projectHref(projectKey, "settings"),
2527
- label: words.nav.settings,
2528
- current: route.name === "settings",
2529
- },
2530
2763
  ...pages
2531
2764
  .filter((candidate) => candidate.level === "project")
2532
2765
  .map((candidate) => ({
@@ -2537,6 +2770,24 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2537
2770
  route.level === "project" &&
2538
2771
  route.page === candidate.key,
2539
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
+ },
2540
2791
  ];
2541
2792
  /** A page of the console's own at the address, or nothing there. */
2542
2793
  const customPageOf = (key: string, level: CustomPage["level"]): ReactNode => {
@@ -2562,6 +2813,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2562
2813
  data={data}
2563
2814
  components={components}
2564
2815
  navigation={navigation}
2816
+ client={client}
2817
+ language={pack.tag}
2565
2818
  making={route.name === "new-project"}
2566
2819
  />
2567
2820
  );
@@ -2574,6 +2827,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2574
2827
  data={data}
2575
2828
  components={components}
2576
2829
  navigation={navigation}
2830
+ client={client}
2831
+ language={pack.tag}
2577
2832
  />
2578
2833
  );
2579
2834
  } else if (data.projectError !== undefined) {
@@ -2600,6 +2855,8 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2600
2855
  data={data}
2601
2856
  components={components}
2602
2857
  navigation={navigation}
2858
+ client={client}
2859
+ language={pack.tag}
2603
2860
  />
2604
2861
  );
2605
2862
  }
@@ -2632,7 +2889,17 @@ export function createConsole(config: ConsoleConfig = {}): ConsoleApp {
2632
2889
  route.name === "calendar" ||
2633
2890
  (route.name === "goal" && route.form === undefined) ||
2634
2891
  (route.name === "custom" &&
2635
- 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
+ ))
2636
2903
  }
2637
2904
  onNavigate={(href) => navigation.go(href)}
2638
2905
  onSignOut={viaToken ? undefined : () => void data.actions.signOut()}