@skillcdn/console 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/api.d.ts +4 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +255 -33
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +20 -5
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +98 -26
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +39 -15
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +5 -1
- package/dist/cli/plugin-check.d.ts.map +1 -1
- package/dist/cli/plugin-check.js +23 -4
- package/dist/cli/plugin-check.js.map +1 -1
- package/dist/cli/plugin-cli.d.ts.map +1 -1
- package/dist/cli/plugin-cli.js +10 -1
- package/dist/cli/plugin-cli.js.map +1 -1
- package/dist/cli/plugin-init.d.ts +2 -0
- package/dist/cli/plugin-init.d.ts.map +1 -1
- package/dist/cli/plugin-init.js +6 -0
- package/dist/cli/plugin-init.js.map +1 -1
- package/dist/client.d.ts +32 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +49 -18
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +6 -3
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +24 -18
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +5 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +25 -5
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +34 -8
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +2 -2
- package/dist/components/goals.js.map +1 -1
- package/dist/components/home.d.ts +35 -0
- package/dist/components/home.d.ts.map +1 -0
- package/dist/components/home.js +326 -0
- package/dist/components/home.js.map +1 -0
- 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/pack-connect.d.ts.map +1 -1
- package/dist/components/pack-connect.js +5 -4
- package/dist/components/pack-connect.js.map +1 -1
- package/dist/components/people.d.ts +45 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +125 -4
- 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 +3 -13
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +75 -21
- 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 +283 -45
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts +0 -8
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +4 -14
- 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/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +2 -1
- package/dist/components/task-view.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 +15 -2
- package/dist/components/ui.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +5 -6
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +1123 -258
- package/dist/console.d.ts +12 -4
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +123 -27
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +18 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +41 -1
- package/dist/data.js.map +1 -1
- package/dist/docs-page.d.ts.map +1 -1
- package/dist/docs-page.js +6 -5
- package/dist/docs-page.js.map +1 -1
- package/dist/home-tiles.d.ts +92 -0
- package/dist/home-tiles.d.ts.map +1 -0
- package/dist/home-tiles.js +294 -0
- package/dist/home-tiles.js.map +1 -0
- package/dist/i18n/en.d.ts +264 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +260 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/index.d.ts +4 -2
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +10 -11
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +255 -4
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +12 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +12 -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 +63 -3
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +54 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +7 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +24 -1
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +174 -12
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +14 -1
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +18 -2
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +9 -2
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +9 -2
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +98 -6
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +171 -10
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +120 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +78 -5
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/sandbox.d.ts +4 -3
- package/dist/plugin/sandbox.d.ts.map +1 -1
- package/dist/plugin/sandbox.js +33 -6
- package/dist/plugin/sandbox.js.map +1 -1
- package/dist/plugin/semver.d.ts +26 -0
- package/dist/plugin/semver.d.ts.map +1 -0
- package/dist/plugin/semver.js +76 -0
- package/dist/plugin/semver.js.map +1 -0
- package/dist/plugin/testing.d.ts +15 -1
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +42 -5
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +16 -1
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +44 -1
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +3 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -2
- 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/rules.d.ts +2 -2
- package/dist/rules.js +3 -3
- package/dist/schemas.d.ts +864 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +156 -9
- package/dist/schemas.js.map +1 -1
- package/dist/use-action.d.ts +8 -2
- package/dist/use-action.d.ts.map +1 -1
- package/dist/use-action.js +13 -6
- package/dist/use-action.js.map +1 -1
- package/dist/vocabulary.d.ts +3 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +9 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +10 -6
- package/docs/specs/plugins.md +83 -62
- package/docs/specs/rest.md +34 -16
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +15 -12
- package/skills/planning-the-board/references/setting-up.md +2 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +23 -0
- package/src/cli/cli.ts +323 -30
- package/src/cli/format.ts +122 -26
- package/src/cli/help.ts +43 -14
- package/src/cli/plugin-check.ts +41 -2
- package/src/cli/plugin-cli.ts +10 -1
- package/src/cli/plugin-init.ts +8 -0
- package/src/client.ts +78 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +86 -45
- package/src/components/decision-form.tsx +73 -36
- package/src/components/decision-list.tsx +45 -13
- package/src/components/event-feed.tsx +40 -7
- package/src/components/goals.tsx +4 -1
- package/src/components/home.tsx +615 -0
- package/src/components/my-decisions.tsx +50 -5
- package/src/components/pack-connect.tsx +5 -4
- package/src/components/people.tsx +500 -61
- package/src/components/picker.tsx +11 -2
- package/src/components/plugin-frame.tsx +85 -63
- package/src/components/plugin-settings.tsx +735 -185
- package/src/components/projects.tsx +14 -69
- package/src/components/searches.ts +47 -0
- package/src/components/task-form.tsx +23 -28
- package/src/components/task-view.tsx +7 -0
- package/src/components/ui.tsx +42 -2
- package/src/connect-page.tsx +5 -6
- package/src/console.tsx +303 -64
- package/src/data.ts +62 -3
- package/src/docs-page.tsx +6 -5
- package/src/home-tiles.ts +438 -0
- package/src/i18n/en.ts +274 -0
- package/src/i18n/index.ts +17 -14
- package/src/i18n/ko.ts +267 -4
- package/src/index.ts +18 -5
- package/src/limits.ts +12 -0
- package/src/plugin/args.ts +16 -11
- package/src/plugin/bridge.ts +122 -1
- package/src/plugin/capabilities.ts +28 -1
- package/src/plugin/context.ts +186 -9
- package/src/plugin/define.ts +38 -1
- package/src/plugin/limits.ts +9 -2
- package/src/plugin/manifest.ts +228 -13
- package/src/plugin/rest-schemas.ts +89 -6
- package/src/plugin/sandbox.ts +36 -8
- package/src/plugin/semver.ts +89 -0
- package/src/plugin/testing.ts +63 -6
- package/src/plugin/web.ts +66 -0
- package/src/plugin.ts +19 -0
- package/src/router.ts +25 -0
- package/src/routes.ts +17 -2
- package/src/rules.ts +3 -3
- package/src/schemas.ts +197 -10
- package/src/styles/console.css +1123 -258
- package/src/use-action.ts +16 -7
- package/src/vocabulary.ts +10 -0
- package/template/plugin/console-plugin.json +3 -1
|
@@ -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,18 @@ 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,
|
|
28
|
+
cx,
|
|
23
29
|
EmptyState,
|
|
24
30
|
formatInstant,
|
|
25
31
|
LinkButton,
|
|
@@ -110,6 +116,9 @@ export function PluginSettingsList(props: PluginSettingsListProps) {
|
|
|
110
116
|
{pluginWord(plugin.name, props.language)}{" "}
|
|
111
117
|
<span className="sc-muted">
|
|
112
118
|
{plugin.key} {plugin.version}
|
|
119
|
+
{plugin.console !== null && plugin.console !== undefined && (
|
|
120
|
+
<> · {settings.consoleRange(plugin.console)}</>
|
|
121
|
+
)}
|
|
113
122
|
</span>
|
|
114
123
|
</a>
|
|
115
124
|
<span className="sc-task-row-meta">
|
|
@@ -130,6 +139,10 @@ export interface PluginSettingsViewProps {
|
|
|
130
139
|
readonly people: readonly RestPerson[];
|
|
131
140
|
readonly language: string;
|
|
132
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;
|
|
133
146
|
/** Called after a change, so that the page reads the plugin again. */
|
|
134
147
|
readonly onChanged: () => Promise<void>;
|
|
135
148
|
/** Where the list is, for after a purge. */
|
|
@@ -150,20 +163,28 @@ function toggled(set: ReadonlySet<string>, value: string, on: boolean): Readonly
|
|
|
150
163
|
/** A row of a list to tick: the box, a name, a word under it. */
|
|
151
164
|
function TickRow(props: {
|
|
152
165
|
readonly checked: boolean;
|
|
153
|
-
readonly onChange
|
|
166
|
+
readonly onChange?: ((checked: boolean) => void) | undefined;
|
|
154
167
|
readonly name: string;
|
|
155
168
|
readonly under?: string | undefined;
|
|
169
|
+
/** A word beside the name: that this one is new, say. */
|
|
170
|
+
readonly tag?: string | undefined;
|
|
171
|
+
/** Whether it stays as it is: on, and not the person's to turn off. */
|
|
172
|
+
readonly locked?: boolean | undefined;
|
|
156
173
|
}) {
|
|
157
174
|
return (
|
|
158
175
|
<li>
|
|
159
|
-
<label className="sc-plugin-tick">
|
|
176
|
+
<label className={cx("sc-plugin-tick", props.locked === true && "sc-plugin-tick-locked")}>
|
|
160
177
|
<input
|
|
161
178
|
type="checkbox"
|
|
162
179
|
checked={props.checked}
|
|
163
|
-
|
|
180
|
+
disabled={props.locked === true}
|
|
181
|
+
onChange={(event) => props.onChange?.(event.target.checked)}
|
|
164
182
|
/>
|
|
165
183
|
<span className="sc-plugin-tick-words">
|
|
166
|
-
<span className="sc-plugin-tick-name">
|
|
184
|
+
<span className="sc-plugin-tick-name">
|
|
185
|
+
{props.name}
|
|
186
|
+
{props.tag !== undefined && <Badge tone="warning">{props.tag}</Badge>}
|
|
187
|
+
</span>
|
|
167
188
|
{props.under !== undefined && props.under !== "" && (
|
|
168
189
|
<span className="sc-plugin-tick-under">{props.under}</span>
|
|
169
190
|
)}
|
|
@@ -178,12 +199,18 @@ function Overview(props: {
|
|
|
178
199
|
readonly detail: RestPluginDetail;
|
|
179
200
|
readonly projects: readonly RestProject[];
|
|
180
201
|
readonly language: string;
|
|
202
|
+
readonly busy: boolean;
|
|
203
|
+
/** Called with what the plugin holds from now on, an optional capability turned on or off. */
|
|
204
|
+
readonly onConsent: (consent: readonly string[]) => void;
|
|
181
205
|
}) {
|
|
182
206
|
const words = useWords();
|
|
183
207
|
const settings = words.plugins.settings;
|
|
184
208
|
const { detail, language } = props;
|
|
185
209
|
const enabled = detail.state === "enabled";
|
|
186
210
|
const rows: { readonly key: string; readonly name: string; readonly value: ReactNode }[] = [];
|
|
211
|
+
if (detail.console !== null && detail.console !== undefined) {
|
|
212
|
+
rows.push({ key: "console", name: settings.console, value: <code>{detail.console}</code> });
|
|
213
|
+
}
|
|
187
214
|
if (detail.board !== null) {
|
|
188
215
|
rows.push({
|
|
189
216
|
key: "board",
|
|
@@ -197,6 +224,13 @@ function Overview(props: {
|
|
|
197
224
|
} else if (enabled && detail.boardNeeds !== "none") {
|
|
198
225
|
rows.push({ key: "board", name: settings.board, value: settings.nothing });
|
|
199
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
|
+
}
|
|
200
234
|
if (enabled && detail.projects !== null) {
|
|
201
235
|
const names = detail.projects;
|
|
202
236
|
rows.push({
|
|
@@ -214,24 +248,66 @@ function Overview(props: {
|
|
|
214
248
|
}
|
|
215
249
|
if (enabled && detail.consent !== null) {
|
|
216
250
|
const consent = detail.consent;
|
|
251
|
+
const optional = detail.optionalCapabilities ?? [];
|
|
252
|
+
const required = consent.filter((capability) => !optional.includes(capability));
|
|
217
253
|
rows.push({
|
|
218
254
|
key: "capabilities",
|
|
219
255
|
name: settings.capabilities,
|
|
220
256
|
value:
|
|
221
|
-
consent.length === 0 ? (
|
|
257
|
+
consent.length === 0 && optional.length === 0 ? (
|
|
222
258
|
settings.nothing
|
|
223
259
|
) : (
|
|
224
|
-
|
|
225
|
-
{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
260
|
+
<>
|
|
261
|
+
{required.length > 0 && (
|
|
262
|
+
<ul className="sc-plugin-held">
|
|
263
|
+
{required.map((capability) => {
|
|
264
|
+
const text = capabilityText(words, capability);
|
|
265
|
+
return (
|
|
266
|
+
<li key={capability}>
|
|
267
|
+
<span className="sc-plugin-tick-name">{text.name}</span>
|
|
268
|
+
{text.opens !== "" && (
|
|
269
|
+
<span className="sc-plugin-tick-under"> {text.opens}</span>
|
|
270
|
+
)}
|
|
271
|
+
</li>
|
|
272
|
+
);
|
|
273
|
+
})}
|
|
274
|
+
</ul>
|
|
275
|
+
)}
|
|
276
|
+
{optional.length > 0 && (
|
|
277
|
+
<div className="sc-plugin-optional">
|
|
278
|
+
<span className="sc-field-label">{settings.optional}</span>
|
|
279
|
+
<ul className="sc-plugin-held">
|
|
280
|
+
{optional.map((capability) => {
|
|
281
|
+
const text = capabilityText(words, capability);
|
|
282
|
+
return (
|
|
283
|
+
<li key={capability}>
|
|
284
|
+
<label className="sc-check">
|
|
285
|
+
<input
|
|
286
|
+
type="checkbox"
|
|
287
|
+
checked={consent.includes(capability)}
|
|
288
|
+
disabled={props.busy}
|
|
289
|
+
onChange={(event) =>
|
|
290
|
+
props.onConsent(
|
|
291
|
+
event.target.checked
|
|
292
|
+
? [...consent, capability]
|
|
293
|
+
: consent.filter((held) => held !== capability),
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
/>
|
|
297
|
+
<span>
|
|
298
|
+
<span className="sc-plugin-tick-name">{text.name}</span>
|
|
299
|
+
{text.opens !== "" && (
|
|
300
|
+
<span className="sc-plugin-tick-under"> {text.opens}</span>
|
|
301
|
+
)}
|
|
302
|
+
</span>
|
|
303
|
+
</label>
|
|
304
|
+
</li>
|
|
305
|
+
);
|
|
306
|
+
})}
|
|
307
|
+
</ul>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
</>
|
|
235
311
|
),
|
|
236
312
|
});
|
|
237
313
|
}
|
|
@@ -272,9 +348,209 @@ function Overview(props: {
|
|
|
272
348
|
);
|
|
273
349
|
}
|
|
274
350
|
|
|
351
|
+
/** How many choices a list of an enum shows as pills before it becomes a column of boxes. */
|
|
352
|
+
const PILLS_AT_MOST = 8;
|
|
353
|
+
|
|
354
|
+
/** The control of one item of a list, as a text field: a number, a day, a time or words. */
|
|
355
|
+
function itemInputType(of: string | null): "number" | "date" | "time" | "text" {
|
|
356
|
+
switch (of) {
|
|
357
|
+
case "number":
|
|
358
|
+
return "number";
|
|
359
|
+
case "day":
|
|
360
|
+
return "date";
|
|
361
|
+
case "time":
|
|
362
|
+
return "time";
|
|
363
|
+
default:
|
|
364
|
+
return "text";
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** The items of a list setting as the form holds them. */
|
|
369
|
+
function itemsOf(value: unknown): readonly (string | number)[] {
|
|
370
|
+
return Array.isArray(value)
|
|
371
|
+
? value.filter(
|
|
372
|
+
(item): item is string | number => typeof item === "string" || typeof item === "number",
|
|
373
|
+
)
|
|
374
|
+
: [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* A list of an enum: a pill per choice while they are few, lit while chosen, else a column of
|
|
379
|
+
* boxes; either way the value is the chosen values, in the options' order.
|
|
380
|
+
*/
|
|
381
|
+
function EnumListControl(props: {
|
|
382
|
+
readonly setting: RestPluginSetting;
|
|
383
|
+
readonly value: unknown;
|
|
384
|
+
readonly onChange: (value: unknown) => void;
|
|
385
|
+
readonly language: string;
|
|
386
|
+
}) {
|
|
387
|
+
const options = props.setting.options ?? [];
|
|
388
|
+
const chosen = new Set(itemsOf(props.value).map(String));
|
|
389
|
+
const toggle = (option: string, on: boolean) => {
|
|
390
|
+
const next = options
|
|
391
|
+
.map((candidate) => candidate.value)
|
|
392
|
+
.filter((candidate) => (candidate === option ? on : chosen.has(candidate)));
|
|
393
|
+
props.onChange(next.length === 0 ? null : next);
|
|
394
|
+
};
|
|
395
|
+
const pills = options.length <= PILLS_AT_MOST;
|
|
396
|
+
const name = pluginWord(props.setting.name, props.language);
|
|
397
|
+
const choices = options.map((option) => (
|
|
398
|
+
<label key={option.value} className={pills ? "sc-toggle-chip" : "sc-check"}>
|
|
399
|
+
<input
|
|
400
|
+
type="checkbox"
|
|
401
|
+
checked={chosen.has(option.value)}
|
|
402
|
+
onChange={(event) => toggle(option.value, event.target.checked)}
|
|
403
|
+
/>
|
|
404
|
+
{pluginWord(option.name, props.language)}
|
|
405
|
+
</label>
|
|
406
|
+
));
|
|
407
|
+
return pills ? (
|
|
408
|
+
<fieldset className="sc-toggle-chips" aria-label={name}>
|
|
409
|
+
{choices}
|
|
410
|
+
</fieldset>
|
|
411
|
+
) : (
|
|
412
|
+
<ul className="sc-check-list" aria-label={name}>
|
|
413
|
+
{choices.map((choice) => (
|
|
414
|
+
<li key={choice.key}>{choice}</li>
|
|
415
|
+
))}
|
|
416
|
+
</ul>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
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
|
+
|
|
423
|
+
/**
|
|
424
|
+
* A list of people or of projects: each chosen one a chip with the way to take it out, and one
|
|
425
|
+
* choice to add another among those not chosen yet; the value is their ids.
|
|
426
|
+
*/
|
|
427
|
+
function RefListControl(props: {
|
|
428
|
+
readonly setting: RestPluginSetting;
|
|
429
|
+
readonly value: unknown;
|
|
430
|
+
readonly onChange: (value: unknown) => void;
|
|
431
|
+
readonly id: string;
|
|
432
|
+
readonly language: string;
|
|
433
|
+
readonly people: readonly RestPerson[];
|
|
434
|
+
readonly projects: readonly RestProject[];
|
|
435
|
+
}) {
|
|
436
|
+
const words = useWords();
|
|
437
|
+
const chosen = itemsOf(props.value).map(String);
|
|
438
|
+
const candidates: readonly Candidate[] =
|
|
439
|
+
props.setting.of === "person"
|
|
440
|
+
? props.people
|
|
441
|
+
.filter((person) => person.status === "active" || chosen.includes(person.id))
|
|
442
|
+
.map((person) => ({ id: person.id, label: personChoice(person) }))
|
|
443
|
+
: props.projects.map((project) => ({
|
|
444
|
+
id: project.id,
|
|
445
|
+
label: `${project.name} (${project.key})`,
|
|
446
|
+
}));
|
|
447
|
+
const labelOf = (id: string) => candidates.find((candidate) => candidate.id === id)?.label ?? id;
|
|
448
|
+
const set = (next: readonly string[]) => props.onChange(next.length === 0 ? null : next);
|
|
449
|
+
const full = props.setting.max !== null && chosen.length >= props.setting.max;
|
|
450
|
+
return (
|
|
451
|
+
<div className="sc-chips">
|
|
452
|
+
{chosen.map((id) => (
|
|
453
|
+
<Chip
|
|
454
|
+
key={id}
|
|
455
|
+
onRemove={() => set(chosen.filter((kept) => kept !== id))}
|
|
456
|
+
removeLabel={`${words.common.remove}: ${labelOf(id)}`}
|
|
457
|
+
>
|
|
458
|
+
{labelOf(id)}
|
|
459
|
+
</Chip>
|
|
460
|
+
))}
|
|
461
|
+
{!full && (
|
|
462
|
+
<Picker<Candidate>
|
|
463
|
+
id={props.id}
|
|
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]);
|
|
477
|
+
}
|
|
478
|
+
}}
|
|
479
|
+
/>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* A list of texts, numbers, days or times: each item a chip with the way to take it out, and a
|
|
487
|
+
* field that adds one on Enter or with the button; `min` and `max` bound how many.
|
|
488
|
+
*/
|
|
489
|
+
function ItemListControl(props: {
|
|
490
|
+
readonly setting: RestPluginSetting;
|
|
491
|
+
readonly value: unknown;
|
|
492
|
+
readonly onChange: (value: unknown) => void;
|
|
493
|
+
readonly id: string;
|
|
494
|
+
}) {
|
|
495
|
+
const words = useWords();
|
|
496
|
+
const [draft, setDraft] = useState("");
|
|
497
|
+
const items = itemsOf(props.value);
|
|
498
|
+
const set = (next: readonly (string | number)[]) =>
|
|
499
|
+
props.onChange(next.length === 0 ? null : next);
|
|
500
|
+
const full = props.setting.max !== null && items.length >= props.setting.max;
|
|
501
|
+
const add = () => {
|
|
502
|
+
const word = draft.trim();
|
|
503
|
+
if (word.length === 0 || full) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const item = props.setting.of === "number" ? Number(word) : word;
|
|
507
|
+
if (typeof item === "number" && !Number.isFinite(item)) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
set([...items, item]);
|
|
511
|
+
setDraft("");
|
|
512
|
+
};
|
|
513
|
+
return (
|
|
514
|
+
<div className="sc-chips">
|
|
515
|
+
{items.map((item, index) => (
|
|
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
|
+
>
|
|
522
|
+
{String(item)}
|
|
523
|
+
</Chip>
|
|
524
|
+
))}
|
|
525
|
+
{!full && (
|
|
526
|
+
<div className="sc-chip-input">
|
|
527
|
+
<input
|
|
528
|
+
id={props.id}
|
|
529
|
+
className="sc-input"
|
|
530
|
+
type={itemInputType(props.setting.of)}
|
|
531
|
+
value={draft}
|
|
532
|
+
placeholder={words.plugins.settings.itemPlaceholder}
|
|
533
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
534
|
+
onKeyDown={(event) => {
|
|
535
|
+
if (event.key === "Enter") {
|
|
536
|
+
event.preventDefault();
|
|
537
|
+
add();
|
|
538
|
+
}
|
|
539
|
+
}}
|
|
540
|
+
/>
|
|
541
|
+
<Button type="button" size="sm" disabled={draft.trim().length === 0} onClick={add}>
|
|
542
|
+
{words.plugins.settings.addItem}
|
|
543
|
+
</Button>
|
|
544
|
+
</div>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
275
550
|
/**
|
|
276
551
|
* A setting's control, by its type: a person or a project is chosen among the workspace's, by
|
|
277
|
-
* name, and held as its id; a
|
|
552
|
+
* name, and held as its id; a time of day is read in the workspace's time zone; a list has a
|
|
553
|
+
* control per kind of item; a secret has a row of its own below.
|
|
278
554
|
*/
|
|
279
555
|
function SettingControl(props: {
|
|
280
556
|
readonly setting: RestPluginSetting;
|
|
@@ -285,44 +561,46 @@ function SettingControl(props: {
|
|
|
285
561
|
readonly people: readonly RestPerson[];
|
|
286
562
|
readonly projects: readonly RestProject[];
|
|
287
563
|
}) {
|
|
564
|
+
const words = useWords();
|
|
288
565
|
const { setting, value, onChange, id } = props;
|
|
289
566
|
const chosen = typeof value === "string" ? value : "";
|
|
290
|
-
const choose = (event: { readonly target: { readonly value: string } }) =>
|
|
291
|
-
onChange(event.target.value === "" ? null : event.target.value);
|
|
292
567
|
switch (setting.type) {
|
|
293
568
|
case "person": {
|
|
294
569
|
// The people in; the one chosen stays listed when they have left, so that the value shows.
|
|
295
570
|
const people = props.people.filter(
|
|
296
571
|
(person) => person.status === "active" || person.id === chosen,
|
|
297
572
|
);
|
|
573
|
+
const current = people.find((person) => person.id === chosen);
|
|
298
574
|
return (
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
{
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
{
|
|
307
|
-
|
|
308
|
-
)}
|
|
309
|
-
|
|
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
|
+
/>
|
|
310
586
|
);
|
|
311
587
|
}
|
|
312
|
-
case "project":
|
|
588
|
+
case "project": {
|
|
589
|
+
const current = props.projects.find((project) => project.id === chosen);
|
|
313
590
|
return (
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
{
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
))}
|
|
321
|
-
{
|
|
322
|
-
|
|
323
|
-
)}
|
|
324
|
-
|
|
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
|
+
/>
|
|
325
602
|
);
|
|
603
|
+
}
|
|
326
604
|
case "boolean":
|
|
327
605
|
return (
|
|
328
606
|
<input
|
|
@@ -372,25 +650,44 @@ function SettingControl(props: {
|
|
|
372
650
|
onChange={(event) => onChange(event.target.value === "" ? null : event.target.value)}
|
|
373
651
|
/>
|
|
374
652
|
);
|
|
375
|
-
case "
|
|
653
|
+
case "time":
|
|
376
654
|
return (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
.map((item) => item.trim())
|
|
388
|
-
.filter((item) => item.length > 0)
|
|
389
|
-
.map((item) => (setting.of === "number" ? Number(item) : item)),
|
|
390
|
-
)
|
|
391
|
-
}
|
|
392
|
-
/>
|
|
655
|
+
<>
|
|
656
|
+
<input
|
|
657
|
+
id={id}
|
|
658
|
+
className="sc-input"
|
|
659
|
+
type="time"
|
|
660
|
+
value={typeof value === "string" ? value : ""}
|
|
661
|
+
onChange={(event) => onChange(event.target.value === "" ? null : event.target.value)}
|
|
662
|
+
/>
|
|
663
|
+
<p className="sc-settings-hint">{words.plugins.settings.timeZoneHint}</p>
|
|
664
|
+
</>
|
|
393
665
|
);
|
|
666
|
+
case "list":
|
|
667
|
+
if (setting.of === "enum") {
|
|
668
|
+
return (
|
|
669
|
+
<EnumListControl
|
|
670
|
+
setting={setting}
|
|
671
|
+
value={value}
|
|
672
|
+
onChange={onChange}
|
|
673
|
+
language={props.language}
|
|
674
|
+
/>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
if (setting.of === "person" || setting.of === "project") {
|
|
678
|
+
return (
|
|
679
|
+
<RefListControl
|
|
680
|
+
language={props.language}
|
|
681
|
+
setting={setting}
|
|
682
|
+
value={value}
|
|
683
|
+
onChange={onChange}
|
|
684
|
+
id={id}
|
|
685
|
+
people={props.people}
|
|
686
|
+
projects={props.projects}
|
|
687
|
+
/>
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
return <ItemListControl setting={setting} value={value} onChange={onChange} id={id} />;
|
|
394
691
|
default:
|
|
395
692
|
return (
|
|
396
693
|
<input
|
|
@@ -427,36 +724,57 @@ function SettingsForm(props: {
|
|
|
427
724
|
event.preventDefault();
|
|
428
725
|
props.onSave(values);
|
|
429
726
|
};
|
|
727
|
+
/** A row per setting: the name and the hint on the left, the control on the right. */
|
|
728
|
+
const rows = (shown: readonly RestPluginSetting[]) =>
|
|
729
|
+
shown.map((setting) => (
|
|
730
|
+
<div key={setting.key} className="sc-settings-row">
|
|
731
|
+
<div className="sc-settings-words">
|
|
732
|
+
<label className="sc-settings-label" htmlFor={`${prefix}-${setting.key}`}>
|
|
733
|
+
{pluginWord(setting.name, props.language)}
|
|
734
|
+
{setting.required && " *"}
|
|
735
|
+
</label>
|
|
736
|
+
{setting.description !== null && (
|
|
737
|
+
<p className="sc-settings-hint">{pluginWord(setting.description, props.language)}</p>
|
|
738
|
+
)}
|
|
739
|
+
</div>
|
|
740
|
+
<div className="sc-settings-control">
|
|
741
|
+
<SettingControl
|
|
742
|
+
setting={setting}
|
|
743
|
+
value={values[setting.key]}
|
|
744
|
+
id={`${prefix}-${setting.key}`}
|
|
745
|
+
language={props.language}
|
|
746
|
+
people={props.people}
|
|
747
|
+
projects={props.projects}
|
|
748
|
+
onChange={(value) => setValues((current) => ({ ...current, [setting.key]: value }))}
|
|
749
|
+
/>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
));
|
|
430
753
|
return (
|
|
431
754
|
<form
|
|
432
755
|
className="sc-form sc-settings-form"
|
|
433
756
|
onSubmit={submit}
|
|
434
757
|
aria-label={words.plugins.settings.settingsForm}
|
|
435
758
|
>
|
|
436
|
-
{settings.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
onChange={(value) => setValues((current) => ({ ...current, [setting.key]: value }))}
|
|
456
|
-
/>
|
|
457
|
-
</div>
|
|
458
|
-
</div>
|
|
459
|
-
))}
|
|
759
|
+
{rows(settings.filter((setting) => setting.group === null || setting.group === undefined))}
|
|
760
|
+
{(props.detail.settingGroups ?? []).map((group) => {
|
|
761
|
+
const grouped = settings.filter((setting) => setting.group === group.key);
|
|
762
|
+
if (grouped.length === 0) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
const description = pluginWord(group.description, props.language);
|
|
766
|
+
return (
|
|
767
|
+
<section
|
|
768
|
+
key={group.key}
|
|
769
|
+
className="sc-settings-group"
|
|
770
|
+
aria-label={pluginWord(group.name, props.language)}
|
|
771
|
+
>
|
|
772
|
+
<h3 className="sc-settings-group-title">{pluginWord(group.name, props.language)}</h3>
|
|
773
|
+
{description !== "" && <p className="sc-settings-hint">{description}</p>}
|
|
774
|
+
{rows(grouped)}
|
|
775
|
+
</section>
|
|
776
|
+
);
|
|
777
|
+
})}
|
|
460
778
|
<div className="sc-form-actions">
|
|
461
779
|
<Button type="submit" variant="primary" disabled={props.busy}>
|
|
462
780
|
{words.common.save}
|
|
@@ -466,6 +784,38 @@ function SettingsForm(props: {
|
|
|
466
784
|
);
|
|
467
785
|
}
|
|
468
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
|
+
|
|
469
819
|
/** One secret: whether and when it is set, a field to set it anew with its buttons on the line. */
|
|
470
820
|
function SecretRow(props: {
|
|
471
821
|
readonly setting: RestPluginSetting;
|
|
@@ -488,9 +838,12 @@ function SecretRow(props: {
|
|
|
488
838
|
</label>
|
|
489
839
|
<p className="sc-settings-hint">
|
|
490
840
|
{setting.set && setting.setAt !== null
|
|
491
|
-
? settings
|
|
841
|
+
? secretStanding(settings, setting, props.language)
|
|
492
842
|
: settings.secretUnset}
|
|
493
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
|
+
)}
|
|
494
847
|
{setting.description !== null && (
|
|
495
848
|
<p className="sc-settings-hint">{pluginWord(setting.description, props.language)}</p>
|
|
496
849
|
)}
|
|
@@ -527,25 +880,35 @@ function SecretRow(props: {
|
|
|
527
880
|
}
|
|
528
881
|
|
|
529
882
|
/**
|
|
530
|
-
* Enabling: the capabilities
|
|
531
|
-
*
|
|
883
|
+
* Enabling: the capabilities the plugin requires, on and not the administrator's to turn off, and
|
|
884
|
+
* those it can go without, off until chosen, those consented to before on already (ADR-0060); the
|
|
885
|
+
* board project, the one it had chosen already; the projects, as they were.
|
|
532
886
|
*/
|
|
533
887
|
function EnableForm(props: {
|
|
534
888
|
readonly detail: RestPluginDetail;
|
|
535
889
|
readonly projects: readonly RestProject[];
|
|
536
890
|
readonly language: string;
|
|
537
891
|
readonly busy: boolean;
|
|
538
|
-
readonly onEnable: (
|
|
892
|
+
readonly onEnable: (
|
|
893
|
+
input: RestPluginEnableInput,
|
|
894
|
+
grants: readonly { readonly plugin: string; readonly role: string }[],
|
|
895
|
+
) => void;
|
|
539
896
|
}) {
|
|
540
897
|
const words = useWords();
|
|
541
898
|
const settings = words.plugins.settings;
|
|
542
899
|
const { detail, language } = props;
|
|
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());
|
|
543
904
|
const [consented, setConsented] = useState<ReadonlySet<string>>(
|
|
544
|
-
() =>
|
|
545
|
-
new Set(
|
|
546
|
-
(detail.consent ?? []).filter((capability) => detail.capabilities.includes(capability)),
|
|
547
|
-
),
|
|
905
|
+
() => new Set((detail.consent ?? []).filter((capability) => optional.includes(capability))),
|
|
548
906
|
);
|
|
907
|
+
/** A capability a new version asks and nobody has consented to yet is said to be new. */
|
|
908
|
+
const tagOf = (capability: string): string | undefined =>
|
|
909
|
+
detail.state === "needs_consent" && !(detail.consent ?? []).includes(capability)
|
|
910
|
+
? settings.newCapability
|
|
911
|
+
: undefined;
|
|
549
912
|
const needsBoard = detail.boardNeeds !== "none";
|
|
550
913
|
const [boardMode, setBoardMode] = useState<"none" | "create" | "choose">(
|
|
551
914
|
detail.boardNeeds === "none"
|
|
@@ -566,7 +929,6 @@ function EnableForm(props: {
|
|
|
566
929
|
() => new Set(Array.isArray(before) ? before : []),
|
|
567
930
|
);
|
|
568
931
|
const ids = { key: useId(), name: useId(), description: useId(), choose: useId() };
|
|
569
|
-
const everyCapability = detail.capabilities.every((capability) => consented.has(capability));
|
|
570
932
|
const boardReady =
|
|
571
933
|
boardMode === "none"
|
|
572
934
|
? detail.boardNeeds !== "required"
|
|
@@ -575,36 +937,72 @@ function EnableForm(props: {
|
|
|
575
937
|
: chosen.length > 0;
|
|
576
938
|
const submit = (event: FormEvent) => {
|
|
577
939
|
event.preventDefault();
|
|
578
|
-
props.onEnable(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
+
);
|
|
588
955
|
};
|
|
589
956
|
return (
|
|
590
957
|
<form className="sc-form sc-plugin-enable" onSubmit={submit} aria-label={settings.enable}>
|
|
591
|
-
{detail.capabilities.length > 0 && (
|
|
958
|
+
{detail.capabilities.length + optional.length > 0 && (
|
|
592
959
|
<div className="sc-plugin-group">
|
|
593
960
|
<h3 className="sc-plugin-group-title">{settings.capabilities}</h3>
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
<
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
961
|
+
{detail.capabilities.length > 0 && (
|
|
962
|
+
<div className="sc-plugin-capabilities">
|
|
963
|
+
<p className="sc-plugin-capabilities-head">
|
|
964
|
+
<span className="sc-plugin-capabilities-name">{settings.required}</span>
|
|
965
|
+
<span className="sc-settings-hint">{settings.requiredHint}</span>
|
|
966
|
+
</p>
|
|
967
|
+
<ul className="sc-plugin-ticks">
|
|
968
|
+
{detail.capabilities.map((capability) => {
|
|
969
|
+
const text = capabilityText(words, capability);
|
|
970
|
+
return (
|
|
971
|
+
<TickRow
|
|
972
|
+
key={capability}
|
|
973
|
+
checked
|
|
974
|
+
locked
|
|
975
|
+
name={text.name}
|
|
976
|
+
under={text.opens}
|
|
977
|
+
tag={tagOf(capability)}
|
|
978
|
+
/>
|
|
979
|
+
);
|
|
980
|
+
})}
|
|
981
|
+
</ul>
|
|
982
|
+
</div>
|
|
983
|
+
)}
|
|
984
|
+
{optional.length > 0 && (
|
|
985
|
+
<div className="sc-plugin-capabilities">
|
|
986
|
+
<p className="sc-plugin-capabilities-head">
|
|
987
|
+
<span className="sc-plugin-capabilities-name">{settings.optional}</span>
|
|
988
|
+
<span className="sc-settings-hint">{settings.optionalHint}</span>
|
|
989
|
+
</p>
|
|
990
|
+
<ul className="sc-plugin-ticks">
|
|
991
|
+
{optional.map((capability) => {
|
|
992
|
+
const text = capabilityText(words, capability);
|
|
993
|
+
return (
|
|
994
|
+
<TickRow
|
|
995
|
+
key={capability}
|
|
996
|
+
checked={consented.has(capability)}
|
|
997
|
+
onChange={(on) => setConsented((current) => toggled(current, capability, on))}
|
|
998
|
+
name={text.name}
|
|
999
|
+
under={text.opens}
|
|
1000
|
+
/>
|
|
1001
|
+
);
|
|
1002
|
+
})}
|
|
1003
|
+
</ul>
|
|
1004
|
+
</div>
|
|
1005
|
+
)}
|
|
608
1006
|
</div>
|
|
609
1007
|
)}
|
|
610
1008
|
{needsBoard && (
|
|
@@ -685,18 +1083,16 @@ function EnableForm(props: {
|
|
|
685
1083
|
<label className="sc-field-label" htmlFor={ids.choose}>
|
|
686
1084
|
{settings.boardChoose}
|
|
687
1085
|
</label>
|
|
688
|
-
<
|
|
1086
|
+
<Picker
|
|
689
1087
|
id={ids.choose}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
{
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
))}
|
|
699
|
-
</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
|
+
/>
|
|
700
1096
|
</div>
|
|
701
1097
|
</div>
|
|
702
1098
|
)}
|
|
@@ -739,12 +1135,46 @@ function EnableForm(props: {
|
|
|
739
1135
|
)}
|
|
740
1136
|
</div>
|
|
741
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
|
+
)}
|
|
742
1176
|
<div className="sc-form-actions">
|
|
743
|
-
<Button
|
|
744
|
-
type="submit"
|
|
745
|
-
variant="primary"
|
|
746
|
-
disabled={props.busy || !everyCapability || !boardReady}
|
|
747
|
-
>
|
|
1177
|
+
<Button type="submit" variant="primary" disabled={props.busy || !boardReady}>
|
|
748
1178
|
{settings.enable}
|
|
749
1179
|
</Button>
|
|
750
1180
|
</div>
|
|
@@ -761,6 +1191,12 @@ function holderOf(grant: RestPluginGrant, words: Messages["plugins"]["settings"]
|
|
|
761
1191
|
if (grant.project !== null) {
|
|
762
1192
|
return words.grantMembersOf(grant.project.name);
|
|
763
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
|
+
}
|
|
764
1200
|
if (grant.person !== null) {
|
|
765
1201
|
return personName(grant.person);
|
|
766
1202
|
}
|
|
@@ -784,13 +1220,17 @@ function RoleRow(props: {
|
|
|
784
1220
|
readonly grants: readonly RestPluginGrant[];
|
|
785
1221
|
readonly projects: readonly RestProject[];
|
|
786
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[];
|
|
787
1227
|
readonly language: string;
|
|
788
1228
|
readonly busy: boolean;
|
|
789
1229
|
readonly onGrant: (input: RestPluginGrantInput) => void;
|
|
790
1230
|
readonly onRevoke: (id: string) => void;
|
|
791
1231
|
}) {
|
|
792
1232
|
const settings = useWords().plugins.settings;
|
|
793
|
-
const [whom, setWhom] = useState(
|
|
1233
|
+
const [whom, setWhom] = useState<GrantChoice | undefined>(undefined);
|
|
794
1234
|
const id = useId();
|
|
795
1235
|
const { role, grants } = props;
|
|
796
1236
|
const everyoneHas = grants.some((grant) => grant.everyone);
|
|
@@ -800,21 +1240,76 @@ function RoleRow(props: {
|
|
|
800
1240
|
const peopleWith = new Set(
|
|
801
1241
|
grants.flatMap((grant) => (grant.person === null ? [] : [grant.person.id])),
|
|
802
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));
|
|
803
1255
|
const projects = props.projects.filter((project) => !projectsWith.has(project.key));
|
|
804
1256
|
const people = props.people.filter(
|
|
805
1257
|
(person) => person.status === "active" && !peopleWith.has(person.id),
|
|
806
1258
|
);
|
|
807
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}`;
|
|
808
1296
|
const submit = (event: FormEvent) => {
|
|
809
1297
|
event.preventDefault();
|
|
810
|
-
if (whom ===
|
|
1298
|
+
if (whom === undefined) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (whom.kind === "everyone") {
|
|
811
1302
|
props.onGrant({ role: role.key, everyone: true });
|
|
812
|
-
} else if (whom.
|
|
813
|
-
props.onGrant({ role: role.key, project: whom.
|
|
814
|
-
} else if (whom.
|
|
815
|
-
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 });
|
|
816
1311
|
}
|
|
817
|
-
setWhom(
|
|
1312
|
+
setWhom(undefined);
|
|
818
1313
|
};
|
|
819
1314
|
return (
|
|
820
1315
|
<li className="sc-plugin-role">
|
|
@@ -823,30 +1318,28 @@ function RoleRow(props: {
|
|
|
823
1318
|
{pluginWord(role.name, props.language)}
|
|
824
1319
|
<code className="sc-plugin-role-key">{role.key}</code>
|
|
825
1320
|
{props.isDefault && <Badge tone="accent">{settings.roleDefault}</Badge>}
|
|
1321
|
+
{role.follows === "reports" && <Badge tone="accent">{settings.roleManagers}</Badge>}
|
|
826
1322
|
</p>
|
|
827
1323
|
{description !== "" && <p className="sc-settings-hint">{description}</p>}
|
|
828
1324
|
</div>
|
|
829
1325
|
<div>
|
|
830
|
-
<div className="sc-plugin-holders">
|
|
831
|
-
{props.isDefault &&
|
|
832
|
-
|
|
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>
|
|
833
1330
|
)}
|
|
834
1331
|
{grants.map((grant) => {
|
|
835
1332
|
const holder = holderOf(grant, settings);
|
|
836
1333
|
return (
|
|
837
|
-
<
|
|
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
|
+
>
|
|
838
1341
|
{holder}
|
|
839
|
-
|
|
840
|
-
size="sm"
|
|
841
|
-
variant="ghost"
|
|
842
|
-
disabled={props.busy}
|
|
843
|
-
onClick={() => props.onRevoke(grant.id)}
|
|
844
|
-
aria-label={`${settings.revoke}: ${holder}`}
|
|
845
|
-
title={settings.revoke}
|
|
846
|
-
>
|
|
847
|
-
×
|
|
848
|
-
</Button>
|
|
849
|
-
</span>
|
|
1342
|
+
</Chip>
|
|
850
1343
|
);
|
|
851
1344
|
})}
|
|
852
1345
|
</div>
|
|
@@ -854,36 +1347,18 @@ function RoleRow(props: {
|
|
|
854
1347
|
<label htmlFor={id} className="sc-visually-hidden">
|
|
855
1348
|
{settings.grantWhom}
|
|
856
1349
|
</label>
|
|
857
|
-
<
|
|
1350
|
+
<Picker
|
|
858
1351
|
id={id}
|
|
859
|
-
className="sc-input sc-plugin-grant-input"
|
|
860
1352
|
value={whom}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
{
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
{
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
<option key={project.key} value={`project:${project.key}`}>
|
|
871
|
-
{project.name}
|
|
872
|
-
</option>
|
|
873
|
-
))}
|
|
874
|
-
</optgroup>
|
|
875
|
-
)}
|
|
876
|
-
{people.length > 0 && (
|
|
877
|
-
<optgroup label={settings.grantPeople}>
|
|
878
|
-
{people.map((person) => (
|
|
879
|
-
<option key={person.id} value={`person:${person.id}`}>
|
|
880
|
-
{personChoice(person)}
|
|
881
|
-
</option>
|
|
882
|
-
))}
|
|
883
|
-
</optgroup>
|
|
884
|
-
)}
|
|
885
|
-
</select>
|
|
886
|
-
<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}>
|
|
887
1362
|
{settings.grant}
|
|
888
1363
|
</Button>
|
|
889
1364
|
</form>
|
|
@@ -892,19 +1367,33 @@ function RoleRow(props: {
|
|
|
892
1367
|
);
|
|
893
1368
|
}
|
|
894
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
|
+
|
|
895
1376
|
/** The roles the plugin declares, each with who holds it; a plugin without roles is every member's. */
|
|
896
1377
|
function RolesSection(props: {
|
|
897
1378
|
readonly detail: RestPluginDetail;
|
|
898
1379
|
readonly projects: readonly RestProject[];
|
|
899
1380
|
readonly people: readonly RestPerson[];
|
|
1381
|
+
readonly plugins: readonly RestPlugin[];
|
|
1382
|
+
readonly teams: readonly RestTeam[];
|
|
900
1383
|
readonly language: string;
|
|
901
1384
|
readonly busy: boolean;
|
|
902
1385
|
readonly onGrant: (input: RestPluginGrantInput) => void;
|
|
903
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;
|
|
904
1389
|
}) {
|
|
905
1390
|
const settings = useWords().plugins.settings;
|
|
906
1391
|
const { detail } = props;
|
|
907
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 ?? [];
|
|
908
1397
|
return (
|
|
909
1398
|
<section className="sc-panel" aria-label={settings.roles}>
|
|
910
1399
|
<h2 className="sc-section-title">{settings.roles}</h2>
|
|
@@ -920,6 +1409,8 @@ function RolesSection(props: {
|
|
|
920
1409
|
grants={grants.filter((grant) => grant.role === role.key)}
|
|
921
1410
|
projects={props.projects}
|
|
922
1411
|
people={props.people}
|
|
1412
|
+
plugins={others}
|
|
1413
|
+
teams={props.teams}
|
|
923
1414
|
language={props.language}
|
|
924
1415
|
busy={props.busy}
|
|
925
1416
|
onGrant={props.onGrant}
|
|
@@ -928,6 +1419,39 @@ function RolesSection(props: {
|
|
|
928
1419
|
))}
|
|
929
1420
|
</ul>
|
|
930
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
|
+
)}
|
|
931
1455
|
</section>
|
|
932
1456
|
);
|
|
933
1457
|
}
|
|
@@ -995,7 +1519,21 @@ export function PluginSettingsView(props: PluginSettingsViewProps) {
|
|
|
995
1519
|
</ul>
|
|
996
1520
|
</Callout>
|
|
997
1521
|
)}
|
|
998
|
-
|
|
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
|
+
)}
|
|
1530
|
+
<Overview
|
|
1531
|
+
detail={detail}
|
|
1532
|
+
projects={props.projects}
|
|
1533
|
+
language={language}
|
|
1534
|
+
busy={busy}
|
|
1535
|
+
onConsent={(consent) => change(() => plugin.updateConsent(consent))}
|
|
1536
|
+
/>
|
|
999
1537
|
{canEnable && (
|
|
1000
1538
|
<section className="sc-panel sc-panel-form" aria-label={settings.enable}>
|
|
1001
1539
|
<h2 className="sc-section-title">{settings.enable}</h2>
|
|
@@ -1004,7 +1542,14 @@ export function PluginSettingsView(props: PluginSettingsViewProps) {
|
|
|
1004
1542
|
projects={props.projects}
|
|
1005
1543
|
language={language}
|
|
1006
1544
|
busy={busy}
|
|
1007
|
-
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
|
+
}
|
|
1008
1553
|
/>
|
|
1009
1554
|
</section>
|
|
1010
1555
|
)}
|
|
@@ -1038,6 +1583,11 @@ export function PluginSettingsView(props: PluginSettingsViewProps) {
|
|
|
1038
1583
|
{detail.grants !== null && (
|
|
1039
1584
|
<RolesSection
|
|
1040
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
|
+
}
|
|
1041
1591
|
projects={props.projects}
|
|
1042
1592
|
people={props.people}
|
|
1043
1593
|
language={language}
|