@skillcdn/console 0.4.3 → 0.4.5

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 (268) hide show
  1. package/README.md +6 -6
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +255 -33
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +20 -5
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +98 -26
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +39 -15
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/plugin-check.d.ts +5 -1
  17. package/dist/cli/plugin-check.d.ts.map +1 -1
  18. package/dist/cli/plugin-check.js +23 -4
  19. package/dist/cli/plugin-check.js.map +1 -1
  20. package/dist/cli/plugin-cli.d.ts.map +1 -1
  21. package/dist/cli/plugin-cli.js +10 -1
  22. package/dist/cli/plugin-cli.js.map +1 -1
  23. package/dist/cli/plugin-init.d.ts +2 -0
  24. package/dist/cli/plugin-init.d.ts.map +1 -1
  25. package/dist/cli/plugin-init.js +6 -0
  26. package/dist/cli/plugin-init.js.map +1 -1
  27. package/dist/client.d.ts +32 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +14 -2
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.js +1 -1
  32. package/dist/components/board.d.ts +8 -0
  33. package/dist/components/board.d.ts.map +1 -1
  34. package/dist/components/board.js +49 -18
  35. package/dist/components/board.js.map +1 -1
  36. package/dist/components/decision-form.d.ts +6 -3
  37. package/dist/components/decision-form.d.ts.map +1 -1
  38. package/dist/components/decision-form.js +24 -18
  39. package/dist/components/decision-form.js.map +1 -1
  40. package/dist/components/decision-list.d.ts +5 -1
  41. package/dist/components/decision-list.d.ts.map +1 -1
  42. package/dist/components/decision-list.js +25 -5
  43. package/dist/components/decision-list.js.map +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +34 -8
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/goals.d.ts.map +1 -1
  48. package/dist/components/goals.js +2 -2
  49. package/dist/components/goals.js.map +1 -1
  50. package/dist/components/home.d.ts +35 -0
  51. package/dist/components/home.d.ts.map +1 -0
  52. package/dist/components/home.js +326 -0
  53. package/dist/components/home.js.map +1 -0
  54. package/dist/components/my-decisions.d.ts +20 -3
  55. package/dist/components/my-decisions.d.ts.map +1 -1
  56. package/dist/components/my-decisions.js +14 -1
  57. package/dist/components/my-decisions.js.map +1 -1
  58. package/dist/components/pack-connect.d.ts.map +1 -1
  59. package/dist/components/pack-connect.js +5 -4
  60. package/dist/components/pack-connect.js.map +1 -1
  61. package/dist/components/people.d.ts +45 -1
  62. package/dist/components/people.d.ts.map +1 -1
  63. package/dist/components/people.js +125 -4
  64. package/dist/components/people.js.map +1 -1
  65. package/dist/components/picker.d.ts +8 -0
  66. package/dist/components/picker.d.ts.map +1 -1
  67. package/dist/components/picker.js +2 -2
  68. package/dist/components/picker.js.map +1 -1
  69. package/dist/components/plugin-frame.d.ts +3 -13
  70. package/dist/components/plugin-frame.d.ts.map +1 -1
  71. package/dist/components/plugin-frame.js +75 -21
  72. package/dist/components/plugin-frame.js.map +1 -1
  73. package/dist/components/plugin-settings.d.ts +5 -1
  74. package/dist/components/plugin-settings.d.ts.map +1 -1
  75. package/dist/components/plugin-settings.js +283 -45
  76. package/dist/components/plugin-settings.js.map +1 -1
  77. package/dist/components/projects.d.ts +0 -8
  78. package/dist/components/projects.d.ts.map +1 -1
  79. package/dist/components/projects.js +4 -14
  80. package/dist/components/projects.js.map +1 -1
  81. package/dist/components/searches.d.ts +11 -0
  82. package/dist/components/searches.d.ts.map +1 -0
  83. package/dist/components/searches.js +30 -0
  84. package/dist/components/searches.js.map +1 -0
  85. package/dist/components/task-form.d.ts.map +1 -1
  86. package/dist/components/task-form.js +6 -5
  87. package/dist/components/task-form.js.map +1 -1
  88. package/dist/components/task-view.d.ts.map +1 -1
  89. package/dist/components/task-view.js +2 -1
  90. package/dist/components/task-view.js.map +1 -1
  91. package/dist/components/ui.d.ts +16 -0
  92. package/dist/components/ui.d.ts.map +1 -1
  93. package/dist/components/ui.js +15 -2
  94. package/dist/components/ui.js.map +1 -1
  95. package/dist/connect-page.d.ts.map +1 -1
  96. package/dist/connect-page.js +5 -6
  97. package/dist/connect-page.js.map +1 -1
  98. package/dist/console.css +1123 -258
  99. package/dist/console.d.ts +12 -4
  100. package/dist/console.d.ts.map +1 -1
  101. package/dist/console.js +123 -27
  102. package/dist/console.js.map +1 -1
  103. package/dist/data.d.ts +18 -3
  104. package/dist/data.d.ts.map +1 -1
  105. package/dist/data.js +41 -1
  106. package/dist/data.js.map +1 -1
  107. package/dist/docs-page.d.ts.map +1 -1
  108. package/dist/docs-page.js +6 -5
  109. package/dist/docs-page.js.map +1 -1
  110. package/dist/home-tiles.d.ts +92 -0
  111. package/dist/home-tiles.d.ts.map +1 -0
  112. package/dist/home-tiles.js +294 -0
  113. package/dist/home-tiles.js.map +1 -0
  114. package/dist/i18n/en.d.ts +264 -1
  115. package/dist/i18n/en.d.ts.map +1 -1
  116. package/dist/i18n/en.js +260 -1
  117. package/dist/i18n/en.js.map +1 -1
  118. package/dist/i18n/index.d.ts +4 -2
  119. package/dist/i18n/index.d.ts.map +1 -1
  120. package/dist/i18n/index.js +10 -11
  121. package/dist/i18n/index.js.map +1 -1
  122. package/dist/i18n/ko.d.ts.map +1 -1
  123. package/dist/i18n/ko.js +255 -4
  124. package/dist/i18n/ko.js.map +1 -1
  125. package/dist/index.d.ts +7 -5
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +7 -5
  128. package/dist/index.js.map +1 -1
  129. package/dist/limits.d.ts +12 -0
  130. package/dist/limits.d.ts.map +1 -1
  131. package/dist/limits.js +12 -0
  132. package/dist/limits.js.map +1 -1
  133. package/dist/plugin/args.d.ts +6 -1
  134. package/dist/plugin/args.d.ts.map +1 -1
  135. package/dist/plugin/args.js +16 -11
  136. package/dist/plugin/args.js.map +1 -1
  137. package/dist/plugin/bridge.d.ts +63 -3
  138. package/dist/plugin/bridge.d.ts.map +1 -1
  139. package/dist/plugin/bridge.js +54 -0
  140. package/dist/plugin/bridge.js.map +1 -1
  141. package/dist/plugin/capabilities.d.ts +7 -1
  142. package/dist/plugin/capabilities.d.ts.map +1 -1
  143. package/dist/plugin/capabilities.js +24 -1
  144. package/dist/plugin/capabilities.js.map +1 -1
  145. package/dist/plugin/context.d.ts +174 -12
  146. package/dist/plugin/context.d.ts.map +1 -1
  147. package/dist/plugin/define.d.ts +14 -1
  148. package/dist/plugin/define.d.ts.map +1 -1
  149. package/dist/plugin/define.js +18 -2
  150. package/dist/plugin/define.js.map +1 -1
  151. package/dist/plugin/limits.d.ts +9 -2
  152. package/dist/plugin/limits.d.ts.map +1 -1
  153. package/dist/plugin/limits.js +9 -2
  154. package/dist/plugin/limits.js.map +1 -1
  155. package/dist/plugin/manifest.d.ts +98 -6
  156. package/dist/plugin/manifest.d.ts.map +1 -1
  157. package/dist/plugin/manifest.js +171 -10
  158. package/dist/plugin/manifest.js.map +1 -1
  159. package/dist/plugin/rest-schemas.d.ts +120 -0
  160. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  161. package/dist/plugin/rest-schemas.js +78 -5
  162. package/dist/plugin/rest-schemas.js.map +1 -1
  163. package/dist/plugin/sandbox.d.ts +4 -3
  164. package/dist/plugin/sandbox.d.ts.map +1 -1
  165. package/dist/plugin/sandbox.js +33 -6
  166. package/dist/plugin/sandbox.js.map +1 -1
  167. package/dist/plugin/semver.d.ts +26 -0
  168. package/dist/plugin/semver.d.ts.map +1 -0
  169. package/dist/plugin/semver.js +76 -0
  170. package/dist/plugin/semver.js.map +1 -0
  171. package/dist/plugin/testing.d.ts +15 -1
  172. package/dist/plugin/testing.d.ts.map +1 -1
  173. package/dist/plugin/testing.js +42 -5
  174. package/dist/plugin/testing.js.map +1 -1
  175. package/dist/plugin/web.d.ts +16 -1
  176. package/dist/plugin/web.d.ts.map +1 -1
  177. package/dist/plugin/web.js +44 -1
  178. package/dist/plugin/web.js.map +1 -1
  179. package/dist/plugin.d.ts +3 -2
  180. package/dist/plugin.d.ts.map +1 -1
  181. package/dist/plugin.js +3 -2
  182. package/dist/plugin.js.map +1 -1
  183. package/dist/router.d.ts +19 -0
  184. package/dist/router.d.ts.map +1 -1
  185. package/dist/router.js +18 -0
  186. package/dist/router.js.map +1 -1
  187. package/dist/routes.d.ts +11 -2
  188. package/dist/routes.d.ts.map +1 -1
  189. package/dist/routes.js +14 -1
  190. package/dist/routes.js.map +1 -1
  191. package/dist/rules.d.ts +2 -2
  192. package/dist/rules.js +3 -3
  193. package/dist/schemas.d.ts +864 -2
  194. package/dist/schemas.d.ts.map +1 -1
  195. package/dist/schemas.js +156 -9
  196. package/dist/schemas.js.map +1 -1
  197. package/dist/use-action.d.ts +8 -2
  198. package/dist/use-action.d.ts.map +1 -1
  199. package/dist/use-action.js +13 -6
  200. package/dist/use-action.js.map +1 -1
  201. package/dist/vocabulary.d.ts +3 -1
  202. package/dist/vocabulary.d.ts.map +1 -1
  203. package/dist/vocabulary.js +9 -0
  204. package/dist/vocabulary.js.map +1 -1
  205. package/docs/specs/cli.md +10 -6
  206. package/docs/specs/plugins.md +83 -62
  207. package/docs/specs/rest.md +34 -16
  208. package/package.json +1 -1
  209. package/skills/building-a-plugin/SKILL.md +15 -12
  210. package/skills/planning-the-board/references/setting-up.md +2 -1
  211. package/skills/working-the-board/SKILL.md +3 -3
  212. package/src/api.ts +23 -0
  213. package/src/cli/cli.ts +323 -30
  214. package/src/cli/format.ts +122 -26
  215. package/src/cli/help.ts +43 -14
  216. package/src/cli/plugin-check.ts +41 -2
  217. package/src/cli/plugin-cli.ts +10 -1
  218. package/src/cli/plugin-init.ts +8 -0
  219. package/src/client.ts +78 -0
  220. package/src/compat.ts +1 -1
  221. package/src/components/board.tsx +86 -45
  222. package/src/components/decision-form.tsx +73 -36
  223. package/src/components/decision-list.tsx +45 -13
  224. package/src/components/event-feed.tsx +40 -7
  225. package/src/components/goals.tsx +4 -1
  226. package/src/components/home.tsx +615 -0
  227. package/src/components/my-decisions.tsx +50 -5
  228. package/src/components/pack-connect.tsx +5 -4
  229. package/src/components/people.tsx +500 -61
  230. package/src/components/picker.tsx +11 -2
  231. package/src/components/plugin-frame.tsx +85 -63
  232. package/src/components/plugin-settings.tsx +735 -185
  233. package/src/components/projects.tsx +14 -69
  234. package/src/components/searches.ts +47 -0
  235. package/src/components/task-form.tsx +23 -28
  236. package/src/components/task-view.tsx +7 -0
  237. package/src/components/ui.tsx +42 -2
  238. package/src/connect-page.tsx +5 -6
  239. package/src/console.tsx +303 -64
  240. package/src/data.ts +62 -3
  241. package/src/docs-page.tsx +6 -5
  242. package/src/home-tiles.ts +438 -0
  243. package/src/i18n/en.ts +274 -0
  244. package/src/i18n/index.ts +17 -14
  245. package/src/i18n/ko.ts +267 -4
  246. package/src/index.ts +18 -5
  247. package/src/limits.ts +12 -0
  248. package/src/plugin/args.ts +16 -11
  249. package/src/plugin/bridge.ts +122 -1
  250. package/src/plugin/capabilities.ts +28 -1
  251. package/src/plugin/context.ts +186 -9
  252. package/src/plugin/define.ts +38 -1
  253. package/src/plugin/limits.ts +9 -2
  254. package/src/plugin/manifest.ts +228 -13
  255. package/src/plugin/rest-schemas.ts +89 -6
  256. package/src/plugin/sandbox.ts +36 -8
  257. package/src/plugin/semver.ts +89 -0
  258. package/src/plugin/testing.ts +63 -6
  259. package/src/plugin/web.ts +66 -0
  260. package/src/plugin.ts +19 -0
  261. package/src/router.ts +25 -0
  262. package/src/routes.ts +17 -2
  263. package/src/rules.ts +3 -3
  264. package/src/schemas.ts +197 -10
  265. package/src/styles/console.css +1123 -258
  266. package/src/use-action.ts +16 -7
  267. package/src/vocabulary.ts +10 -0
  268. package/template/plugin/console-plugin.json +3 -1
