@skillcdn/console 0.4.4 → 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 (192) hide show
  1. package/README.md +3 -3
  2. package/dist/api.d.ts +3 -3
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +3 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +162 -15
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +16 -3
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +68 -19
  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 +26 -5
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/client.d.ts +28 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +12 -2
  19. package/dist/client.js.map +1 -1
  20. package/dist/compat.js +1 -1
  21. package/dist/components/board.d.ts.map +1 -1
  22. package/dist/components/board.js +46 -15
  23. package/dist/components/board.js.map +1 -1
  24. package/dist/components/decision-form.d.ts +0 -3
  25. package/dist/components/decision-form.d.ts.map +1 -1
  26. package/dist/components/decision-form.js +3 -15
  27. package/dist/components/decision-form.js.map +1 -1
  28. package/dist/components/event-feed.d.ts.map +1 -1
  29. package/dist/components/event-feed.js +30 -10
  30. package/dist/components/event-feed.js.map +1 -1
  31. package/dist/components/my-decisions.d.ts +20 -3
  32. package/dist/components/my-decisions.d.ts.map +1 -1
  33. package/dist/components/my-decisions.js +14 -1
  34. package/dist/components/my-decisions.js.map +1 -1
  35. package/dist/components/people.d.ts +43 -1
  36. package/dist/components/people.d.ts.map +1 -1
  37. package/dist/components/people.js +117 -9
  38. package/dist/components/people.js.map +1 -1
  39. package/dist/components/picker.d.ts +8 -0
  40. package/dist/components/picker.d.ts.map +1 -1
  41. package/dist/components/picker.js +2 -2
  42. package/dist/components/picker.js.map +1 -1
  43. package/dist/components/plugin-frame.d.ts +0 -1
  44. package/dist/components/plugin-frame.d.ts.map +1 -1
  45. package/dist/components/plugin-frame.js +22 -1
  46. package/dist/components/plugin-frame.js.map +1 -1
  47. package/dist/components/plugin-settings.d.ts +5 -1
  48. package/dist/components/plugin-settings.d.ts.map +1 -1
  49. package/dist/components/plugin-settings.js +143 -30
  50. package/dist/components/plugin-settings.js.map +1 -1
  51. package/dist/components/projects.d.ts.map +1 -1
  52. package/dist/components/projects.js +3 -1
  53. package/dist/components/projects.js.map +1 -1
  54. package/dist/components/searches.d.ts +11 -0
  55. package/dist/components/searches.d.ts.map +1 -0
  56. package/dist/components/searches.js +30 -0
  57. package/dist/components/searches.js.map +1 -0
  58. package/dist/components/task-form.d.ts.map +1 -1
  59. package/dist/components/task-form.js +6 -5
  60. package/dist/components/task-form.js.map +1 -1
  61. package/dist/components/ui.d.ts +16 -0
  62. package/dist/components/ui.d.ts.map +1 -1
  63. package/dist/components/ui.js +10 -0
  64. package/dist/components/ui.js.map +1 -1
  65. package/dist/console.css +362 -67
  66. package/dist/console.d.ts +2 -1
  67. package/dist/console.d.ts.map +1 -1
  68. package/dist/console.js +90 -14
  69. package/dist/console.js.map +1 -1
  70. package/dist/data.d.ts +14 -1
  71. package/dist/data.d.ts.map +1 -1
  72. package/dist/data.js +36 -1
  73. package/dist/data.js.map +1 -1
  74. package/dist/i18n/en.d.ts +89 -0
  75. package/dist/i18n/en.d.ts.map +1 -1
  76. package/dist/i18n/en.js +87 -2
  77. package/dist/i18n/en.js.map +1 -1
  78. package/dist/i18n/ko.d.ts.map +1 -1
  79. package/dist/i18n/ko.js +87 -3
  80. package/dist/i18n/ko.js.map +1 -1
  81. package/dist/index.d.ts +3 -2
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +3 -2
  84. package/dist/index.js.map +1 -1
  85. package/dist/limits.d.ts +6 -0
  86. package/dist/limits.d.ts.map +1 -1
  87. package/dist/limits.js +6 -0
  88. package/dist/limits.js.map +1 -1
  89. package/dist/plugin/args.d.ts +6 -1
  90. package/dist/plugin/args.d.ts.map +1 -1
  91. package/dist/plugin/args.js +16 -11
  92. package/dist/plugin/args.js.map +1 -1
  93. package/dist/plugin/bridge.d.ts +20 -1
  94. package/dist/plugin/bridge.d.ts.map +1 -1
  95. package/dist/plugin/bridge.js +28 -0
  96. package/dist/plugin/bridge.js.map +1 -1
  97. package/dist/plugin/capabilities.d.ts +1 -1
  98. package/dist/plugin/capabilities.d.ts.map +1 -1
  99. package/dist/plugin/capabilities.js +10 -0
  100. package/dist/plugin/capabilities.js.map +1 -1
  101. package/dist/plugin/context.d.ts +158 -13
  102. package/dist/plugin/context.d.ts.map +1 -1
  103. package/dist/plugin/define.d.ts +13 -1
  104. package/dist/plugin/define.d.ts.map +1 -1
  105. package/dist/plugin/define.js +17 -1
  106. package/dist/plugin/define.js.map +1 -1
  107. package/dist/plugin/limits.d.ts +4 -0
  108. package/dist/plugin/limits.d.ts.map +1 -1
  109. package/dist/plugin/limits.js +4 -0
  110. package/dist/plugin/limits.js.map +1 -1
  111. package/dist/plugin/manifest.d.ts +15 -0
  112. package/dist/plugin/manifest.d.ts.map +1 -1
  113. package/dist/plugin/manifest.js +61 -4
  114. package/dist/plugin/manifest.js.map +1 -1
  115. package/dist/plugin/rest-schemas.d.ts +44 -0
  116. package/dist/plugin/rest-schemas.d.ts.map +1 -1
  117. package/dist/plugin/rest-schemas.js +55 -3
  118. package/dist/plugin/rest-schemas.js.map +1 -1
  119. package/dist/plugin/testing.d.ts +15 -1
  120. package/dist/plugin/testing.d.ts.map +1 -1
  121. package/dist/plugin/testing.js +40 -4
  122. package/dist/plugin/testing.js.map +1 -1
  123. package/dist/plugin/web.d.ts +6 -0
  124. package/dist/plugin/web.d.ts.map +1 -1
  125. package/dist/plugin/web.js +10 -1
  126. package/dist/plugin/web.js.map +1 -1
  127. package/dist/plugin.d.ts +1 -1
  128. package/dist/plugin.d.ts.map +1 -1
  129. package/dist/plugin.js +1 -1
  130. package/dist/plugin.js.map +1 -1
  131. package/dist/router.d.ts +19 -0
  132. package/dist/router.d.ts.map +1 -1
  133. package/dist/router.js +18 -0
  134. package/dist/router.js.map +1 -1
  135. package/dist/routes.d.ts +11 -2
  136. package/dist/routes.d.ts.map +1 -1
  137. package/dist/routes.js +14 -1
  138. package/dist/routes.js.map +1 -1
  139. package/dist/schemas.d.ts +367 -0
  140. package/dist/schemas.d.ts.map +1 -1
  141. package/dist/schemas.js +62 -2
  142. package/dist/schemas.js.map +1 -1
  143. package/dist/vocabulary.d.ts +1 -1
  144. package/dist/vocabulary.d.ts.map +1 -1
  145. package/dist/vocabulary.js +6 -0
  146. package/dist/vocabulary.js.map +1 -1
  147. package/docs/specs/cli.md +7 -4
  148. package/docs/specs/plugins.md +47 -39
  149. package/docs/specs/rest.md +22 -5
  150. package/package.json +1 -1
  151. package/skills/building-a-plugin/SKILL.md +8 -6
  152. package/skills/working-the-board/SKILL.md +1 -1
  153. package/src/api.ts +17 -0
  154. package/src/cli/cli.ts +200 -12
  155. package/src/cli/format.ts +82 -19
  156. package/src/cli/help.ts +30 -5
  157. package/src/client.ts +70 -0
  158. package/src/compat.ts +1 -1
  159. package/src/components/board.tsx +83 -42
  160. package/src/components/decision-form.tsx +13 -32
  161. package/src/components/event-feed.tsx +36 -9
  162. package/src/components/my-decisions.tsx +50 -5
  163. package/src/components/people.tsx +394 -37
  164. package/src/components/picker.tsx +11 -2
  165. package/src/components/plugin-frame.tsx +23 -0
  166. package/src/components/plugin-settings.tsx +346 -137
  167. package/src/components/projects.tsx +14 -14
  168. package/src/components/searches.ts +47 -0
  169. package/src/components/task-form.tsx +23 -28
  170. package/src/components/ui.tsx +36 -0
  171. package/src/console.tsx +229 -40
  172. package/src/data.ts +53 -1
  173. package/src/i18n/en.ts +98 -2
  174. package/src/i18n/ko.ts +95 -3
  175. package/src/index.ts +11 -0
  176. package/src/limits.ts +6 -0
  177. package/src/plugin/args.ts +16 -11
  178. package/src/plugin/bridge.ts +45 -0
  179. package/src/plugin/capabilities.ts +11 -0
  180. package/src/plugin/context.ts +170 -10
  181. package/src/plugin/define.ts +34 -0
  182. package/src/plugin/limits.ts +4 -0
  183. package/src/plugin/manifest.ts +74 -3
  184. package/src/plugin/rest-schemas.ts +62 -5
  185. package/src/plugin/testing.ts +61 -5
  186. package/src/plugin/web.ts +17 -0
  187. package/src/plugin.ts +1 -0
  188. package/src/router.ts +25 -0
  189. package/src/routes.ts +17 -2
  190. package/src/schemas.ts +81 -2
  191. package/src/styles/console.css +362 -67
  192. package/src/vocabulary.ts +6 -0
