@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.
- package/README.md +3 -3
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +162 -15
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +16 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +68 -19
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +26 -5
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +28 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +46 -15
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +0 -3
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +3 -15
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +30 -10
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/my-decisions.d.ts +20 -3
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +14 -1
- package/dist/components/my-decisions.js.map +1 -1
- package/dist/components/people.d.ts +43 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +117 -9
- package/dist/components/people.js.map +1 -1
- package/dist/components/picker.d.ts +8 -0
- package/dist/components/picker.d.ts.map +1 -1
- package/dist/components/picker.js +2 -2
- package/dist/components/picker.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +0 -1
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +22 -1
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts +5 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +143 -30
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -1
- package/dist/components/projects.js.map +1 -1
- package/dist/components/searches.d.ts +11 -0
- package/dist/components/searches.d.ts.map +1 -0
- package/dist/components/searches.js +30 -0
- package/dist/components/searches.js.map +1 -0
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +6 -5
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/ui.d.ts +16 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +10 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/console.css +362 -67
- package/dist/console.d.ts +2 -1
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +90 -14
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +36 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +89 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +87 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +87 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/args.d.ts +6 -1
- package/dist/plugin/args.d.ts.map +1 -1
- package/dist/plugin/args.js +16 -11
- package/dist/plugin/args.js.map +1 -1
- package/dist/plugin/bridge.d.ts +20 -1
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +28 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +1 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +10 -0
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +158 -13
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +13 -1
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +17 -1
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +4 -0
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +4 -0
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +15 -0
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +61 -4
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +44 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +55 -3
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/testing.d.ts +15 -1
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +40 -4
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +6 -0
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +10 -1
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/router.d.ts +19 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +18 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +11 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +14 -1
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +367 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +62 -2
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +1 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +6 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +7 -4
- package/docs/specs/plugins.md +47 -39
- package/docs/specs/rest.md +22 -5
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +8 -6
- package/skills/working-the-board/SKILL.md +1 -1
- package/src/api.ts +17 -0
- package/src/cli/cli.ts +200 -12
- package/src/cli/format.ts +82 -19
- package/src/cli/help.ts +30 -5
- package/src/client.ts +70 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +83 -42
- package/src/components/decision-form.tsx +13 -32
- package/src/components/event-feed.tsx +36 -9
- package/src/components/my-decisions.tsx +50 -5
- package/src/components/people.tsx +394 -37
- package/src/components/picker.tsx +11 -2
- package/src/components/plugin-frame.tsx +23 -0
- package/src/components/plugin-settings.tsx +346 -137
- package/src/components/projects.tsx +14 -14
- package/src/components/searches.ts +47 -0
- package/src/components/task-form.tsx +23 -28
- package/src/components/ui.tsx +36 -0
- package/src/console.tsx +229 -40
- package/src/data.ts +53 -1
- package/src/i18n/en.ts +98 -2
- package/src/i18n/ko.ts +95 -3
- package/src/index.ts +11 -0
- package/src/limits.ts +6 -0
- package/src/plugin/args.ts +16 -11
- package/src/plugin/bridge.ts +45 -0
- package/src/plugin/capabilities.ts +11 -0
- package/src/plugin/context.ts +170 -10
- package/src/plugin/define.ts +34 -0
- package/src/plugin/limits.ts +4 -0
- package/src/plugin/manifest.ts +74 -3
- package/src/plugin/rest-schemas.ts +62 -5
- package/src/plugin/testing.ts +61 -5
- package/src/plugin/web.ts +17 -0
- package/src/plugin.ts +1 -0
- package/src/router.ts +25 -0
- package/src/routes.ts +17 -2
- package/src/schemas.ts +81 -2
- package/src/styles/console.css +362 -67
- 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
|
|
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.
|
|
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
|
package/src/components/board.tsx
CHANGED
|
@@ -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
|
-
<
|
|
799
|
-
<
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
818
|
-
<
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
827
|
-
}
|
|
828
|
-
if (value === "mine") {
|
|
879
|
+
} else if (choice.kind === "mine") {
|
|
829
880
|
change({ mine: true, person: undefined });
|
|
830
|
-
|
|
881
|
+
} else {
|
|
882
|
+
change({ person: choice.person.id, mine: undefined });
|
|
831
883
|
}
|
|
832
|
-
change({ person: value, mine: undefined });
|
|
833
884
|
}}
|
|
834
|
-
|
|
835
|
-
|
|
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
|
|
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
|
-
<
|
|
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
|
-
|
|
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">×</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.
|
|
49
|
-
? data.
|
|
50
|
-
: data.
|
|
51
|
-
? feed.
|
|
52
|
-
:
|
|
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
|
|
144
|
-
? feed.event.
|
|
145
|
-
:
|
|
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 (
|
|
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
|
|
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
|
-
/**
|
|
31
|
-
|
|
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={
|
|
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
|
+
}
|