package/src/client.ts CHANGED
@@ -3,6 +3,8 @@ import {
3
3
  CLAIM_PATH,
4
4
  categoryPath,
5
5
  connectPath,
6
+ decisionFilePath,
7
+ decisionPath,
6
8
  personTokensPath,
7
9
  personWorkPath,
8
10
  pluginPath,
@@ -73,6 +75,7 @@ import {
73
75
  type RestProjectPatch,
74
76
  type RestProjectRules,
75
77
  type RestProjects,
78
+ type RestReachedDecision,
76
79
  type RestReportInput,
77
80
  type RestRules,
78
81
  type RestRulesInput,
@@ -89,6 +92,10 @@ import {
89
92
  type RestTaskInput,
90
93
  type RestTaskPatch,
91
94
  type RestTasks,
95
+ type RestTeam,
96
+ type RestTeamInput,
97
+ type RestTeamPatch,
98
+ type RestTeams,
92
99
  type RestTokenCreated,
93
100
  type RestTokenInput,
94
101
  type RestTokens,
@@ -137,6 +144,7 @@ import {
137
144
  restProjectRulesSchema,
138
145
  restProjectSchema,
139
146
  restProjectsSchema,
147
+ restReachedDecisionSchema,
140
148
  restRulesSchema,
141
149
  restRunSchema,
142
150
  restRunsSchema,
@@ -147,6 +155,8 @@ import {
147
155
  restSkillsSchema,
148
156
  restTaskSchema,
149
157
  restTasksSchema,
158
+ restTeamSchema,
159
+ restTeamsSchema,
150
160
  restTokenCreatedSchema,
151
161
  restTokensSchema,
152
162
  restVersionSchema,
@@ -498,6 +508,8 @@ export interface PluginClient {
498
508
  fileUrl(fileId: string): string;
499
509
  /** The bytes of a record's file, with the name and the type the console says. */
500
510
  readFile(fileId: string, signal?: AbortSignal): Promise<FileRead>;
511
+ /** A person's picture, for a frame, which names the person and never an address (ADR-0063). */
512
+ personPicture(personId: string, signal?: AbortSignal): Promise<FileRead>;
501
513
  /** Where the frame of a page or a widget is (ADR-0054), on a project when its key is given. */
502
514
  frameUrl(options: {
503
515
  readonly page?: string | undefined;
@@ -543,6 +555,8 @@ export interface PluginClient {
543
555
  disable(): Promise<RestPluginDetail>;
544
556
  /** Changes the settings' values, without secrets; `null` clears one. */
545
557
  updateSettings(patch: RestPluginSettingsPatch): Promise<RestPluginDetail>;
558
+ /** Says what an enabled plugin holds: those it requires, named again, and the optional ones to turn on. */
559
+ updateConsent(consent: readonly string[]): Promise<RestPluginDetail>;
546
560
  /** Seals a secret for the plugin; it is answered by nothing from then on. */
547
561
  setSecret(name: string, value: string): Promise<void>;
548
562
  removeSecret(name: string): Promise<void>;
@@ -584,6 +598,17 @@ export interface ConsoleClient {
584
598
  createListing(input: RestListingInput): Promise<RestListing>;
585
599
  /** Takes a listing away before anyone signed in with it; for an administrator. */
586
600
  removeListing(id: string): Promise<void>;
601
+ /** The teams of the workspace (ADR-0073), each with the members the asker may see. */
602
+ teams(signal?: AbortSignal): Promise<RestTeams>;
603
+ /**
604
+ * Makes, renames and removes a team, and puts a person in one or takes them out; for an
605
+ * administrator. A team is called by its key (ADR-0074), which never changes.
606
+ */
607
+ createTeam(input: RestTeamInput): Promise<RestTeam>;
608
+ renameTeam(key: string, patch: RestTeamPatch): Promise<RestTeam>;
609
+ removeTeam(key: string): Promise<void>;
610
+ addTeamMember(key: string, personId: string): Promise<RestTeam>;
611
+ removeTeamMember(key: string, personId: string): Promise<RestTeam>;
587
612
  /** The projects the asker may see, with what they are in each. */
588
613
  projects(signal?: AbortSignal): Promise<RestProjects>;
589
614
  /** Makes a project, whose owner the asker becomes. */
@@ -620,6 +645,26 @@ export interface ConsoleClient {
620
645
  filter?: { readonly open?: boolean; readonly raised?: boolean } & Page,
621
646
  signal?: AbortSignal,
622
647
  ): Promise<RestMyDecisions>;
648
+ /**
649
+ * One decision by its id, wherever it is (ADR-0068): in a project the asker may see, or one
650
+ * that reaches them from elsewhere, addressed to them, raised by them, or a plugin's addressed
651
+ * to someone they manage; with its project and whether they may answer it.
652
+ */
653
+ decision(id: string, signal?: AbortSignal): Promise<RestReachedDecision>;
654
+ /** The same, after waiting up to `waitSeconds` for its answer, as a project's `awaitDecision` waits. */
655
+ awaitDecision(
656
+ id: string,
657
+ waitSeconds: number,
658
+ signal?: AbortSignal,
659
+ ): Promise<RestReachedDecision>;
660
+ /** Answers a decision wherever it is, as whoever it reaches with the right to answer (ADR-0068). */
661
+ answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
662
+ /**
663
+ * Where the bytes of a file put on a decision (ADR-0055) are read at the decision's own address,
664
+ * by the decision's id and the file's: for whoever the decision reaches, in a project they may
665
+ * see or not (ADR-0068), as the project's `fileUrl` is for whoever may see the project.
666
+ */
667
+ decisionFileUrl(decisionId: string, fileId: string): string;
623
668
  /** Another person's plate, within what the asker may see: for an administrator, or an owner of a project they are in. */
624
669
  personWork(personId: string, signal?: AbortSignal): Promise<RestPersonWork>;
625
670
  /** The tokens of whoever asks. */
@@ -1143,6 +1188,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
1143
1188
  ),
1144
1189
  fileUrl: (fileId) => `${base}${at("files", fileId)}`,
1145
1190
  readFile: (fileId, signal) => read(`${base}${at("files", fileId)}`, fileId, signal),
1191
+ personPicture: (personId, signal) =>
1192
+ read(`${base}${at("people", personId, "picture")}`, personId, signal),
1146
1193
  frameUrl: (options) =>
1147
1194
  withQuery(at("frame"), {
1148
1195
  page: options.page,
@@ -1213,6 +1260,8 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
1213
1260
  disable: () => request("POST", `${base}${at("disable")}`, undefined, restPluginDetailSchema),
1214
1261
  updateSettings: (patch) =>
1215
1262
  request("PATCH", `${base}${at("settings")}`, patch, restPluginDetailSchema),
1263
+ updateConsent: (consent) =>
1264
+ request("PATCH", `${base}${at("consent")}`, { consent }, restPluginDetailSchema),
1216
1265
  setSecret: (name, value) =>
1217
1266
  request("PUT", `${base}${at("secrets", name)}`, { value }, undefined),
1218
1267
  removeSecret: (name) =>
@@ -1277,6 +1326,22 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
1277
1326
  request("POST", `${base}${REST_ROUTES.listings}`, input, restListingSchema),
1278
1327
  removeListing: (id) =>
1279
1328
  request("DELETE", `${base}${restPath("listings", id)}`, undefined, undefined),
1329
+ teams: (signal) =>
1330
+ request("GET", `${base}${REST_ROUTES.teams}`, undefined, restTeamsSchema, signal),
1331
+ createTeam: (input) => request("POST", `${base}${REST_ROUTES.teams}`, input, restTeamSchema),
1332
+ renameTeam: (key, patch) =>
1333
+ request("PATCH", `${base}${restPath("teams", key)}`, patch, restTeamSchema),
1334
+ removeTeam: (key) =>
1335
+ request("DELETE", `${base}${restPath("teams", key)}`, undefined, undefined),
1336
+ addTeamMember: (key, personId) =>
1337
+ request("POST", `${base}${restPath("teams", key)}/members`, { personId }, restTeamSchema),
1338
+ removeTeamMember: (key, personId) =>
1339
+ request(
1340
+ "DELETE",
1341
+ `${base}${restPath("teams", key)}/members/${encodeURIComponent(personId)}`,
1342
+ undefined,
1343
+ restTeamSchema,
1344
+ ),
1280
1345
  projects: (signal) =>
1281
1346
  request("GET", `${base}${REST_ROUTES.projects}`, undefined, restProjectsSchema, signal),
1282
1347
  createProject: (input) =>
@@ -1323,6 +1388,19 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
1323
1388
  restMyDecisionsSchema,
1324
1389
  signal,
1325
1390
  ),
1391
+ decision: (id, signal) =>
1392
+ request("GET", `${base}${decisionPath(id)}`, undefined, restReachedDecisionSchema, signal),
1393
+ awaitDecision: (id, waitSeconds, signal) =>
1394
+ request(
1395
+ "GET",
1396
+ withQuery(decisionPath(id), { wait: String(waitSeconds) }),
1397
+ undefined,
1398
+ restReachedDecisionSchema,
1399
+ signal,
1400
+ ),
1401
+ answerDecision: (id, input) =>
1402
+ request("POST", `${base}${decisionPath(id)}/answer`, input, restDecisionSchema),
1403
+ decisionFileUrl: (decisionId, fileId) => `${base}${decisionFilePath(decisionId, fileId)}`,
1326
1404
  personWork: (personId, signal) =>
1327
1405
  request("GET", `${base}${personWorkPath(personId)}`, undefined, restPersonWorkSchema, signal),
1328
1406
  tokens: (signal) =>
package/src/compat.ts CHANGED
@@ -113,7 +113,7 @@ export interface ClientPolicy {
113
113
  * last changed what the command reads or sends (ADR-0038), whose changelog entry begins with
114
114
  * `Breaking:`; `pnpm check` holds it to a version the package has, or is about to have.
115
115
  */
116
- export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.2" };
116
+ export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.5" };
117
117
 
118
118
  /**
119
119
  * The oldest console this command works with (ADR-0038): raised by the release that last
@@ -18,6 +18,8 @@ import {
18
18
  } from "../api.js";
19
19
  import { useWords } from "../i18n/index.js";
20
20
  import type { BoardFilter } from "../router.js";
21
+ import { Picker } from "./picker.js";
22
+ import { listSearch } from "./searches.js";
21
23
  import { isPlainClick } from "./shell.js";
22
24
  import {
23
25
  Badge,
@@ -25,6 +27,7 @@ import {
25
27
  CategoryBadge,
26
28
  cx,
27
29
  DueBadge,
30
+ PersonChip,
28
31
  PriorityBadge,
29
32
  personName,
30
33
  Workers,
@@ -71,13 +74,13 @@ export interface BoardProps {
71
74
  export const FOLDED_STATES: readonly TaskState[] = ["done", "dropped"];
72
75
 
73
76
  /** How far a mouse travels on a card before the press is a drag and not a click. */
74
- const DRAG_SLACK = 4;
77
+ export const DRAG_SLACK = 4;
75
78
  /**
76
79
  * How long a finger rests on a card before the card comes loose, and how far it may wander
77
80
  * meanwhile; a finger that moves sooner is scrolling the page, not moving the card.
78
81
  */
79
- const HOLD_MS = 300;
80
- const HOLD_SLACK = 8;
82
+ export const HOLD_MS = 300;
83
+ export const HOLD_SLACK = 8;
81
84
  /** How near the pointer comes to the board's edge before the board scrolls to show more columns. */
82
85
  const EDGE = 48;
83
86
  /** How fast the board scrolls, in pixels a second, with the pointer at the edge itself; slower further in. */
@@ -727,6 +730,13 @@ export function filterTasks(
727
730
  });
728
731
  }
729
732
 
733
+ /** A goal to narrow the board to, or no goal at all. */
734
+ type GoalChoice = { readonly kind: "none" } | { readonly kind: "goal"; readonly goal: RestGoal };
735
+ /** A person to narrow the board to, or whoever is looking. */
736
+ type PersonChoice =
737
+ | { readonly kind: "mine" }
738
+ | { readonly kind: "person"; readonly person: RestPerson };
739
+
730
740
  export interface BoardFiltersProps {
731
741
  readonly filter: BoardFilter;
732
742
  readonly categories: readonly RestCategory[];
@@ -754,6 +764,44 @@ export function BoardFilters(props: BoardFiltersProps) {
754
764
  }
755
765
  props.onChange(next as BoardFilter);
756
766
  };
767
+ // The goal and the person are searched for (ADR-0037), among the page's own; what the address
768
+ // names and the page does not hold is held as it is, so that the filter still shows.
769
+ const goalChoices: readonly GoalChoice[] = [
770
+ { kind: "none" },
771
+ ...goals.map((goal) => ({ kind: "goal" as const, goal })),
772
+ ];
773
+ const chosenGoal = goals.find((goal) => goal.id === filter.goal);
774
+ const goalChoice: GoalChoice | undefined =
775
+ filter.goal === undefined
776
+ ? undefined
777
+ : filter.goal === ""
778
+ ? { kind: "none" }
779
+ : chosenGoal === undefined
780
+ ? undefined
781
+ : { kind: "goal", goal: chosenGoal };
782
+ const goalWords = (choice: GoalChoice) =>
783
+ choice.kind === "none" ? words.noGoal : choice.goal.title;
784
+ const findGoal = listSearch(goalChoices, (choice, typed) =>
785
+ goalWords(choice).toLowerCase().includes(typed),
786
+ );
787
+ const personChoices: readonly PersonChoice[] = [
788
+ ...(props.me === undefined ? [] : [{ kind: "mine" as const }]),
789
+ ...props.people
790
+ .filter((person) => person.id !== props.me?.id)
791
+ .map((person) => ({ kind: "person" as const, person })),
792
+ ];
793
+ const chosenPerson = props.people.find((person) => person.id === filter.person);
794
+ const personChoice: PersonChoice | undefined =
795
+ filter.mine === true
796
+ ? { kind: "mine" }
797
+ : filter.person === undefined || chosenPerson === undefined
798
+ ? undefined
799
+ : { kind: "person", person: chosenPerson };
800
+ const personWords = (choice: PersonChoice) =>
801
+ choice.kind === "mine" ? words.mine : `${personName(choice.person)} ${choice.person.login}`;
802
+ const findPerson = listSearch(personChoices, (choice, typed) =>
803
+ personWords(choice).toLowerCase().includes(typed),
804
+ );
757
805
  return (
758
806
  <form
759
807
  className="sc-filters"
@@ -795,54 +843,47 @@ export function BoardFilters(props: BoardFiltersProps) {
795
843
  </select>
796
844
  </label>
797
845
  {goals.length > 0 && (
798
- <label className="sc-field sc-field-inline">
799
- <span className="sc-visually-hidden">{words.anyGoal}</span>
800
- <select
801
- className="sc-input"
802
- value={filter.goal ?? "*"}
803
- onChange={(event) =>
804
- event.target.value === "*" ? change({}, "goal") : change({ goal: event.target.value })
846
+ <div className="sc-field sc-field-inline">
847
+ <Picker
848
+ value={goalChoice}
849
+ held={goalChoice === undefined && filter.goal !== undefined ? filter.goal : undefined}
850
+ search={findGoal}
851
+ keyOf={(choice) => (choice.kind === "none" ? "" : choice.goal.id)}
852
+ render={goalWords}
853
+ label={words.anyGoal}
854
+ placeholder={words.anyGoal}
855
+ onChange={(choice) =>
856
+ choice === undefined
857
+ ? change({}, "goal")
858
+ : change({ goal: choice.kind === "none" ? "" : choice.goal.id })
805
859
  }
806
- >
807
- <option value="*">{words.anyGoal}</option>
808
- <option value="">{words.noGoal}</option>
809
- {goals.map((goal) => (
810
- <option key={goal.id} value={goal.id}>
811
- {goal.title}
812
- </option>
813
- ))}
814
- </select>
815
- </label>
860
+ />
861
+ </div>
816
862
  )}
817
- <label className="sc-field sc-field-inline">
818
- <span className="sc-visually-hidden">{words.everyone}</span>
819
- <select
820
- className="sc-input"
821
- value={filter.mine === true ? "mine" : (filter.person ?? "*")}
822
- onChange={(event) => {
823
- const value = event.target.value;
824
- if (value === "*") {
863
+ <div className="sc-field sc-field-inline">
864
+ <Picker
865
+ value={personChoice}
866
+ held={
867
+ personChoice === undefined && filter.person !== undefined ? filter.person : undefined
868
+ }
869
+ search={findPerson}
870
+ keyOf={(choice) => (choice.kind === "mine" ? "mine" : choice.person.id)}
871
+ render={(choice) =>
872
+ choice.kind === "mine" ? words.mine : <PersonChip person={choice.person} size="sm" />
873
+ }
874
+ label={words.everyone}
875
+ placeholder={words.everyone}
876
+ onChange={(choice) => {
877
+ if (choice === undefined) {
825
878
  change({ mine: undefined, person: undefined });
826
- return;
827
- }
828
- if (value === "mine") {
879
+ } else if (choice.kind === "mine") {
829
880
  change({ mine: true, person: undefined });
830
- return;
881
+ } else {
882
+ change({ person: choice.person.id, mine: undefined });
831
883
  }
832
- change({ person: value, mine: undefined });
833
884
  }}
834
- >
835
- <option value="*">{words.everyone}</option>
836
- {props.me !== undefined && <option value="mine">{words.mine}</option>}
837
- {props.people
838
- .filter((person) => person.id !== props.me?.id)
839
- .map((person) => (
840
- <option key={person.id} value={person.id}>
841
- {personName(person)}
842
- </option>
843
- ))}
844
- </select>
845
- </label>
885
+ />
886
+ </div>
846
887
  <span className="sc-filters-count sc-muted">{words.showing(props.shown, props.total)}</span>
847
888
  {narrowed && (
848
889
  <Button variant="ghost" size="sm" onClick={() => props.onChange({})}>
@@ -1,6 +1,7 @@
1
1
  import { type ClipboardEvent, type FormEvent, useMemo, useState } from "react";
2
2
  import {
3
3
  MAX_BODY_LENGTH,
4
+ MAX_DECIDERS,
4
5
  MAX_FILES_PER_DECISION,
5
6
  MAX_OPTION_LABEL_LENGTH,
6
7
  MAX_OPTIONS,
@@ -12,9 +13,10 @@ import {
12
13
  } from "../api.js";
13
14
  import { useWords } from "../i18n/index.js";
14
15
  import { isPicture, placing, unusedName } from "./files.js";
15
- import { Picker, type PickerSearch } from "./picker.js";
16
+ import { Picker } from "./picker.js";
17
+ import { peopleSearch } from "./searches.js";
16
18
  import { type TaskSearch, taskLabel } from "./task-form.js";
17
- import { Button, formatBytes, PersonChip, personName } from "./ui.js";
19
+ import { Button, Chip, formatBytes, PersonChip, personName } from "./ui.js";
18
20
 
19
21
  // Raising a decision: the question, what a person needs to know, the options to choose from,
20
22
  // and who is to answer (ADR-0042): the person raising it, unless they name another of the
@@ -39,21 +41,21 @@ export interface DecisionFormProps {
39
41
  readonly onCancel?: (() => void) | undefined;
40
42
  }
41
43
 
42
- /** The people of the project found by the words typed, a page at a time, as a picker searches. */
43
- export function peopleSearch(people: readonly RestPerson[]): PickerSearch<RestPerson> {
44
- return async (query, page) => {
45
- const words = query.trim().toLowerCase();
46
- const found = people.filter(
47
- (person) =>
48
- words.length === 0 ||
49
- personName(person).toLowerCase().includes(words) ||
50
- person.login.toLowerCase().includes(words),
51
- );
52
- return {
53
- items: found.slice(page.offset, page.offset + page.limit),
54
- more: found.length > page.offset + page.limit,
55
- };
56
- };
44
+ /**
45
+ * What the form sends of whom the decision is for: nothing for the raiser alone, `deciderId` for
46
+ * one other person, `deciderIds` for several (ADR-0057), the first among them repeated as
47
+ * `deciderId` for a console of before.
48
+ */
49
+ export function decidersInput(
50
+ deciders: readonly RestPerson[],
51
+ me: RestPerson | undefined,
52
+ ): Pick<RestDecisionInput, "deciderId" | "deciderIds"> {
53
+ const ids = deciders.map((person) => person.id);
54
+ if (ids.length === 0 || (ids.length === 1 && ids[0] === me?.id)) {
55
+ return {};
56
+ }
57
+ const first = ids[0] ?? "";
58
+ return ids.length === 1 ? { deciderId: first } : { deciderId: first, deciderIds: ids };
57
59
  }
58
60
 
59
61
  export function DecisionForm(props: DecisionFormProps) {
@@ -63,9 +65,20 @@ export function DecisionForm(props: DecisionFormProps) {
63
65
  const [body, setBody] = useState("");
64
66
  const [about, setAbout] = useState<RestTask | undefined>(undefined);
65
67
  const [options, setOptions] = useState<readonly string[]>(["", ""]);
66
- const [decider, setDecider] = useState<RestPerson | undefined>(props.me);
68
+ // Whom it is for (ADR-0042, ADR-0057): the raiser, until others are named in their place.
69
+ const [deciders, setDeciders] = useState<readonly RestPerson[]>(
70
+ props.me === undefined ? [] : [props.me],
71
+ );
67
72
  const [files, setFiles] = useState<readonly File[]>([]);
68
- const findDeciders = useMemo(() => peopleSearch(props.deciders ?? []), [props.deciders]);
73
+ const findDeciders = useMemo(
74
+ () =>
75
+ peopleSearch(
76
+ (props.deciders ?? []).filter(
77
+ (person) => !deciders.some((chosen) => chosen.id === person.id),
78
+ ),
79
+ ),
80
+ [props.deciders, deciders],
81
+ );
69
82
 
70
83
  const labels = options.map((option) => option.trim()).filter((option) => option.length > 0);
71
84
  const ready = question.trim().length > 0 && labels.length >= MIN_OPTIONS;
@@ -115,7 +128,7 @@ export function DecisionForm(props: DecisionFormProps) {
115
128
  body,
116
129
  options: labels.map((label) => ({ label })),
117
130
  taskId: props.taskId ?? about?.id ?? null,
118
- ...(decider === undefined || decider.id === props.me?.id ? {} : { deciderId: decider.id }),
131
+ ...decidersInput(deciders, props.me),
119
132
  },
120
133
  files,
121
134
  );
@@ -195,24 +208,48 @@ export function DecisionForm(props: DecisionFormProps) {
195
208
  </div>
196
209
  )}
197
210
  {props.deciders !== undefined && (
198
- <div className="sc-field">
199
- <span className="sc-field-label">{form.decider}</span>
200
- <Picker
201
- value={decider}
202
- search={findDeciders}
203
- keyOf={(person) => person.id}
204
- render={(person) => (
205
- <>
206
- <PersonChip person={person} size="sm" />
211
+ <fieldset className="sc-field sc-workers-field">
212
+ <legend className="sc-field-label">{form.decider}</legend>
213
+ <div className="sc-chips">
214
+ {deciders.map((person) => (
215
+ <Chip
216
+ key={person.id}
217
+ onRemove={() =>
218
+ setDeciders((current) => current.filter((chosen) => chosen.id !== person.id))
219
+ }
220
+ removeLabel={`${words.common.remove}: ${personName(person)}`}
221
+ disabled={props.busy}
222
+ >
223
+ <PersonChip person={person} />
207
224
  {person.id === props.me?.id && <span className="sc-muted"> {form.me}</span>}
208
- </>
225
+ </Chip>
226
+ ))}
227
+ {deciders.length === 0 && <span className="sc-muted">{form.deciderPlaceholder}</span>}
228
+ {deciders.length < MAX_DECIDERS && (
229
+ <Picker
230
+ className="sc-picker-add"
231
+ value={undefined}
232
+ search={findDeciders}
233
+ keyOf={(person) => person.id}
234
+ render={(person) => (
235
+ <>
236
+ <PersonChip person={person} size="sm" />
237
+ {person.id === props.me?.id && <span className="sc-muted"> {form.me}</span>}
238
+ </>
239
+ )}
240
+ label={form.addDecider}
241
+ placeholder={form.addDecider}
242
+ disabled={props.busy}
243
+ onChange={(person) => {
244
+ if (person !== undefined) {
245
+ setDeciders((current) => [...current, person]);
246
+ }
247
+ }}
248
+ />
209
249
  )}
210
- label={form.decider}
211
- placeholder={form.deciderPlaceholder}
212
- disabled={props.busy}
213
- onChange={(person) => setDecider(person ?? props.me)}
214
- />
215
- </div>
250
+ </div>
251
+ {deciders.length > 1 && <p className="sc-field-hint">{form.decidersHint}</p>}
252
+ </fieldset>
216
253
  )}
217
254
  <fieldset className="sc-field sc-options">
218
255
  <legend className="sc-field-label">{form.options}</legend>
@@ -8,6 +8,7 @@ import {
8
8
  type RestDecision,
9
9
  type RestDecisionFile,
10
10
  type RestDecisionPatch,
11
+ type RestPerson,
11
12
  type RestTask,
12
13
  } from "../api.js";
13
14
  import { useWords } from "../i18n/index.js";
@@ -43,13 +44,34 @@ export interface DecisionCardProps {
43
44
  readonly busy?: boolean | undefined;
44
45
  }
45
46
 
47
+ /** The word beside who answered: through their agent, when the answer came that way (ADR-0056). */
48
+ function answeredWord(words: ReturnType<typeof useWords>, agent: string | null | undefined) {
49
+ return agent === null || agent === undefined
50
+ ? words.decisions.answeredBy
51
+ : words.decisions.answeredThrough(agentLabel(agent));
52
+ }
53
+
54
+ /** Everyone a decision is addressed to (ADR-0057): `deciders` where the console answers it, else the one decider. */
55
+ export function decidersOf(decision: RestDecision): readonly RestPerson[] {
56
+ return decision.deciders !== undefined && decision.deciders.length > 0
57
+ ? decision.deciders
58
+ : [decision.decider];
59
+ }
60
+
61
+ /** The names of everyone a decision is addressed to, one after another. */
62
+ export function deciderNames(decision: RestDecision): string {
63
+ return decidersOf(decision).map(personName).join(", ");
64
+ }
65
+
46
66
  /**
47
67
  * Who asked, and when: the person, through their agent when a run asked; and whom the decision
48
- * is for, where that is someone else (ADR-0042).
68
+ * is for, where that is not the asker alone (ADR-0042, ADR-0057).
49
69
  */
50
70
  function Asked(props: { readonly decision: RestDecision; readonly day?: boolean }) {
51
71
  const words = useWords();
52
72
  const { decision } = props;
73
+ const deciders = decidersOf(decision);
74
+ const addressed = deciders.some((person) => person.id !== decision.raisedBy.id);
53
75
  return (
54
76
  <>
55
77
  <PersonChip person={decision.raisedBy} />{" "}
@@ -57,18 +79,17 @@ function Asked(props: { readonly decision: RestDecision; readonly day?: boolean
57
79
  ? words.decisions.asked
58
80
  : words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
59
81
  <Time iso={decision.createdAt} precision={props.day === true ? "day" : "minute"} />
60
- {decision.decider.id !== decision.raisedBy.id && (
82
+ {addressed && (
61
83
  <span className="sc-decision-for">
62
84
  <span className="sc-visually-hidden">
63
- {words.decisions.peopleLabel(
64
- personName(decision.raisedBy),
65
- personName(decision.decider),
66
- )}
85
+ {words.decisions.peopleLabel(personName(decision.raisedBy), deciderNames(decision))}
67
86
  </span>
68
87
  <span className="sc-decision-arrow" aria-hidden="true">
69
88
  →
70
89
  </span>{" "}
71
- <PersonChip person={decision.decider} />
90
+ {deciders.map((person) => (
91
+ <PersonChip key={person.id} person={person} />
92
+ ))}
72
93
  </span>
73
94
  )}
74
95
  </>
@@ -233,7 +254,20 @@ function Outcome(props: {
233
254
  {decision.outcome === null ? (
234
255
  <p className="sc-muted">{words.decisions.nothingWritten}</p>
235
256
  ) : (
236
- <Markdown source={decision.outcome} docHref={props.docHref} />
257
+ <>
258
+ <Markdown source={decision.outcome} docHref={props.docHref} />
259
+ {decision.outcomeBy != null && decision.outcomeAt != null && (
260
+ <p className="sc-decision-meta">
261
+ <PersonChip person={decision.outcomeBy} />{" "}
262
+ {decision.outcomeAgent == null
263
+ ? words.decisions.wroteWhatFollowed
264
+ : words.decisions.wroteWhatFollowedThrough(
265
+ agentLabel(decision.outcomeAgent),
266
+ )}{" "}
267
+ <Time iso={decision.outcomeAt} />
268
+ </p>
269
+ )}
270
+ </>
237
271
  )}
238
272
  {props.onUpdate !== undefined && (
239
273
  <Button
@@ -368,14 +402,12 @@ export function DecisionCard(props: DecisionCardProps) {
368
402
  </div>
369
403
  )}
370
404
  <p className="sc-decision-meta">
371
- <PersonChip person={decision.answer.by} /> {words.decisions.answeredBy}{" "}
405
+ <PersonChip person={decision.answer.by} /> {answeredWord(words, decision.answer.agent)}{" "}
372
406
  <Time iso={decision.answer.at} />
373
407
  </p>
374
408
  </div>
375
409
  ) : props.onAnswer === undefined ? (
376
- <p className="sc-decision-waiting">
377
- {words.decisions.waitingFor(personName(decision.decider))}
378
- </p>
410
+ <p className="sc-decision-waiting">{words.decisions.waitingFor(deciderNames(decision))}</p>
379
411
  ) : (
380
412
  <AnswerForm decision={decision} onAnswer={props.onAnswer} busy={props.busy} />
381
413
  )}
@@ -474,7 +506,7 @@ function RecordRow(props: {
474
506
  </div>
475
507
  )}
476
508
  <p className="sc-decision-record-meta">
477
- <PersonChip person={answer.by} /> {words.decisions.answeredBy}{" "}
509
+ <PersonChip person={answer.by} /> {answeredWord(words, answer.agent)}{" "}
478
510
  <Time iso={answer.at} precision="day" />
479
511
  </p>
480
512
  </div>