package/src/cli/help.ts CHANGED
@@ -229,6 +229,30 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
229
229
  about:
230
230
  "The people of the workspace you see, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Everyone, unless the workspace shows a member only the people they share a project with (console workspace says which; an administrator sees everyone). Then, where you see everyone, the logins an administrator let in that nobody has signed in with yet. The logins are what --worker, member and person take; a login is found exactly, so an owner lists someone the list does not show.",
231
231
  },
232
+ teams: {
233
+ usage: "teams",
234
+ synopsis: "The teams of the workspace, each with who is in it.",
235
+ about:
236
+ "The teams of the workspace (a name and its members: a unit to call people by, and to grant a plugin's role to, ADR-0073), by name, each with its key, what it is called by (ADR-0074), how many are in it and their logins. console team makes and keeps them, for an administrator.",
237
+ },
238
+ team: {
239
+ usage:
240
+ 'team new <key> "<name>" | team rename <key> "<new name>" | team remove <key> | team add <key> <login> | team drop <key> <login>',
241
+ synopsis:
242
+ "Makes, renames or removes a team, or puts a person in one or takes them out; for an administrator.",
243
+ about:
244
+ "For an administrator: a team is a key, a name and its members, any number per person. The key is what a grant, a plugin and this command call the team by (ADR-0074): lowercase letters, digits and hyphens, one among the teams' and the projects' keys, and it never changes; the name, one per workspace, is what people read. new makes one; rename gives it another name, the key staying; remove takes it away, with its memberships and the plugin roles granted to it; add and drop put a person in and take them out, by login. The board records each as the workspace's own event, and a plugin with people:read:all hears of them.",
245
+ refusals: [
246
+ "auth.forbidden: only an administrator keeps the teams",
247
+ "request.invalid: the key is not lowercase letters, digits and hyphens, or the name is empty or too long",
248
+ "team.not_found: no team has that key",
249
+ "team.key_taken: a team or a project has that key already",
250
+ "team.name_taken: a team of that name is there already",
251
+ "team.too_many: the workspace holds as many teams as one may",
252
+ "team.invalid_member: the person is not an active person of the workspace",
253
+ "team.not_a_member: the person is not in the team",
254
+ ],
255
+ },
232
256
  workspace: {
233
257
  usage:
234
258
  "workspace | workspace edit [--people-visibility <everyone|shared-projects>] [--week-start <auto|monday|sunday>] [--time-zone <zone|none>] | workspace pack-token --connect | workspace pack-token --stdin | workspace pack-token --remove",
@@ -248,15 +272,16 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
248
272
  },
