@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
|
@@ -3,6 +3,7 @@ import type { ConsoleClient } from "../client.js";
|
|
|
3
3
|
import type { Messages } from "../i18n/en.js";
|
|
4
4
|
import { useWords } from "../i18n/index.js";
|
|
5
5
|
import { PLUGIN_CAPABILITIES, type PluginCapability } from "../plugin/capabilities.js";
|
|
6
|
+
import { settingsHref } from "../router.js";
|
|
6
7
|
import type {
|
|
7
8
|
RestEvent,
|
|
8
9
|
RestPerson,
|
|
@@ -13,13 +14,17 @@ import type {
|
|
|
13
14
|
RestPluginGrantInput,
|
|
14
15
|
RestPluginSetting,
|
|
15
16
|
RestProject,
|
|
17
|
+
RestTeam,
|
|
16
18
|
} from "../schemas.js";
|
|
17
19
|
import { useAction } from "../use-action.js";
|
|
18
20
|
import { EventFeed } from "./event-feed.js";
|
|
21
|
+
import { Picker } from "./picker.js";
|
|
22
|
+
import { listSearch, peopleSearch, projectsSearch } from "./searches.js";
|
|
19
23
|
import {
|
|
20
24
|
Badge,
|
|
21
25
|
Button,
|
|
22
26
|
Callout,
|
|
27
|
+
Chip,
|
|
23
28
|
cx,
|
|
24
29
|
EmptyState,
|
|
25
30
|
formatInstant,
|
|
@@ -134,6 +139,10 @@ export interface PluginSettingsViewProps {
|
|
|
134
139
|
readonly people: readonly RestPerson[];
|
|
135
140
|
readonly language: string;
|
|
136
141
|
readonly client: ConsoleClient;
|
|
142
|
+
/** The plugins of the console, to grant a role of this one to another (ADR-0072); none when left out. */
|
|
143
|
+
readonly plugins?: readonly RestPlugin[] | undefined;
|
|
144
|
+
/** The teams of the workspace, to grant a role to one (ADR-0073); none when left out. */
|
|
145
|
+
readonly teams?: readonly RestTeam[] | undefined;
|
|
137
146
|
/** Called after a change, so that the page reads the plugin again. */
|
|
138
147
|
readonly onChanged: () => Promise<void>;
|
|
139
148
|
/** Where the list is, for after a purge. */
|
|
@@ -215,6 +224,13 @@ function Overview(props: {
|
|
|
215
224
|
} else if (enabled && detail.boardNeeds !== "none") {
|
|
216
225
|
rows.push({ key: "board", name: settings.board, value: settings.nothing });
|
|
217
226
|
}
|
|
227
|
+
if (detail.keeps !== undefined && detail.keeps.length > 0) {
|
|
228
|
+
rows.push({
|
|
229
|
+
key: "keeps",
|
|
230
|
+
name: settings.keeps,
|
|
231
|
+
value: detail.keeps.map((kept) => settings.keepsFor(kept.collection, kept.days)).join(", "),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
218
234
|
if (enabled && detail.projects !== null) {
|
|
219
235
|
const names = detail.projects;
|
|
220
236
|
rows.push({
|
|
@@ -401,6 +417,9 @@ function EnumListControl(props: {
|
|
|
401
417
|
);
|
|
402
418
|
}
|
|
403
419
|
|
|
420
|
+
/** One of a list setting's choices: a person or a project, by id, as it reads. */
|
|
421
|
+
type Candidate = { readonly id: string; readonly label: string };
|
|
422
|
+
|
|
404
423
|
/**
|
|
405
424
|
* A list of people or of projects: each chosen one a chip with the way to take it out, and one
|
|
406
425
|
* choice to add another among those not chosen yet; the value is their ids.
|
|
@@ -410,12 +429,13 @@ function RefListControl(props: {
|
|
|
410
429
|
readonly value: unknown;
|
|
411
430
|
readonly onChange: (value: unknown) => void;
|
|
412
431
|
readonly id: string;
|
|
432
|
+
readonly language: string;
|
|
413
433
|
readonly people: readonly RestPerson[];
|
|
414
434
|
readonly projects: readonly RestProject[];
|
|
415
435
|
}) {
|
|
416
436
|
const words = useWords();
|
|
417
437
|
const chosen = itemsOf(props.value).map(String);
|
|
418
|
-
const candidates: readonly
|
|
438
|
+
const candidates: readonly Candidate[] =
|
|
419
439
|
props.setting.of === "person"
|
|
420
440
|
? props.people
|
|
421
441
|
.filter((person) => person.status === "active" || chosen.includes(person.id))
|
|
@@ -430,38 +450,33 @@ function RefListControl(props: {
|
|
|
430
450
|
return (
|
|
431
451
|
<div className="sc-chips">
|
|
432
452
|
{chosen.map((id) => (
|
|
433
|
-
<
|
|
453
|
+
<Chip
|
|
454
|
+
key={id}
|
|
455
|
+
onRemove={() => set(chosen.filter((kept) => kept !== id))}
|
|
456
|
+
removeLabel={`${words.common.remove}: ${labelOf(id)}`}
|
|
457
|
+
>
|
|
434
458
|
{labelOf(id)}
|
|
435
|
-
|
|
436
|
-
type="button"
|
|
437
|
-
className="sc-chip-remove"
|
|
438
|
-
aria-label={`${words.common.remove}: ${labelOf(id)}`}
|
|
439
|
-
onClick={() => set(chosen.filter((kept) => kept !== id))}
|
|
440
|
-
>
|
|
441
|
-
<span aria-hidden="true">×</span>
|
|
442
|
-
</button>
|
|
443
|
-
</span>
|
|
459
|
+
</Chip>
|
|
444
460
|
))}
|
|
445
461
|
{!full && (
|
|
446
|
-
<
|
|
462
|
+
<Picker<Candidate>
|
|
447
463
|
id={props.id}
|
|
448
|
-
className="sc-
|
|
449
|
-
value=
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
464
|
+
className="sc-picker-add"
|
|
465
|
+
value={undefined}
|
|
466
|
+
search={listSearch(
|
|
467
|
+
candidates.filter((candidate) => !chosen.includes(candidate.id)),
|
|
468
|
+
(candidate, typed) => candidate.label.toLowerCase().includes(typed),
|
|
469
|
+
)}
|
|
470
|
+
keyOf={(candidate) => candidate.id}
|
|
471
|
+
render={(candidate) => candidate.label}
|
|
472
|
+
label={pluginWord(props.setting.name, props.language)}
|
|
473
|
+
placeholder={words.plugins.settings.addOne}
|
|
474
|
+
onChange={(candidate) => {
|
|
475
|
+
if (candidate !== undefined) {
|
|
476
|
+
set([...chosen, candidate.id]);
|
|
453
477
|
}
|
|
454
478
|
}}
|
|
455
|
-
|
|
456
|
-
<option value="">{words.plugins.settings.choose}</option>
|
|
457
|
-
{candidates
|
|
458
|
-
.filter((candidate) => !chosen.includes(candidate.id))
|
|
459
|
-
.map((candidate) => (
|
|
460
|
-
<option key={candidate.id} value={candidate.id}>
|
|
461
|
-
{candidate.label}
|
|
462
|
-
</option>
|
|
463
|
-
))}
|
|
464
|
-
</select>
|
|
479
|
+
/>
|
|
465
480
|
)}
|
|
466
481
|
</div>
|
|
467
482
|
);
|
|
@@ -498,18 +513,14 @@ function ItemListControl(props: {
|
|
|
498
513
|
return (
|
|
499
514
|
<div className="sc-chips">
|
|
500
515
|
{items.map((item, index) => (
|
|
501
|
-
|
|
502
|
-
|
|
516
|
+
<Chip
|
|
517
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: an item may be typed twice
|
|
518
|
+
key={`${index}-${String(item)}`}
|
|
519
|
+
onRemove={() => set(items.filter((_, at) => at !== index))}
|
|
520
|
+
removeLabel={`${words.common.remove}: ${String(item)}`}
|
|
521
|
+
>
|
|
503
522
|
{String(item)}
|
|
504
|
-
|
|
505
|
-
type="button"
|
|
506
|
-
className="sc-chip-remove"
|
|
507
|
-
aria-label={`${words.common.remove}: ${String(item)}`}
|
|
508
|
-
onClick={() => set(items.filter((_, at) => at !== index))}
|
|
509
|
-
>
|
|
510
|
-
<span aria-hidden="true">×</span>
|
|
511
|
-
</button>
|
|
512
|
-
</span>
|
|
523
|
+
</Chip>
|
|
513
524
|
))}
|
|
514
525
|
{!full && (
|
|
515
526
|
<div className="sc-chip-input">
|
|
@@ -553,42 +564,43 @@ function SettingControl(props: {
|
|
|
553
564
|
const words = useWords();
|
|
554
565
|
const { setting, value, onChange, id } = props;
|
|
555
566
|
const chosen = typeof value === "string" ? value : "";
|
|
556
|
-
const choose = (event: { readonly target: { readonly value: string } }) =>
|
|
557
|
-
onChange(event.target.value === "" ? null : event.target.value);
|
|
558
567
|
switch (setting.type) {
|
|
559
568
|
case "person": {
|
|
560
569
|
// The people in; the one chosen stays listed when they have left, so that the value shows.
|
|
561
570
|
const people = props.people.filter(
|
|
562
571
|
(person) => person.status === "active" || person.id === chosen,
|
|
563
572
|
);
|
|
573
|
+
const current = people.find((person) => person.id === chosen);
|
|
564
574
|
return (
|
|
565
|
-
<
|
|
566
|
-
|
|
567
|
-
{
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
{
|
|
573
|
-
|
|
574
|
-
)}
|
|
575
|
-
|
|
575
|
+
<Picker
|
|
576
|
+
id={id}
|
|
577
|
+
value={current}
|
|
578
|
+
held={chosen !== "" && current === undefined ? chosen : undefined}
|
|
579
|
+
search={peopleSearch(people)}
|
|
580
|
+
keyOf={(person) => person.id}
|
|
581
|
+
render={personChoice}
|
|
582
|
+
label={pluginWord(setting.name, props.language)}
|
|
583
|
+
placeholder={words.plugins.settings.choose}
|
|
584
|
+
onChange={(person) => onChange(person?.id ?? null)}
|
|
585
|
+
/>
|
|
576
586
|
);
|
|
577
587
|
}
|
|
578
|
-
case "project":
|
|
588
|
+
case "project": {
|
|
589
|
+
const current = props.projects.find((project) => project.id === chosen);
|
|
579
590
|
return (
|
|
580
|
-
<
|
|
581
|
-
|
|
582
|
-
{
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
))}
|
|
587
|
-
{
|
|
588
|
-
|
|
589
|
-
)}
|
|
590
|
-
|
|
591
|
+
<Picker
|
|
592
|
+
id={id}
|
|
593
|
+
value={current}
|
|
594
|
+
held={chosen !== "" && current === undefined ? chosen : undefined}
|
|
595
|
+
search={projectsSearch(props.projects)}
|
|
596
|
+
keyOf={(project) => project.id}
|
|
597
|
+
render={(project) => `${project.name} (${project.key})`}
|
|
598
|
+
label={pluginWord(setting.name, props.language)}
|
|
599
|
+
placeholder={words.plugins.settings.choose}
|
|
600
|
+
onChange={(project) => onChange(project?.id ?? null)}
|
|
601
|
+
/>
|
|
591
602
|
);
|
|
603
|
+
}
|
|
592
604
|
case "boolean":
|
|
593
605
|
return (
|
|
594
606
|
<input
|
|
@@ -665,6 +677,7 @@ function SettingControl(props: {
|
|
|
665
677
|
if (setting.of === "person" || setting.of === "project") {
|
|
666
678
|
return (
|
|
667
679
|
<RefListControl
|
|
680
|
+
language={props.language}
|
|
668
681
|
setting={setting}
|
|
669
682
|
value={value}
|
|
670
683
|
onChange={onChange}
|
|
@@ -771,6 +784,38 @@ function SettingsForm(props: {
|
|
|
771
784
|
);
|
|
772
785
|
}
|
|
773
786
|
|
|
787
|
+
/**
|
|
788
|
+
* How a secret stands, in a line (ADR-0070): set by an administrator, or sealed from an answer
|
|
789
|
+
* of a host, when; and when it expires, or that it has.
|
|
790
|
+
*/
|
|
791
|
+
function secretStanding(
|
|
792
|
+
settings: Messages["plugins"]["settings"],
|
|
793
|
+
setting: RestPluginSetting,
|
|
794
|
+
language: string,
|
|
795
|
+
): string {
|
|
796
|
+
const when = formatInstant(setting.setAt ?? "", language);
|
|
797
|
+
const source = setting.source ?? "administrator";
|
|
798
|
+
const set =
|
|
799
|
+
source === "administrator" ? settings.secretSet(when) : settings.secretSealedFrom(source, when);
|
|
800
|
+
if (setting.expiresAt === null || setting.expiresAt === undefined) {
|
|
801
|
+
return set;
|
|
802
|
+
}
|
|
803
|
+
const expires = formatInstant(setting.expiresAt, language);
|
|
804
|
+
return `${set} · ${
|
|
805
|
+
Date.parse(setting.expiresAt) <= Date.now()
|
|
806
|
+
? settings.secretExpired(expires)
|
|
807
|
+
: settings.secretExpires(expires)
|
|
808
|
+
}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** Whom a role may go to (ADR-0072): everyone, a project's members, a person, or a plugin. */
|
|
812
|
+
type GrantChoice =
|
|
813
|
+
| { readonly kind: "everyone" }
|
|
814
|
+
| { readonly kind: "project"; readonly project: RestProject }
|
|
815
|
+
| { readonly kind: "person"; readonly person: RestPerson }
|
|
816
|
+
| { readonly kind: "plugin"; readonly plugin: RestPlugin }
|
|
817
|
+
| { readonly kind: "team"; readonly team: RestTeam };
|
|
818
|
+
|
|
774
819
|
/** One secret: whether and when it is set, a field to set it anew with its buttons on the line. */
|
|
775
820
|
function SecretRow(props: {
|
|
776
821
|
readonly setting: RestPluginSetting;
|
|
@@ -793,9 +838,12 @@ function SecretRow(props: {
|
|
|
793
838
|
</label>
|
|
794
839
|
<p className="sc-settings-hint">
|
|
795
840
|
{setting.set && setting.setAt !== null
|
|
796
|
-
? settings
|
|
841
|
+
? secretStanding(settings, setting, props.language)
|
|
797
842
|
: settings.secretUnset}
|
|
798
843
|
</p>
|
|
844
|
+
{setting.hosts !== null && setting.hosts !== undefined && setting.hosts.length > 0 && (
|
|
845
|
+
<p className="sc-settings-hint">{settings.secretHosts(setting.hosts.join(", "))}</p>
|
|
846
|
+
)}
|
|
799
847
|
{setting.description !== null && (
|
|
800
848
|
<p className="sc-settings-hint">{pluginWord(setting.description, props.language)}</p>
|
|
801
849
|
)}
|
|
@@ -841,12 +889,18 @@ function EnableForm(props: {
|
|
|
841
889
|
readonly projects: readonly RestProject[];
|
|
842
890
|
readonly language: string;
|
|
843
891
|
readonly busy: boolean;
|
|
844
|
-
readonly onEnable: (
|
|
892
|
+
readonly onEnable: (
|
|
893
|
+
input: RestPluginEnableInput,
|
|
894
|
+
grants: readonly { readonly plugin: string; readonly role: string }[],
|
|
895
|
+
) => void;
|
|
845
896
|
}) {
|
|
846
897
|
const words = useWords();
|
|
847
898
|
const settings = words.plugins.settings;
|
|
848
899
|
const { detail, language } = props;
|
|
849
900
|
const optional = detail.optionalCapabilities ?? [];
|
|
901
|
+
// The roles the plugin asks of other plugins (ADR-0072): granted on the way where ticked.
|
|
902
|
+
const asks = (detail.asks ?? []).filter((ask) => ask.state !== "granted");
|
|
903
|
+
const [askedNow, setAskedNow] = useState<ReadonlySet<string>>(() => new Set());
|
|
850
904
|
const [consented, setConsented] = useState<ReadonlySet<string>>(
|
|
851
905
|
() => new Set((detail.consent ?? []).filter((capability) => optional.includes(capability))),
|
|
852
906
|
);
|
|
@@ -883,16 +937,21 @@ function EnableForm(props: {
|
|
|
883
937
|
: chosen.length > 0;
|
|
884
938
|
const submit = (event: FormEvent) => {
|
|
885
939
|
event.preventDefault();
|
|
886
|
-
props.onEnable(
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
940
|
+
props.onEnable(
|
|
941
|
+
{
|
|
942
|
+
consent: [...detail.capabilities, ...consented],
|
|
943
|
+
board:
|
|
944
|
+
boardMode === "none"
|
|
945
|
+
? null
|
|
946
|
+
: boardMode === "create"
|
|
947
|
+
? { create: { key: boardKey, name: boardName, description: boardDescription } }
|
|
948
|
+
: { project: chosen },
|
|
949
|
+
projects: allProjects ? "all" : [...onProjects],
|
|
950
|
+
},
|
|
951
|
+
asks
|
|
952
|
+
.filter((ask) => ask.state === "ungranted" && askedNow.has(`${ask.plugin}:${ask.role}`))
|
|
953
|
+
.map((ask) => ({ plugin: ask.plugin, role: ask.role })),
|
|
954
|
+
);
|
|
896
955
|
};
|
|
897
956
|
return (
|
|
898
957
|
<form className="sc-form sc-plugin-enable" onSubmit={submit} aria-label={settings.enable}>
|
|
@@ -1024,18 +1083,16 @@ function EnableForm(props: {
|
|
|
1024
1083
|
<label className="sc-field-label" htmlFor={ids.choose}>
|
|
1025
1084
|
{settings.boardChoose}
|
|
1026
1085
|
</label>
|
|
1027
|
-
<
|
|
1086
|
+
<Picker
|
|
1028
1087
|
id={ids.choose}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
{
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
))}
|
|
1038
|
-
</select>
|
|
1088
|
+
value={props.projects.find((project) => project.key === chosen)}
|
|
1089
|
+
search={projectsSearch(props.projects)}
|
|
1090
|
+
keyOf={(project) => project.key}
|
|
1091
|
+
render={(project) => `${project.name} (${project.key})`}
|
|
1092
|
+
label={settings.boardChoose}
|
|
1093
|
+
placeholder={settings.boardChoose}
|
|
1094
|
+
onChange={(project) => setChosen(project?.key ?? "")}
|
|
1095
|
+
/>
|
|
1039
1096
|
</div>
|
|
1040
1097
|
</div>
|
|
1041
1098
|
)}
|
|
@@ -1078,6 +1135,44 @@ function EnableForm(props: {
|
|
|
1078
1135
|
)}
|
|
1079
1136
|
</div>
|
|
1080
1137
|
<div className="sc-plugin-group">
|
|
1138
|
+
{asks.length > 0 && (
|
|
1139
|
+
<div className="sc-plugin-group">
|
|
1140
|
+
<h3 className="sc-plugin-group-title">{settings.asks}</h3>
|
|
1141
|
+
<p className="sc-settings-hint">{settings.asksHint}</p>
|
|
1142
|
+
<ul className="sc-plugin-ticks">
|
|
1143
|
+
{asks.map((ask) => {
|
|
1144
|
+
const id = `${ask.plugin}:${ask.role}`;
|
|
1145
|
+
return (
|
|
1146
|
+
<TickRow
|
|
1147
|
+
key={id}
|
|
1148
|
+
checked={ask.state === "ungranted" && askedNow.has(id)}
|
|
1149
|
+
onChange={
|
|
1150
|
+
ask.state === "ungranted"
|
|
1151
|
+
? (checked) =>
|
|
1152
|
+
setAskedNow((current) => {
|
|
1153
|
+
const next = new Set(current);
|
|
1154
|
+
if (checked) {
|
|
1155
|
+
next.add(id);
|
|
1156
|
+
} else {
|
|
1157
|
+
next.delete(id);
|
|
1158
|
+
}
|
|
1159
|
+
return next;
|
|
1160
|
+
})
|
|
1161
|
+
: undefined
|
|
1162
|
+
}
|
|
1163
|
+
locked={ask.state === "unavailable"}
|
|
1164
|
+
name={`${ask.role} (${ask.plugin})`}
|
|
1165
|
+
under={
|
|
1166
|
+
ask.state === "unavailable"
|
|
1167
|
+
? settings.askUnavailable(ask.plugin)
|
|
1168
|
+
: pluginWord(ask.why, language)
|
|
1169
|
+
}
|
|
1170
|
+
/>
|
|
1171
|
+
);
|
|
1172
|
+
})}
|
|
1173
|
+
</ul>
|
|
1174
|
+
</div>
|
|
1175
|
+
)}
|
|
1081
1176
|
<div className="sc-form-actions">
|
|
1082
1177
|
<Button type="submit" variant="primary" disabled={props.busy || !boardReady}>
|
|
1083
1178
|
{settings.enable}
|
|
@@ -1096,6 +1191,12 @@ function holderOf(grant: RestPluginGrant, words: Messages["plugins"]["settings"]
|
|
|
1096
1191
|
if (grant.project !== null) {
|
|
1097
1192
|
return words.grantMembersOf(grant.project.name);
|
|
1098
1193
|
}
|
|
1194
|
+
if (grant.plugin !== null && grant.plugin !== undefined) {
|
|
1195
|
+
return words.grantPlugin(grant.plugin);
|
|
1196
|
+
}
|
|
1197
|
+
if (grant.team !== null && grant.team !== undefined) {
|
|
1198
|
+
return words.grantTeam(grant.team.name);
|
|
1199
|
+
}
|
|
1099
1200
|
if (grant.person !== null) {
|
|
1100
1201
|
return personName(grant.person);
|
|
1101
1202
|
}
|
|
@@ -1119,13 +1220,17 @@ function RoleRow(props: {
|
|
|
1119
1220
|
readonly grants: readonly RestPluginGrant[];
|
|
1120
1221
|
readonly projects: readonly RestProject[];
|
|
1121
1222
|
readonly people: readonly RestPerson[];
|
|
1223
|
+
/** The other plugins that are enabled, to hold the role as themselves (ADR-0072). */
|
|
1224
|
+
readonly plugins: readonly RestPlugin[];
|
|
1225
|
+
/** The teams, whose members hold the role granted to one (ADR-0073). */
|
|
1226
|
+
readonly teams: readonly RestTeam[];
|
|
1122
1227
|
readonly language: string;
|
|
1123
1228
|
readonly busy: boolean;
|
|
1124
1229
|
readonly onGrant: (input: RestPluginGrantInput) => void;
|
|
1125
1230
|
readonly onRevoke: (id: string) => void;
|
|
1126
1231
|
}) {
|
|
1127
1232
|
const settings = useWords().plugins.settings;
|
|
1128
|
-
const [whom, setWhom] = useState(
|
|
1233
|
+
const [whom, setWhom] = useState<GrantChoice | undefined>(undefined);
|
|
1129
1234
|
const id = useId();
|
|
1130
1235
|
const { role, grants } = props;
|
|
1131
1236
|
const everyoneHas = grants.some((grant) => grant.everyone);
|
|
@@ -1135,21 +1240,76 @@ function RoleRow(props: {
|
|
|
1135
1240
|
const peopleWith = new Set(
|
|
1136
1241
|
grants.flatMap((grant) => (grant.person === null ? [] : [grant.person.id])),
|
|
1137
1242
|
);
|
|
1243
|
+
const pluginsWith = new Set(
|
|
1244
|
+
grants.flatMap((grant) =>
|
|
1245
|
+
grant.plugin === null || grant.plugin === undefined ? [] : [grant.plugin],
|
|
1246
|
+
),
|
|
1247
|
+
);
|
|
1248
|
+
const plugins = props.plugins.filter((plugin) => !pluginsWith.has(plugin.key));
|
|
1249
|
+
const teamsWith = new Set(
|
|
1250
|
+
grants.flatMap((grant) =>
|
|
1251
|
+
grant.team === null || grant.team === undefined ? [] : [grant.team.key],
|
|
1252
|
+
),
|
|
1253
|
+
);
|
|
1254
|
+
const teams = props.teams.filter((team) => !teamsWith.has(team.key));
|
|
1138
1255
|
const projects = props.projects.filter((project) => !projectsWith.has(project.key));
|
|
1139
1256
|
const people = props.people.filter(
|
|
1140
1257
|
(person) => person.status === "active" && !peopleWith.has(person.id),
|
|
1141
1258
|
);
|
|
1142
1259
|
const description = pluginWord(role.description, props.language);
|
|
1260
|
+
// Whom the role may go to, searched for as one list (ADR-0037): everyone where it may, then
|
|
1261
|
+
// the projects, the people and the plugins without it.
|
|
1262
|
+
const choices: readonly GrantChoice[] = [
|
|
1263
|
+
...(!props.isDefault && !everyoneHas ? [{ kind: "everyone" as const }] : []),
|
|
1264
|
+
...projects.map((project) => ({ kind: "project" as const, project })),
|
|
1265
|
+
...people.map((person) => ({ kind: "person" as const, person })),
|
|
1266
|
+
...plugins.map((plugin) => ({ kind: "plugin" as const, plugin })),
|
|
1267
|
+
...teams.map((team) => ({ kind: "team" as const, team })),
|
|
1268
|
+
];
|
|
1269
|
+
const choiceWords = (choice: GrantChoice): string => {
|
|
1270
|
+
switch (choice.kind) {
|
|
1271
|
+
case "everyone":
|
|
1272
|
+
return settings.grantEveryone;
|
|
1273
|
+
case "project":
|
|
1274
|
+
return settings.grantMembersOf(choice.project.name);
|
|
1275
|
+
case "person":
|
|
1276
|
+
return personChoice(choice.person);
|
|
1277
|
+
case "team":
|
|
1278
|
+
return settings.grantTeam(choice.team.name);
|
|
1279
|
+
case "plugin":
|
|
1280
|
+
return `${settings.grantPlugin(choice.plugin.key)} · ${pluginWord(choice.plugin.name, props.language)}`;
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
const findWhom = listSearch(choices, (choice, typed) =>
|
|
1284
|
+
choiceWords(choice).toLowerCase().includes(typed),
|
|
1285
|
+
);
|
|
1286
|
+
const keyOfChoice = (choice: GrantChoice): string =>
|
|
1287
|
+
choice.kind === "everyone"
|
|
1288
|
+
? "everyone"
|
|
1289
|
+
: choice.kind === "project"
|
|
1290
|
+
? `project:${choice.project.key}`
|
|
1291
|
+
: choice.kind === "person"
|
|
1292
|
+
? `person:${choice.person.id}`
|
|
1293
|
+
: choice.kind === "team"
|
|
1294
|
+
? `team:${choice.team.key}`
|
|
1295
|
+
: `plugin:${choice.plugin.key}`;
|
|
1143
1296
|
const submit = (event: FormEvent) => {
|
|
1144
1297
|
event.preventDefault();
|
|
1145
|
-
if (whom ===
|
|
1298
|
+
if (whom === undefined) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (whom.kind === "everyone") {
|
|
1146
1302
|
props.onGrant({ role: role.key, everyone: true });
|
|
1147
|
-
} else if (whom.
|
|
1148
|
-
props.onGrant({ role: role.key, project: whom.
|
|
1149
|
-
} else if (whom.
|
|
1150
|
-
props.onGrant({ role: role.key, person: whom.
|
|
1303
|
+
} else if (whom.kind === "project") {
|
|
1304
|
+
props.onGrant({ role: role.key, project: whom.project.key });
|
|
1305
|
+
} else if (whom.kind === "person") {
|
|
1306
|
+
props.onGrant({ role: role.key, person: whom.person.id });
|
|
1307
|
+
} else if (whom.kind === "team") {
|
|
1308
|
+
props.onGrant({ role: role.key, team: whom.team.key });
|
|
1309
|
+
} else {
|
|
1310
|
+
props.onGrant({ role: role.key, plugin: whom.plugin.key });
|
|
1151
1311
|
}
|
|
1152
|
-
setWhom(
|
|
1312
|
+
setWhom(undefined);
|
|
1153
1313
|
};
|
|
1154
1314
|
return (
|
|
1155
1315
|
<li className="sc-plugin-role">
|
|
@@ -1158,30 +1318,28 @@ function RoleRow(props: {
|
|
|
1158
1318
|
{pluginWord(role.name, props.language)}
|
|
1159
1319
|
<code className="sc-plugin-role-key">{role.key}</code>
|
|
1160
1320
|
{props.isDefault && <Badge tone="accent">{settings.roleDefault}</Badge>}
|
|
1321
|
+
{role.follows === "reports" && <Badge tone="accent">{settings.roleManagers}</Badge>}
|
|
1161
1322
|
</p>
|
|
1162
1323
|
{description !== "" && <p className="sc-settings-hint">{description}</p>}
|
|
1163
1324
|
</div>
|
|
1164
1325
|
<div>
|
|
1165
|
-
<div className="sc-plugin-holders">
|
|
1166
|
-
{props.isDefault &&
|
|
1167
|
-
|
|
1326
|
+
<div className="sc-chips sc-plugin-holders">
|
|
1327
|
+
{props.isDefault && <Chip className="sc-plugin-holder">{settings.everyMember}</Chip>}
|
|
1328
|
+
{role.follows === "reports" && (
|
|
1329
|
+
<Chip className="sc-plugin-holder">{settings.whoeverManages}</Chip>
|
|
1168
1330
|
)}
|
|
1169
1331
|
{grants.map((grant) => {
|
|
1170
1332
|
const holder = holderOf(grant, settings);
|
|
1171
1333
|
return (
|
|
1172
|
-
<
|
|
1334
|
+
<Chip
|
|
1335
|
+
key={grant.id}
|
|
1336
|
+
className="sc-plugin-holder"
|
|
1337
|
+
onRemove={() => props.onRevoke(grant.id)}
|
|
1338
|
+
removeLabel={`${settings.revoke}: ${holder}`}
|
|
1339
|
+
disabled={props.busy}
|
|
1340
|
+
>
|
|
1173
1341
|
{holder}
|
|
1174
|
-
|
|
1175
|
-
size="sm"
|
|
1176
|
-
variant="ghost"
|
|
1177
|
-
disabled={props.busy}
|
|
1178
|
-
onClick={() => props.onRevoke(grant.id)}
|
|
1179
|
-
aria-label={`${settings.revoke}: ${holder}`}
|
|
1180
|
-
title={settings.revoke}
|
|
1181
|
-
>
|
|
1182
|
-
×
|
|
1183
|
-
</Button>
|
|
1184
|
-
</span>
|
|
1342
|
+
</Chip>
|
|
1185
1343
|
);
|
|
1186
1344
|
})}
|
|
1187
1345
|
</div>
|
|
@@ -1189,36 +1347,18 @@ function RoleRow(props: {
|
|
|
1189
1347
|
<label htmlFor={id} className="sc-visually-hidden">
|
|
1190
1348
|
{settings.grantWhom}
|
|
1191
1349
|
</label>
|
|
1192
|
-
<
|
|
1350
|
+
<Picker
|
|
1193
1351
|
id={id}
|
|
1194
|
-
className="sc-input sc-plugin-grant-input"
|
|
1195
1352
|
value={whom}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
{
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
{
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
<option key={project.key} value={`project:${project.key}`}>
|
|
1206
|
-
{project.name}
|
|
1207
|
-
</option>
|
|
1208
|
-
))}
|
|
1209
|
-
</optgroup>
|
|
1210
|
-
)}
|
|
1211
|
-
{people.length > 0 && (
|
|
1212
|
-
<optgroup label={settings.grantPeople}>
|
|
1213
|
-
{people.map((person) => (
|
|
1214
|
-
<option key={person.id} value={`person:${person.id}`}>
|
|
1215
|
-
{personChoice(person)}
|
|
1216
|
-
</option>
|
|
1217
|
-
))}
|
|
1218
|
-
</optgroup>
|
|
1219
|
-
)}
|
|
1220
|
-
</select>
|
|
1221
|
-
<Button type="submit" disabled={props.busy || whom === ""}>
|
|
1353
|
+
search={findWhom}
|
|
1354
|
+
keyOf={keyOfChoice}
|
|
1355
|
+
render={choiceWords}
|
|
1356
|
+
label={settings.grantWhom}
|
|
1357
|
+
placeholder={settings.grantWhom}
|
|
1358
|
+
disabled={props.busy}
|
|
1359
|
+
onChange={setWhom}
|
|
1360
|
+
/>
|
|
1361
|
+
<Button type="submit" disabled={props.busy || whom === undefined}>
|
|
1222
1362
|
{settings.grant}
|
|
1223
1363
|
</Button>
|
|
1224
1364
|
</form>
|
|
@@ -1227,19 +1367,33 @@ function RoleRow(props: {
|
|
|
1227
1367
|
);
|
|
1228
1368
|
}
|
|
1229
1369
|
|
|
1370
|
+
/** Another plugin by the name it gives itself in the person's language, or its key where the page does not know it. */
|
|
1371
|
+
function pluginNameOf(plugins: readonly RestPlugin[], key: string, language: string): string {
|
|
1372
|
+
const found = plugins.find((plugin) => plugin.key === key);
|
|
1373
|
+
return found === undefined ? key : pluginWord(found.name, language);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1230
1376
|
/** The roles the plugin declares, each with who holds it; a plugin without roles is every member's. */
|
|
1231
1377
|
function RolesSection(props: {
|
|
1232
1378
|
readonly detail: RestPluginDetail;
|
|
1233
1379
|
readonly projects: readonly RestProject[];
|
|
1234
1380
|
readonly people: readonly RestPerson[];
|
|
1381
|
+
readonly plugins: readonly RestPlugin[];
|
|
1382
|
+
readonly teams: readonly RestTeam[];
|
|
1235
1383
|
readonly language: string;
|
|
1236
1384
|
readonly busy: boolean;
|
|
1237
1385
|
readonly onGrant: (input: RestPluginGrantInput) => void;
|
|
1238
1386
|
readonly onRevoke: (id: string) => void;
|
|
1387
|
+
/** Grants a role this plugin asks of another plugin (ADR-0072): the other plugin's key, and the role. */
|
|
1388
|
+
readonly onGrantAsked: (plugin: string, role: string) => void;
|
|
1239
1389
|
}) {
|
|
1240
1390
|
const settings = useWords().plugins.settings;
|
|
1241
1391
|
const { detail } = props;
|
|
1242
1392
|
const grants = detail.grants ?? [];
|
|
1393
|
+
const others = props.plugins.filter(
|
|
1394
|
+
(plugin) => plugin.state === "enabled" && plugin.key !== detail.key,
|
|
1395
|
+
);
|
|
1396
|
+
const asks = detail.asks ?? [];
|
|
1243
1397
|
return (
|
|
1244
1398
|
<section className="sc-panel" aria-label={settings.roles}>
|
|
1245
1399
|
<h2 className="sc-section-title">{settings.roles}</h2>
|
|
@@ -1255,6 +1409,8 @@ function RolesSection(props: {
|
|
|
1255
1409
|
grants={grants.filter((grant) => grant.role === role.key)}
|
|
1256
1410
|
projects={props.projects}
|
|
1257
1411
|
people={props.people}
|
|
1412
|
+
plugins={others}
|
|
1413
|
+
teams={props.teams}
|
|
1258
1414
|
language={props.language}
|
|
1259
1415
|
busy={props.busy}
|
|
1260
1416
|
onGrant={props.onGrant}
|
|
@@ -1263,6 +1419,39 @@ function RolesSection(props: {
|
|
|
1263
1419
|
))}
|
|
1264
1420
|
</ul>
|
|
1265
1421
|
)}
|
|
1422
|
+
{asks.length > 0 && (
|
|
1423
|
+
<div className="sc-plugin-group">
|
|
1424
|
+
<h3 className="sc-plugin-group-title">{settings.asks}</h3>
|
|
1425
|
+
<p className="sc-settings-hint">{settings.asksHint}</p>
|
|
1426
|
+
<ul className="sc-plugin-roles">
|
|
1427
|
+
{asks.map((ask) => (
|
|
1428
|
+
<li key={`${ask.plugin}:${ask.role}`} className="sc-plugin-role">
|
|
1429
|
+
<div>
|
|
1430
|
+
<p className="sc-plugin-role-name">
|
|
1431
|
+
{pluginNameOf(props.plugins, ask.plugin, props.language)}
|
|
1432
|
+
<code className="sc-plugin-role-key">{ask.role}</code>
|
|
1433
|
+
{ask.state === "granted" && <Badge tone="accent">{settings.askGranted}</Badge>}
|
|
1434
|
+
</p>
|
|
1435
|
+
<p className="sc-settings-hint">{pluginWord(ask.why, props.language)}</p>
|
|
1436
|
+
</div>
|
|
1437
|
+
<div>
|
|
1438
|
+
{ask.state === "unavailable" ? (
|
|
1439
|
+
<span className="sc-settings-hint">{settings.askUnavailable(ask.plugin)}</span>
|
|
1440
|
+
) : ask.state === "ungranted" ? (
|
|
1441
|
+
<Button
|
|
1442
|
+
size="sm"
|
|
1443
|
+
disabled={props.busy}
|
|
1444
|
+
onClick={() => props.onGrantAsked(ask.plugin, ask.role)}
|
|
1445
|
+
>
|
|
1446
|
+
{settings.askGrantNow}
|
|
1447
|
+
</Button>
|
|
1448
|
+
) : null}
|
|
1449
|
+
</div>
|
|
1450
|
+
</li>
|
|
1451
|
+
))}
|
|
1452
|
+
</ul>
|
|
1453
|
+
</div>
|
|
1454
|
+
)}
|
|
1266
1455
|
</section>
|
|
1267
1456
|
);
|
|
1268
1457
|
}
|
|
@@ -1330,6 +1519,14 @@ export function PluginSettingsView(props: PluginSettingsViewProps) {
|
|
|
1330
1519
|
</ul>
|
|
1331
1520
|
</Callout>
|
|
1332
1521
|
)}
|
|
1522
|
+
{detail.needsTimeZone === true && (
|
|
1523
|
+
<Callout
|
|
1524
|
+
tone="warning"
|
|
1525
|
+
action={<LinkButton href={settingsHref("workspace")}>{settings.setTimeZone}</LinkButton>}
|
|
1526
|
+
>
|
|
1527
|
+
{settings.needsTimeZone}
|
|
1528
|
+
</Callout>
|
|
1529
|
+
)}
|
|
1333
1530
|
<Overview
|
|
1334
1531
|
detail={detail}
|
|
1335
1532
|
projects={props.projects}
|
|
@@ -1345,7 +1542,14 @@ export function PluginSettingsView(props: PluginSettingsViewProps) {
|
|
|
1345
1542
|
projects={props.projects}
|
|
1346
1543
|
language={language}
|
|
1347
1544
|
busy={busy}
|
|
1348
|
-
onEnable={(input
|
|
1545
|
+
onEnable={(input, grants) =>
|
|
1546
|
+
change(async () => {
|
|
1547
|
+
await plugin.enable(input);
|
|
1548
|
+
for (const grant of grants) {
|
|
1549
|
+
await client.plugin(grant.plugin).grant({ role: grant.role, plugin: detail.key });
|
|
1550
|
+
}
|
|
1551
|
+
})
|
|
1552
|
+
}
|
|
1349
1553
|
/>
|
|
1350
1554
|
</section>
|
|
1351
1555
|
)}
|
|
@@ -1379,6 +1583,11 @@ export function PluginSettingsView(props: PluginSettingsViewProps) {
|
|
|
1379
1583
|
{detail.grants !== null && (
|
|
1380
1584
|
<RolesSection
|
|
1381
1585
|
detail={detail}
|
|
1586
|
+
plugins={props.plugins ?? []}
|
|
1587
|
+
teams={props.teams ?? []}
|
|
1588
|
+
onGrantAsked={(other, role) =>
|
|
1589
|
+
change(() => client.plugin(other).grant({ role, plugin: detail.key }))
|
|
1590
|
+
}
|
|
1382
1591
|
projects={props.projects}
|
|
1383
1592
|
people={props.people}
|
|
1384
1593
|
language={language}
|