249
273
  person: {
250
274
  usage:
251
- "person role <login> <admin|member> | person manager <login> <manager login|none> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>",
275
+ "person role <login> <admin|member> | person manager <login> <manager login|none> | person email <login> <address|none> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>",
252
276
  synopsis:
253
- "Says what a person is in the workspace and whom they report to, lets a login in or takes one out; for an administrator.",
277
+ "Says what a person is in the workspace, whom they report to and their work email, lets a login in or takes one out; for an administrator.",
254
278
  about:
255
- "For an administrator: the person themselves on the People page, or their agent here, named on it. role says what a person is in the workspace; the board keeps at least one administrator in. manager says whom a person reports to, the reporting line of the directory, which the plugins read as a default (the approver of their leave, say) in place of a line of their own: another active person of the workspace who does not report to them, directly or through others, or none; console people shows it. add lets a login in: whoever signs in with that account at that host is a person from then on; the host is read off the login (an address is Google's, anything else GitHub's) unless --host says. remove takes a login away that nobody has signed in with yet. deactivate takes a person out: refused on every request at once, their agents with them, kept with everything they did, and listed nowhere; reactivate takes them back in. A person the organization let in comes and goes with it, and is neither.",
279
+ "For an administrator: the person themselves on the People page, or their agent here, named on it. role says what a person is in the workspace; the board keeps at least one administrator in. manager says whom a person reports to, the reporting line of the directory, which the plugins read as a default (the approver of their leave, say) in place of a line of their own: another active person of the workspace who does not report to them, directly or through others, or none; console people shows it. email writes the person's work email, or none, by which a plugin allowed to read it matches them to an account elsewhere: a person who signs in at Google has their address from the sign-in, and an administrator writes everyone else's; it is read by an administrator, the person and such a plugin, and by nobody from the feed, which says only that it changed. add lets a login in: whoever signs in with that account at that host is a person from then on; the host is read off the login (an address is Google's, anything else GitHub's) unless --host says. remove takes a login away that nobody has signed in with yet. deactivate takes a person out: refused on every request at once, their agents with them, kept with everything they did, and listed nowhere; reactivate takes them back in. A person the organization let in comes and goes with it, and is neither.",
256
280
  refusals: [
257
281
  "auth.forbidden: only an administrator says who is in and what they are",
258
282
  "person.last_admin: the board keeps at least one administrator in; make another one first",
259
283
  "person.invalid_manager: the manager is not another active person of the workspace, or reports to this person already",
284
+ "person.email_taken: the address is another person's in this workspace",
260
285
  "person.of_organization: the organization says whether that person is in",
261
286
  "listing.exists: the login is let in already, or is a person's who is in",
262
287
  "listing.unknown_host: nobody signs in through that provider here",
@@ -380,13 +405,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
380
405
  },
381
406
  plugin: {
382
407
  usage:
383
- "plugin <key> | plugin <key> help [<command>] | plugin <key> <command> [<args>] [--<name> <value>]... [--file <path> | -] [--run <id>] | plugin <key> records <collection> [<id>] [--where <field>=<value>]... [--search <words>] [--project <key>] [--order <field>[:desc]] [--limit <n>] [--after <cursor>] [--archived] [--versions] | plugin <key> skills [<folder>|<file>] [--search <words> [--limit <n>] [--cursor <cursor>]] | plugin <key> skill <path> | plugin <key> skill --file <path> [--offset <n>] [--limit <n>] | plugin <key> enable --consent <capability>,... [--board new|<key>] [--projects all|<key>,...] [--set <key>=<value>]... | plugin <key> disable | plugin <key> capabilities [--consent <capability>,...|none] | plugin <key> settings [--set <key>=<value>]... | plugin <key> secret <name> [--remove] | plugin <key> grant <role> --person <login>|--project <key>|--everyone | plugin <key> revoke <grant id>",
408
+ "plugin <key> | plugin <key> help [<command>] | plugin <key> <command> [<args>] [--<name> <value>]... [--file <path> | -] [--run <id>] | plugin <key> records <collection> [<id>] [--where <field>=<value>]... [--search <words>] [--project <key>] [--order <field>[:desc]] [--limit <n>] [--after <cursor>] [--archived] [--versions] | plugin <key> skills [<folder>|<file>] [--search <words> [--limit <n>] [--cursor <cursor>]] | plugin <key> skill <path> | plugin <key> skill --file <path> [--offset <n>] [--limit <n>] | plugin <key> enable --consent <capability>,... [--board new|<key>] [--projects all|<key>,...] [--set <key>=<value>]... | plugin <key> disable | plugin <key> capabilities [--consent <capability>,...|none] | plugin <key> settings [--set <key>=<value>]... | plugin <key> secret <name> [--remove] | plugin <key> grant <role> --person <login>|--project <key>|--plugin <key>|--team <key>|--everyone | plugin <key> revoke <grant id>",
384
409
  overview:
385
410
  "plugin <key> [help [<command>] | <command> [<args>] | records <collection> | skills | skill <path> | enable | disable | capabilities | settings | secret | grant | revoke]",
386
411
  synopsis:
387
412
  "One plugin: what it is for and its commands; a command run as you; its records, its skills; its settings, for an administrator.",
388
413
  about:
389
- "One plugin of this console, by its key. Alone: what it is, the sentence for agents, its state, the roles you hold in it, its board project, then one line per command and query you may run, with its usage, where its skills are, and its settings. help [<command>]: one command or query in full, as console help prints one: its arguments, their types and bounds, and the refusals; every argument is given by its place when positional, else as --<name> <value> or --<name>=<value>; a boolean as --<name> alone; a list as the flag given again, or with commas between; a day as YYYY-MM-DD; a person by login, or me; a project by key; a file by --file <path>, or - for standard input, which the command uploads first, with the plugin's files capability. <command> [<args>]: runs the command or the query as the person whose token you hold; the console parses the words against the definition and refuses the first that does not fit; prints what the command says, or a query's answer as JSON, a file a query answers saved under its name; --json prints the whole answer. A command that raised a decision prints its id and exits with 3: console decision <id> --wait 100 waits for the answer. The event the command leaves names your open run in the plugin's board project, --run <id> saying which when several are open; an argument marked secret is hidden in it. records <collection>: the records you may read, one a line, newest first; --where <field>=<value>, given again for several, narrows by a field the collection indexes (id, owner and project always; a number, true, false and null are read as such); --search <words> by words; --project <key> to those linked to a project; --order <field>, with :desc for the newest first; a page of --limit with --after <cursor> as the last page printed; --archived the ones put away; <id> reads one record whole, --versions its versions. skills and skill: the plugin's own skills, a pack read from its package, exactly as console skills and console skill read a project's pack. enable, disable, settings, secret, grant and revoke are an administrator's. enable prints what each capability the plugin asks opens and what a malicious plugin would reach through it, and refuses unless --consent names every capability again; --board new makes the board project the manifest proposes, --board <key> chooses one, left out where the plugin needs none; --projects all, or the keys of the projects the plugin is on; --set a setting's value, which settings changes afterwards too, an empty value clearing one, a number or true and false read as such. secret <name> reads the value from standard input, seals it and shows it never; --remove takes it away. grant gives a role to a person by login, to the members of a project by key, or to everyone, and revoke takes a grant back by its id, which console plugin <key> lists to an administrator.",
414
+ "One plugin of this console, by its key. Alone: what it is, the sentence for agents, its state, the roles you hold in it, its board project, then one line per command and query you may run, with its usage, where its skills are, and its settings. help [<command>]: one command or query in full, as console help prints one: its arguments, their types and bounds, and the refusals; every argument is given by its place when positional, else as --<name> <value> or --<name>=<value>; a boolean as --<name> alone; a list as the flag given again, or with commas between; a day as YYYY-MM-DD; a person by login, or me; a project by key; a file by --file <path>, or - for standard input, which the command uploads first, with the plugin's files capability. <command> [<args>]: runs the command or the query as the person whose token you hold; the console parses the words against the definition and refuses the first that does not fit; prints what the command says, or a query's answer as JSON, a file a query answers saved under its name; --json prints the whole answer. A command that raised a decision prints its id and exits with 3: console decision <id> --wait 100 waits for the answer. The event the command leaves names your open run in the plugin's board project, --run <id> saying which when several are open; an argument marked secret is hidden in it. records <collection>: the records you may read, one a line, newest first; --where <field>=<value>, given again for several, narrows by a field the collection indexes (id, owner and project always; a number, true, false and null are read as such); --search <words> by words; --project <key> to those linked to a project; --order <field>, with :desc for the newest first; a page of --limit with --after <cursor> as the last page printed; --archived the ones put away; <id> reads one record whole, --versions its versions. skills and skill: the plugin's own skills, a pack read from its package, exactly as console skills and console skill read a project's pack. enable, disable, settings, secret, grant and revoke are an administrator's. enable prints what each capability the plugin asks opens and what a malicious plugin would reach through it, and refuses unless --consent names every capability again; --board new makes the board project the manifest proposes, --board <key> chooses one, left out where the plugin needs none; --projects all, or the keys of the projects the plugin is on; --set a setting's value, which settings changes afterwards too, an empty value clearing one, a number or true and false read as such. secret <name> reads the value from standard input, seals it and shows it never; --remove takes it away. grant gives a role to a person by login, to the members of a project by key, to another plugin by key, to the members of a team by key, or to everyone, and revoke takes a grant back by its id, which console plugin <key> lists to an administrator.",
390
415
  limits: [
391
416
  "a command line: up to 200 words of up to 50,000 characters each; one file, up to the file bound (console help hand-in), with the plugin's files capability",
392
417
  "records: a page of up to 200; where: a field the collection indexes",
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,
@@ -588,6 +598,17 @@ export interface ConsoleClient {
588
598
  createListing(input: RestListingInput): Promise<RestListing>;
589
599
  /** Takes a listing away before anyone signed in with it; for an administrator. */
590
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>;
591
612
  /** The projects the asker may see, with what they are in each. */
592
613
  projects(signal?: AbortSignal): Promise<RestProjects>;
593
614
  /** Makes a project, whose owner the asker becomes. */
@@ -624,6 +645,26 @@ export interface ConsoleClient {
624
645
  filter?: { readonly open?: boolean; readonly raised?: boolean } & Page,
625
646
  signal?: AbortSignal,
626
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;
627
668
  /** Another person's plate, within what the asker may see: for an administrator, or an owner of a project they are in. */
628
669
  personWork(personId: string, signal?: AbortSignal): Promise<RestPersonWork>;
629
670
  /** The tokens of whoever asks. */
@@ -1285,6 +1326,22 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
1285
1326
  request("POST", `${base}${REST_ROUTES.listings}`, input, restListingSchema),
1286
1327
  removeListing: (id) =>
1287
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
+ ),
1288
1345
  projects: (signal) =>
1289
1346
  request("GET", `${base}${REST_ROUTES.projects}`, undefined, restProjectsSchema, signal),
1290
1347
  createProject: (input) =>
@@ -1331,6 +1388,19 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
1331
1388
  restMyDecisionsSchema,
1332
1389
  signal,
1333
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)}`,
1334
1404
  personWork: (personId, signal) =>
1335
1405
  request("GET", `${base}${personWorkPath(personId)}`, undefined, restPersonWorkSchema, signal),
1336
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.4" };
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,
@@ -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({})}>
@@ -13,9 +13,10 @@ import {
13
13
  } from "../api.js";
14
14
  import { useWords } from "../i18n/index.js";
15
15
  import { isPicture, placing, unusedName } from "./files.js";
16
- import { Picker, type PickerSearch } from "./picker.js";
16
+ import { Picker } from "./picker.js";
17
+ import { peopleSearch } from "./searches.js";
17
18
  import { type TaskSearch, taskLabel } from "./task-form.js";
18
- import { Button, formatBytes, PersonChip, personName } from "./ui.js";
19
+ import { Button, Chip, formatBytes, PersonChip, personName } from "./ui.js";
19
20
 
20
21
  // Raising a decision: the question, what a person needs to know, the options to choose from,
21
22
  // and who is to answer (ADR-0042): the person raising it, unless they name another of the
@@ -57,23 +58,6 @@ export function decidersInput(
57
58
  return ids.length === 1 ? { deciderId: first } : { deciderId: first, deciderIds: ids };
58
59
  }
59
60
 
60
- /** The people of the project found by the words typed, a page at a time, as a picker searches. */
61
- export function peopleSearch(people: readonly RestPerson[]): PickerSearch<RestPerson> {
62
- return async (query, page) => {
63
- const words = query.trim().toLowerCase();
64
- const found = people.filter(
65
- (person) =>
66
- words.length === 0 ||
67
- personName(person).toLowerCase().includes(words) ||
68
- person.login.toLowerCase().includes(words),
69
- );
70
- return {
71
- items: found.slice(page.offset, page.offset + page.limit),
72
- more: found.length > page.offset + page.limit,
73
- };
74
- };
75
- }
76
-
77
61
  export function DecisionForm(props: DecisionFormProps) {
78
62
  const words = useWords();
79
63
  const form = words.decisions.form;
@@ -228,25 +212,22 @@ export function DecisionForm(props: DecisionFormProps) {
228
212
  <legend className="sc-field-label">{form.decider}</legend>
229
213
  <div className="sc-chips">
230
214
  {deciders.map((person) => (
231
- <span key={person.id} className="sc-chip">
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
+ >
232
223
  <PersonChip person={person} />
233
224
  {person.id === props.me?.id && <span className="sc-muted"> {form.me}</span>}
234
- <button
235
- type="button"
236
- className="sc-chip-remove"
237
- aria-label={`${words.common.remove}: ${personName(person)}`}
238
- disabled={props.busy}
239
- onClick={() =>
240
- setDeciders((current) => current.filter((chosen) => chosen.id !== person.id))
241
- }
242
- >
243
- <span aria-hidden="true">&times;</span>
244
- </button>
245
- </span>
225
+ </Chip>
246
226
  ))}
247
227
  {deciders.length === 0 && <span className="sc-muted">{form.deciderPlaceholder}</span>}
248
228
  {deciders.length < MAX_DECIDERS && (
249
229
  <Picker
230
+ className="sc-picker-add"
250
231
  value={undefined}
251
232
  search={findDeciders}
252
233
  keyOf={(person) => person.id}
@@ -45,11 +45,15 @@ function sentenceOf(event: RestEvent, words: Messages): string {
45
45
  const grantee =
46
46
  data.everyone === true
47
47
  ? feed.everyone
48
- : data.login !== undefined
49
- ? data.login
50
- : data.key !== undefined
51
- ? feed.membersOf(data.name ?? data.key)
52
- : feed.someone;
48
+ : data.grantee !== undefined
49
+ ? feed.thePlugin(data.grantee)
50
+ : data.team !== undefined
51
+ ? feed.theTeam(data.team)
52
+ : data.login !== undefined
53
+ ? data.login
54
+ : data.key !== undefined
55
+ ? feed.membersOf(data.name ?? data.key)
56
+ : feed.someone;
53
57
  switch (event.kind) {
54
58
  case "person.joined":
55
59
  return feed.event.personJoined();
@@ -67,6 +71,18 @@ function sentenceOf(event: RestEvent, words: Messages): string {
67
71
  return data.manager === null || data.manager === undefined
68
72
  ? feed.event.managerCleared(login)
69
73
  : feed.event.managerChanged(login, data.manager);
74
+ case "person.email_changed":
75
+ return data.set === true ? feed.event.emailChanged(login) : feed.event.emailCleared(login);
76
+ case "team.created":
77
+ return feed.event.teamCreated(data.team ?? feed.aTeam);
78
+ case "team.renamed":
79
+ return feed.event.teamRenamed(data.name ?? feed.aTeam, data.team ?? feed.aTeam);
80
+ case "team.removed":
81
+ return feed.event.teamRemoved(data.team ?? feed.aTeam);
82
+ case "team.member_added":
83
+ return feed.event.teamMemberAdded(login, data.team ?? feed.aTeam);
84
+ case "team.member_removed":
85
+ return feed.event.teamMemberRemoved(login, data.team ?? feed.aTeam);
70
86
  case "workspace.updated":
71
87
  return feed.event.workspaceUpdated(fields);
72
88
  case "project.created":
@@ -140,9 +156,11 @@ function sentenceOf(event: RestEvent, words: Messages): string {
140
156
  case "plugin.disabled":
141
157
  return feed.event.pluginDisabled(plugin);
142
158
  case "plugin.configured":
143
- return data.capabilities === undefined
144
- ? feed.event.pluginConfigured(fields, plugin)
145
- : feed.event.pluginConsented(plugin);
159
+ return data.capabilities !== undefined
160
+ ? feed.event.pluginConsented(plugin)
161
+ : data.source !== undefined && data.source !== "administrator"
162
+ ? feed.event.pluginSealed(fields, plugin, data.source)
163
+ : feed.event.pluginConfigured(fields, plugin);
146
164
  case "plugin.granted":
147
165
  return feed.event.pluginGranted(data.role ?? feed.role.other, grantee, plugin);
148
166
  case "plugin.revoked":
@@ -195,7 +213,11 @@ function formatFeedDay(day: string, language: string, now: Date = new Date()): s
195
213
 
196
214
  /** Whether two events were done by the same person as the same agent, so the name is said once. */
197
215
  function sameHands(a: RestEvent, b: RestEvent): boolean {
198
- return (a.actor?.id ?? null) === (b.actor?.id ?? null) && a.agent === b.agent;
216
+ return (
217
+ (a.actor?.id ?? null) === (b.actor?.id ?? null) &&
218
+ a.agent === b.agent &&
219
+ (a.via ?? null) === (b.via ?? null)
220
+ );
199
221
  }
200
222
 
201
223
  /** The hour of an event, for a row whose day is in the heading above it. */
@@ -266,6 +288,11 @@ export function EventFeed(props: EventFeedProps) {
266
288
  {words.feed.as(agentLabel(run.actor.agent))}
267
289
  </span>
268
290
  )}
291
+ {run.actor.via !== null && run.actor.via !== undefined && (
292
+ <span className="sc-feed-agent" title={words.feed.viaTitle}>
293
+ {words.feed.via(run.actor.via)}
294
+ </span>
295
+ )}
269
296
  </span>
270
297
  <ol className="sc-feed-run-list">
271
298
  {run.events.map((event) => {
@@ -1,5 +1,11 @@
1
1
  import type { ReactNode } from "react";
2
- import type { RestAnswerInput, RestDecision, RestMyDecisions } from "../api.js";
2
+ import {
3
+ decisionFilePath,
4
+ type RestAnswerInput,
5
+ type RestDecision,
6
+ type RestMyDecisions,
7
+ type RestReachedDecision,
8
+ } from "../api.js";
3
9
  import { useWords } from "../i18n/index.js";
4
10
  import { DecisionCard, DecisionLog } from "./decision-list.js";
5
11
  import { ProjectMark } from "./projects.js";
@@ -11,7 +17,9 @@ import { Button } from "./ui.js";
11
17
  // whom they wait for; in the second the record, the answered ones of both kinds, newest first.
12
18
  // Every card and row names the project, and who asked and who answers, as the cards do
13
19
  // everywhere. Takes its data as props and nothing from the network; the page reads more when
14
- // asked.
20
+ // asked. A decision here may be in a project the person may not see (ADR-0068), so the files
21
+ // put on it are read at the decision's own address, which serves them to whoever the decision
22
+ // reaches, and not at the project's.
15
23
 
16
24
  /** One kind of a person's decisions, a page at a time. */
17
25
  export interface DecisionsPage {
@@ -27,8 +35,14 @@ export interface MyDecisionsProps {
27
35
  /** The decisions the person asked of someone else: waiting and answered. */
28
36
  readonly raised: DecisionsPage;
29
37
  readonly decisionHref: (projectKey: string, decisionId: string) => string;
30
- /** Where a file put on a decision is read, by its project's key and its id (ADR-0055). */
31
- readonly fileHref?: ((projectKey: string, fileId: string) => string) | undefined;
38
+ /**
39
+ * Where a file put on a decision is read (ADR-0055), by the project's key, the file's id and
40
+ * the decision's; left out, at the decision's own address, `decisionFilePath`, which serves it
41
+ * to whoever the decision reaches, in a project they may see or not (ADR-0068).
42
+ */
43
+ readonly fileHref?:
44
+ | ((projectKey: string, fileId: string, decisionId: string) => string)
45
+ | undefined;
32
46
  /** Called with the person's answer on a decision of theirs, in its project. Left out, a waiting decision is answered on its own page. */
33
47
  readonly onAnswer?:
34
48
  | ((projectKey: string, decision: RestDecision, input: RestAnswerInput) => void)
@@ -82,7 +96,11 @@ export function MyDecisions(props: MyDecisionsProps) {
82
96
  decision={item.decision}
83
97
  href={props.decisionHref(item.project.key, item.decision.id)}
84
98
  project={<InProject project={item.project} />}
85
- fileHref={fileHref === undefined ? undefined : (fileId) => fileHref(item.project.key, fileId)}
99
+ fileHref={(fileId) =>
100
+ fileHref === undefined
101
+ ? decisionFilePath(item.decision.id, fileId)
102
+ : fileHref(item.project.key, fileId, item.decision.id)
103
+ }
86
104
  onAnswer={
87
105
  answerable && onAnswer !== undefined
88
106
  ? (decision, input) => onAnswer(item.project.key, decision, input)
@@ -139,3 +157,30 @@ export function MyDecisions(props: MyDecisionsProps) {
139
157
  </div>
140
158
  );
141
159
  }
160
+
161
+ export interface ReachedDecisionProps {
162
+ /** The decision as the person reaches it (ADR-0068): its project, the decision, and whether they may answer it. */
163
+ readonly reached: RestReachedDecision;
164
+ /** Called with the person's answer; the decision is answered here only where `mayAnswer` says they may. */
165
+ readonly onAnswer?: ((decision: RestDecision, input: RestAnswerInput) => void) | undefined;
166
+ readonly busy?: boolean | undefined;
167
+ }
168
+
169
+ /**
170
+ * One decision wherever it is (ADR-0068), on its own page: the card with its project named, the
171
+ * files put on it read at the decision's own address, since the project may be one the person
172
+ * may not see, and the way to answer it where they may.
173
+ */
174
+ export function ReachedDecision(props: ReachedDecisionProps) {
175
+ const { reached } = props;
176
+ const { decision } = reached;
177
+ return (
178
+ <DecisionCard
179
+ decision={decision}
180
+ project={<InProject project={reached.project} />}
181
+ fileHref={(fileId) => decisionFilePath(decision.id, fileId)}
182
+ onAnswer={reached.mayAnswer === true ? props.onAnswer : undefined}
183
+ busy={props.busy}
184
+ />
185
+ );
186
+ }