@skillcdn/console 0.4.3 → 0.4.4
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 +4 -4
- 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 +95 -20
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +4 -2
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +32 -9
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +17 -14
- 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 +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -0
- 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 +3 -3
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +6 -0
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +23 -5
- 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 +7 -1
- 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/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 +2 -0
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +15 -2
- package/dist/components/people.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +3 -12
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +53 -20
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +151 -26
- 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 +1 -13
- package/dist/components/projects.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.map +1 -1
- package/dist/components/ui.js +5 -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 +761 -191
- package/dist/console.d.ts +10 -3
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +33 -13
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +4 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +5 -0
- 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 +174 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +175 -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 +170 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- 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/bridge.d.ts +44 -3
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +26 -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 +14 -1
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +18 -1
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +1 -0
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +1 -1
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +5 -2
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +5 -2
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +83 -6
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +111 -7
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +76 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +23 -2
- 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.map +1 -1
- package/dist/plugin/testing.js +2 -1
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +10 -1
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +34 -0
- 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/rules.d.ts +2 -2
- package/dist/rules.js +3 -3
- package/dist/schemas.d.ts +497 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +96 -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 +3 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +4 -3
- package/docs/specs/plugins.md +46 -33
- package/docs/specs/rest.md +16 -15
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +8 -7
- package/skills/planning-the-board/references/setting-up.md +2 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +6 -0
- package/src/cli/cli.ts +124 -19
- package/src/cli/format.ts +42 -9
- package/src/cli/help.ts +17 -13
- 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 +8 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +3 -3
- package/src/components/decision-form.tsx +75 -19
- package/src/components/decision-list.tsx +45 -13
- package/src/components/event-feed.tsx +7 -1
- package/src/components/goals.tsx +4 -1
- package/src/components/home.tsx +615 -0
- package/src/components/pack-connect.tsx +5 -4
- package/src/components/people.tsx +136 -54
- package/src/components/plugin-frame.tsx +62 -63
- package/src/components/plugin-settings.tsx +428 -87
- package/src/components/projects.tsx +0 -55
- package/src/components/task-view.tsx +7 -0
- package/src/components/ui.tsx +6 -2
- package/src/connect-page.tsx +5 -6
- package/src/console.tsx +80 -30
- package/src/data.ts +9 -2
- package/src/docs-page.tsx +6 -5
- package/src/home-tiles.ts +438 -0
- package/src/i18n/en.ts +178 -0
- package/src/i18n/index.ts +17 -14
- package/src/i18n/ko.ts +174 -3
- package/src/index.ts +7 -5
- package/src/limits.ts +6 -0
- package/src/plugin/bridge.ts +77 -1
- package/src/plugin/capabilities.ts +17 -1
- package/src/plugin/context.ts +18 -1
- package/src/plugin/define.ts +4 -1
- package/src/plugin/limits.ts +5 -2
- package/src/plugin/manifest.ts +154 -10
- package/src/plugin/rest-schemas.ts +27 -1
- package/src/plugin/sandbox.ts +36 -8
- package/src/plugin/semver.ts +89 -0
- package/src/plugin/testing.ts +2 -1
- package/src/plugin/web.ts +49 -0
- package/src/plugin.ts +18 -0
- package/src/rules.ts +3 -3
- package/src/schemas.ts +118 -10
- package/src/styles/console.css +761 -191
- package/src/use-action.ts +16 -7
- package/src/vocabulary.ts +4 -0
- package/template/plugin/console-plugin.json +3 -1
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
Badge,
|
|
21
21
|
Button,
|
|
22
22
|
Callout,
|
|
23
|
+
cx,
|
|
23
24
|
EmptyState,
|
|
24
25
|
formatInstant,
|
|
25
26
|
LinkButton,
|
|
@@ -110,6 +111,9 @@ export function PluginSettingsList(props: PluginSettingsListProps) {
|
|
|
110
111
|
{pluginWord(plugin.name, props.language)}{" "}
|
|
111
112
|
<span className="sc-muted">
|
|
112
113
|
{plugin.key} {plugin.version}
|
|
114
|
+
{plugin.console !== null && plugin.console !== undefined && (
|
|
115
|
+
<> · {settings.consoleRange(plugin.console)}</>
|
|
116
|
+
)}
|
|
113
117
|
</span>
|
|
114
118
|
</a>
|
|
115
119
|
<span className="sc-task-row-meta">
|
|
@@ -150,20 +154,28 @@ function toggled(set: ReadonlySet<string>, value: string, on: boolean): Readonly
|
|
|
150
154
|
/** A row of a list to tick: the box, a name, a word under it. */
|
|
151
155
|
function TickRow(props: {
|
|
152
156
|
readonly checked: boolean;
|
|
153
|
-
readonly onChange
|
|
157
|
+
readonly onChange?: ((checked: boolean) => void) | undefined;
|
|
154
158
|
readonly name: string;
|
|
155
159
|
readonly under?: string | undefined;
|
|
160
|
+
/** A word beside the name: that this one is new, say. */
|
|
161
|
+
readonly tag?: string | undefined;
|
|
162
|
+
/** Whether it stays as it is: on, and not the person's to turn off. */
|
|
163
|
+
readonly locked?: boolean | undefined;
|
|
156
164
|
}) {
|
|
157
165
|
return (
|
|
158
166
|
<li>
|
|
159
|
-
<label className="sc-plugin-tick">
|
|
167
|
+
<label className={cx("sc-plugin-tick", props.locked === true && "sc-plugin-tick-locked")}>
|
|
160
168
|
<input
|
|
161
169
|
type="checkbox"
|
|
162
170
|
checked={props.checked}
|
|
163
|
-
|
|
171
|
+
disabled={props.locked === true}
|
|
172
|
+
onChange={(event) => props.onChange?.(event.target.checked)}
|
|
164
173
|
/>
|
|
165
174
|
<span className="sc-plugin-tick-words">
|
|
166
|
-
<span className="sc-plugin-tick-name">
|
|
175
|
+
<span className="sc-plugin-tick-name">
|
|
176
|
+
{props.name}
|
|
177
|
+
{props.tag !== undefined && <Badge tone="warning">{props.tag}</Badge>}
|
|
178
|
+
</span>
|
|
167
179
|
{props.under !== undefined && props.under !== "" && (
|
|
168
180
|
<span className="sc-plugin-tick-under">{props.under}</span>
|
|
169
181
|
)}
|
|
@@ -178,12 +190,18 @@ function Overview(props: {
|
|
|
178
190
|
readonly detail: RestPluginDetail;
|
|
179
191
|
readonly projects: readonly RestProject[];
|
|
180
192
|
readonly language: string;
|
|
193
|
+
readonly busy: boolean;
|
|
194
|
+
/** Called with what the plugin holds from now on, an optional capability turned on or off. */
|
|
195
|
+
readonly onConsent: (consent: readonly string[]) => void;
|
|
181
196
|
}) {
|
|
182
197
|
const words = useWords();
|
|
183
198
|
const settings = words.plugins.settings;
|
|
184
199
|
const { detail, language } = props;
|
|
185
200
|
const enabled = detail.state === "enabled";
|
|
186
201
|
const rows: { readonly key: string; readonly name: string; readonly value: ReactNode }[] = [];
|
|
202
|
+
if (detail.console !== null && detail.console !== undefined) {
|
|
203
|
+
rows.push({ key: "console", name: settings.console, value: <code>{detail.console}</code> });
|
|
204
|
+
}
|
|
187
205
|
if (detail.board !== null) {
|
|
188
206
|
rows.push({
|
|
189
207
|
key: "board",
|
|
@@ -214,24 +232,66 @@ function Overview(props: {
|
|
|
214
232
|
}
|
|
215
233
|
if (enabled && detail.consent !== null) {
|
|
216
234
|
const consent = detail.consent;
|
|
235
|
+
const optional = detail.optionalCapabilities ?? [];
|
|
236
|
+
const required = consent.filter((capability) => !optional.includes(capability));
|
|
217
237
|
rows.push({
|
|
218
238
|
key: "capabilities",
|
|
219
239
|
name: settings.capabilities,
|
|
220
240
|
value:
|
|
221
|
-
consent.length === 0 ? (
|
|
241
|
+
consent.length === 0 && optional.length === 0 ? (
|
|
222
242
|
settings.nothing
|
|
223
243
|
) : (
|
|
224
|
-
|
|
225
|
-
{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
244
|
+
<>
|
|
245
|
+
{required.length > 0 && (
|
|
246
|
+
<ul className="sc-plugin-held">
|
|
247
|
+
{required.map((capability) => {
|
|
248
|
+
const text = capabilityText(words, capability);
|
|
249
|
+
return (
|
|
250
|
+
<li key={capability}>
|
|
251
|
+
<span className="sc-plugin-tick-name">{text.name}</span>
|
|
252
|
+
{text.opens !== "" && (
|
|
253
|
+
<span className="sc-plugin-tick-under"> {text.opens}</span>
|
|
254
|
+
)}
|
|
255
|
+
</li>
|
|
256
|
+
);
|
|
257
|
+
})}
|
|
258
|
+
</ul>
|
|
259
|
+
)}
|
|
260
|
+
{optional.length > 0 && (
|
|
261
|
+
<div className="sc-plugin-optional">
|
|
262
|
+
<span className="sc-field-label">{settings.optional}</span>
|
|
263
|
+
<ul className="sc-plugin-held">
|
|
264
|
+
{optional.map((capability) => {
|
|
265
|
+
const text = capabilityText(words, capability);
|
|
266
|
+
return (
|
|
267
|
+
<li key={capability}>
|
|
268
|
+
<label className="sc-check">
|
|
269
|
+
<input
|
|
270
|
+
type="checkbox"
|
|
271
|
+
checked={consent.includes(capability)}
|
|
272
|
+
disabled={props.busy}
|
|
273
|
+
onChange={(event) =>
|
|
274
|
+
props.onConsent(
|
|
275
|
+
event.target.checked
|
|
276
|
+
? [...consent, capability]
|
|
277
|
+
: consent.filter((held) => held !== capability),
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
/>
|
|
281
|
+
<span>
|
|
282
|
+
<span className="sc-plugin-tick-name">{text.name}</span>
|
|
283
|
+
{text.opens !== "" && (
|
|
284
|
+
<span className="sc-plugin-tick-under"> {text.opens}</span>
|
|
285
|
+
)}
|
|
286
|
+
</span>
|
|
287
|
+
</label>
|
|
288
|
+
</li>
|
|
289
|
+
);
|
|
290
|
+
})}
|
|
291
|
+
</ul>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</>
|
|
235
295
|
),
|
|
236
296
|
});
|
|
237
297
|
}
|
|
@@ -272,9 +332,214 @@ function Overview(props: {
|
|
|
272
332
|
);
|
|
273
333
|
}
|
|
274
334
|
|
|
335
|
+
/** How many choices a list of an enum shows as pills before it becomes a column of boxes. */
|
|
336
|
+
const PILLS_AT_MOST = 8;
|
|
337
|
+
|
|
338
|
+
/** The control of one item of a list, as a text field: a number, a day, a time or words. */
|
|
339
|
+
function itemInputType(of: string | null): "number" | "date" | "time" | "text" {
|
|
340
|
+
switch (of) {
|
|
341
|
+
case "number":
|
|
342
|
+
return "number";
|
|
343
|
+
case "day":
|
|
344
|
+
return "date";
|
|
345
|
+
case "time":
|
|
346
|
+
return "time";
|
|
347
|
+
default:
|
|
348
|
+
return "text";
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The items of a list setting as the form holds them. */
|
|
353
|
+
function itemsOf(value: unknown): readonly (string | number)[] {
|
|
354
|
+
return Array.isArray(value)
|
|
355
|
+
? value.filter(
|
|
356
|
+
(item): item is string | number => typeof item === "string" || typeof item === "number",
|
|
357
|
+
)
|
|
358
|
+
: [];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* A list of an enum: a pill per choice while they are few, lit while chosen, else a column of
|
|
363
|
+
* boxes; either way the value is the chosen values, in the options' order.
|
|
364
|
+
*/
|
|
365
|
+
function EnumListControl(props: {
|
|
366
|
+
readonly setting: RestPluginSetting;
|
|
367
|
+
readonly value: unknown;
|
|
368
|
+
readonly onChange: (value: unknown) => void;
|
|
369
|
+
readonly language: string;
|
|
370
|
+
}) {
|
|
371
|
+
const options = props.setting.options ?? [];
|
|
372
|
+
const chosen = new Set(itemsOf(props.value).map(String));
|
|
373
|
+
const toggle = (option: string, on: boolean) => {
|
|
374
|
+
const next = options
|
|
375
|
+
.map((candidate) => candidate.value)
|
|
376
|
+
.filter((candidate) => (candidate === option ? on : chosen.has(candidate)));
|
|
377
|
+
props.onChange(next.length === 0 ? null : next);
|
|
378
|
+
};
|
|
379
|
+
const pills = options.length <= PILLS_AT_MOST;
|
|
380
|
+
const name = pluginWord(props.setting.name, props.language);
|
|
381
|
+
const choices = options.map((option) => (
|
|
382
|
+
<label key={option.value} className={pills ? "sc-toggle-chip" : "sc-check"}>
|
|
383
|
+
<input
|
|
384
|
+
type="checkbox"
|
|
385
|
+
checked={chosen.has(option.value)}
|
|
386
|
+
onChange={(event) => toggle(option.value, event.target.checked)}
|
|
387
|
+
/>
|
|
388
|
+
{pluginWord(option.name, props.language)}
|
|
389
|
+
</label>
|
|
390
|
+
));
|
|
391
|
+
return pills ? (
|
|
392
|
+
<fieldset className="sc-toggle-chips" aria-label={name}>
|
|
393
|
+
{choices}
|
|
394
|
+
</fieldset>
|
|
395
|
+
) : (
|
|
396
|
+
<ul className="sc-check-list" aria-label={name}>
|
|
397
|
+
{choices.map((choice) => (
|
|
398
|
+
<li key={choice.key}>{choice}</li>
|
|
399
|
+
))}
|
|
400
|
+
</ul>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* A list of people or of projects: each chosen one a chip with the way to take it out, and one
|
|
406
|
+
* choice to add another among those not chosen yet; the value is their ids.
|
|
407
|
+
*/
|
|
408
|
+
function RefListControl(props: {
|
|
409
|
+
readonly setting: RestPluginSetting;
|
|
410
|
+
readonly value: unknown;
|
|
411
|
+
readonly onChange: (value: unknown) => void;
|
|
412
|
+
readonly id: string;
|
|
413
|
+
readonly people: readonly RestPerson[];
|
|
414
|
+
readonly projects: readonly RestProject[];
|
|
415
|
+
}) {
|
|
416
|
+
const words = useWords();
|
|
417
|
+
const chosen = itemsOf(props.value).map(String);
|
|
418
|
+
const candidates: readonly { readonly id: string; readonly label: string }[] =
|
|
419
|
+
props.setting.of === "person"
|
|
420
|
+
? props.people
|
|
421
|
+
.filter((person) => person.status === "active" || chosen.includes(person.id))
|
|
422
|
+
.map((person) => ({ id: person.id, label: personChoice(person) }))
|
|
423
|
+
: props.projects.map((project) => ({
|
|
424
|
+
id: project.id,
|
|
425
|
+
label: `${project.name} (${project.key})`,
|
|
426
|
+
}));
|
|
427
|
+
const labelOf = (id: string) => candidates.find((candidate) => candidate.id === id)?.label ?? id;
|
|
428
|
+
const set = (next: readonly string[]) => props.onChange(next.length === 0 ? null : next);
|
|
429
|
+
const full = props.setting.max !== null && chosen.length >= props.setting.max;
|
|
430
|
+
return (
|
|
431
|
+
<div className="sc-chips">
|
|
432
|
+
{chosen.map((id) => (
|
|
433
|
+
<span key={id} className="sc-chip">
|
|
434
|
+
{labelOf(id)}
|
|
435
|
+
<button
|
|
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>
|
|
444
|
+
))}
|
|
445
|
+
{!full && (
|
|
446
|
+
<select
|
|
447
|
+
id={props.id}
|
|
448
|
+
className="sc-input sc-chip-add"
|
|
449
|
+
value=""
|
|
450
|
+
onChange={(event) => {
|
|
451
|
+
if (event.target.value !== "") {
|
|
452
|
+
set([...chosen, event.target.value]);
|
|
453
|
+
}
|
|
454
|
+
}}
|
|
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>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* A list of texts, numbers, days or times: each item a chip with the way to take it out, and a
|
|
472
|
+
* field that adds one on Enter or with the button; `min` and `max` bound how many.
|
|
473
|
+
*/
|
|
474
|
+
function ItemListControl(props: {
|
|
475
|
+
readonly setting: RestPluginSetting;
|
|
476
|
+
readonly value: unknown;
|
|
477
|
+
readonly onChange: (value: unknown) => void;
|
|
478
|
+
readonly id: string;
|
|
479
|
+
}) {
|
|
480
|
+
const words = useWords();
|
|
481
|
+
const [draft, setDraft] = useState("");
|
|
482
|
+
const items = itemsOf(props.value);
|
|
483
|
+
const set = (next: readonly (string | number)[]) =>
|
|
484
|
+
props.onChange(next.length === 0 ? null : next);
|
|
485
|
+
const full = props.setting.max !== null && items.length >= props.setting.max;
|
|
486
|
+
const add = () => {
|
|
487
|
+
const word = draft.trim();
|
|
488
|
+
if (word.length === 0 || full) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const item = props.setting.of === "number" ? Number(word) : word;
|
|
492
|
+
if (typeof item === "number" && !Number.isFinite(item)) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
set([...items, item]);
|
|
496
|
+
setDraft("");
|
|
497
|
+
};
|
|
498
|
+
return (
|
|
499
|
+
<div className="sc-chips">
|
|
500
|
+
{items.map((item, index) => (
|
|
501
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: an item may be typed twice
|
|
502
|
+
<span key={`${index}-${String(item)}`} className="sc-chip">
|
|
503
|
+
{String(item)}
|
|
504
|
+
<button
|
|
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>
|
|
513
|
+
))}
|
|
514
|
+
{!full && (
|
|
515
|
+
<div className="sc-chip-input">
|
|
516
|
+
<input
|
|
517
|
+
id={props.id}
|
|
518
|
+
className="sc-input"
|
|
519
|
+
type={itemInputType(props.setting.of)}
|
|
520
|
+
value={draft}
|
|
521
|
+
placeholder={words.plugins.settings.itemPlaceholder}
|
|
522
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
523
|
+
onKeyDown={(event) => {
|
|
524
|
+
if (event.key === "Enter") {
|
|
525
|
+
event.preventDefault();
|
|
526
|
+
add();
|
|
527
|
+
}
|
|
528
|
+
}}
|
|
529
|
+
/>
|
|
530
|
+
<Button type="button" size="sm" disabled={draft.trim().length === 0} onClick={add}>
|
|
531
|
+
{words.plugins.settings.addItem}
|
|
532
|
+
</Button>
|
|
533
|
+
</div>
|
|
534
|
+
)}
|
|
535
|
+
</div>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
275
539
|
/**
|
|
276
540
|
* 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
|
|
541
|
+
* name, and held as its id; a time of day is read in the workspace's time zone; a list has a
|
|
542
|
+
* control per kind of item; a secret has a row of its own below.
|
|
278
543
|
*/
|
|
279
544
|
function SettingControl(props: {
|
|
280
545
|
readonly setting: RestPluginSetting;
|
|
@@ -285,6 +550,7 @@ function SettingControl(props: {
|
|
|
285
550
|
readonly people: readonly RestPerson[];
|
|
286
551
|
readonly projects: readonly RestProject[];
|
|
287
552
|
}) {
|
|
553
|
+
const words = useWords();
|
|
288
554
|
const { setting, value, onChange, id } = props;
|
|
289
555
|
const chosen = typeof value === "string" ? value : "";
|
|
290
556
|
const choose = (event: { readonly target: { readonly value: string } }) =>
|
|
@@ -372,25 +638,43 @@ function SettingControl(props: {
|
|
|
372
638
|
onChange={(event) => onChange(event.target.value === "" ? null : event.target.value)}
|
|
373
639
|
/>
|
|
374
640
|
);
|
|
375
|
-
case "
|
|
641
|
+
case "time":
|
|
376
642
|
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
|
-
/>
|
|
643
|
+
<>
|
|
644
|
+
<input
|
|
645
|
+
id={id}
|
|
646
|
+
className="sc-input"
|
|
647
|
+
type="time"
|
|
648
|
+
value={typeof value === "string" ? value : ""}
|
|
649
|
+
onChange={(event) => onChange(event.target.value === "" ? null : event.target.value)}
|
|
650
|
+
/>
|
|
651
|
+
<p className="sc-settings-hint">{words.plugins.settings.timeZoneHint}</p>
|
|
652
|
+
</>
|
|
393
653
|
);
|
|
654
|
+
case "list":
|
|
655
|
+
if (setting.of === "enum") {
|
|
656
|
+
return (
|
|
657
|
+
<EnumListControl
|
|
658
|
+
setting={setting}
|
|
659
|
+
value={value}
|
|
660
|
+
onChange={onChange}
|
|
661
|
+
language={props.language}
|
|
662
|
+
/>
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
if (setting.of === "person" || setting.of === "project") {
|
|
666
|
+
return (
|
|
667
|
+
<RefListControl
|
|
668
|
+
setting={setting}
|
|
669
|
+
value={value}
|
|
670
|
+
onChange={onChange}
|
|
671
|
+
id={id}
|
|
672
|
+
people={props.people}
|
|
673
|
+
projects={props.projects}
|
|
674
|
+
/>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return <ItemListControl setting={setting} value={value} onChange={onChange} id={id} />;
|
|
394
678
|
default:
|
|
395
679
|
return (
|
|
396
680
|
<input
|
|
@@ -427,36 +711,57 @@ function SettingsForm(props: {
|
|
|
427
711
|
event.preventDefault();
|
|
428
712
|
props.onSave(values);
|
|
429
713
|
};
|
|
714
|
+
/** A row per setting: the name and the hint on the left, the control on the right. */
|
|
715
|
+
const rows = (shown: readonly RestPluginSetting[]) =>
|
|
716
|
+
shown.map((setting) => (
|
|
717
|
+
<div key={setting.key} className="sc-settings-row">
|
|
718
|
+
<div className="sc-settings-words">
|
|
719
|
+
<label className="sc-settings-label" htmlFor={`${prefix}-${setting.key}`}>
|
|
720
|
+
{pluginWord(setting.name, props.language)}
|
|
721
|
+
{setting.required && " *"}
|
|
722
|
+
</label>
|
|
723
|
+
{setting.description !== null && (
|
|
724
|
+
<p className="sc-settings-hint">{pluginWord(setting.description, props.language)}</p>
|
|
725
|
+
)}
|
|
726
|
+
</div>
|
|
727
|
+
<div className="sc-settings-control">
|
|
728
|
+
<SettingControl
|
|
729
|
+
setting={setting}
|
|
730
|
+
value={values[setting.key]}
|
|
731
|
+
id={`${prefix}-${setting.key}`}
|
|
732
|
+
language={props.language}
|
|
733
|
+
people={props.people}
|
|
734
|
+
projects={props.projects}
|
|
735
|
+
onChange={(value) => setValues((current) => ({ ...current, [setting.key]: value }))}
|
|
736
|
+
/>
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
));
|
|
430
740
|
return (
|
|
431
741
|
<form
|
|
432
742
|
className="sc-form sc-settings-form"
|
|
433
743
|
onSubmit={submit}
|
|
434
744
|
aria-label={words.plugins.settings.settingsForm}
|
|
435
745
|
>
|
|
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
|
-
))}
|
|
746
|
+
{rows(settings.filter((setting) => setting.group === null || setting.group === undefined))}
|
|
747
|
+
{(props.detail.settingGroups ?? []).map((group) => {
|
|
748
|
+
const grouped = settings.filter((setting) => setting.group === group.key);
|
|
749
|
+
if (grouped.length === 0) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
const description = pluginWord(group.description, props.language);
|
|
753
|
+
return (
|
|
754
|
+
<section
|
|
755
|
+
key={group.key}
|
|
756
|
+
className="sc-settings-group"
|
|
757
|
+
aria-label={pluginWord(group.name, props.language)}
|
|
758
|
+
>
|
|
759
|
+
<h3 className="sc-settings-group-title">{pluginWord(group.name, props.language)}</h3>
|
|
760
|
+
{description !== "" && <p className="sc-settings-hint">{description}</p>}
|
|
761
|
+
{rows(grouped)}
|
|
762
|
+
</section>
|
|
763
|
+
);
|
|
764
|
+
})}
|
|
460
765
|
<div className="sc-form-actions">
|
|
461
766
|
<Button type="submit" variant="primary" disabled={props.busy}>
|
|
462
767
|
{words.common.save}
|
|
@@ -527,8 +832,9 @@ function SecretRow(props: {
|
|
|
527
832
|
}
|
|
528
833
|
|
|
529
834
|
/**
|
|
530
|
-
* Enabling: the capabilities
|
|
531
|
-
*
|
|
835
|
+
* Enabling: the capabilities the plugin requires, on and not the administrator's to turn off, and
|
|
836
|
+
* those it can go without, off until chosen, those consented to before on already (ADR-0060); the
|
|
837
|
+
* board project, the one it had chosen already; the projects, as they were.
|
|
532
838
|
*/
|
|
533
839
|
function EnableForm(props: {
|
|
534
840
|
readonly detail: RestPluginDetail;
|
|
@@ -540,12 +846,15 @@ function EnableForm(props: {
|
|
|
540
846
|
const words = useWords();
|
|
541
847
|
const settings = words.plugins.settings;
|
|
542
848
|
const { detail, language } = props;
|
|
849
|
+
const optional = detail.optionalCapabilities ?? [];
|
|
543
850
|
const [consented, setConsented] = useState<ReadonlySet<string>>(
|
|
544
|
-
() =>
|
|
545
|
-
new Set(
|
|
546
|
-
(detail.consent ?? []).filter((capability) => detail.capabilities.includes(capability)),
|
|
547
|
-
),
|
|
851
|
+
() => new Set((detail.consent ?? []).filter((capability) => optional.includes(capability))),
|
|
548
852
|
);
|
|
853
|
+
/** A capability a new version asks and nobody has consented to yet is said to be new. */
|
|
854
|
+
const tagOf = (capability: string): string | undefined =>
|
|
855
|
+
detail.state === "needs_consent" && !(detail.consent ?? []).includes(capability)
|
|
856
|
+
? settings.newCapability
|
|
857
|
+
: undefined;
|
|
549
858
|
const needsBoard = detail.boardNeeds !== "none";
|
|
550
859
|
const [boardMode, setBoardMode] = useState<"none" | "create" | "choose">(
|
|
551
860
|
detail.boardNeeds === "none"
|
|
@@ -566,7 +875,6 @@ function EnableForm(props: {
|
|
|
566
875
|
() => new Set(Array.isArray(before) ? before : []),
|
|
567
876
|
);
|
|
568
877
|
const ids = { key: useId(), name: useId(), description: useId(), choose: useId() };
|
|
569
|
-
const everyCapability = detail.capabilities.every((capability) => consented.has(capability));
|
|
570
878
|
const boardReady =
|
|
571
879
|
boardMode === "none"
|
|
572
880
|
? detail.boardNeeds !== "required"
|
|
@@ -576,7 +884,7 @@ function EnableForm(props: {
|
|
|
576
884
|
const submit = (event: FormEvent) => {
|
|
577
885
|
event.preventDefault();
|
|
578
886
|
props.onEnable({
|
|
579
|
-
consent: [...consented],
|
|
887
|
+
consent: [...detail.capabilities, ...consented],
|
|
580
888
|
board:
|
|
581
889
|
boardMode === "none"
|
|
582
890
|
? null
|
|
@@ -588,23 +896,54 @@ function EnableForm(props: {
|
|
|
588
896
|
};
|
|
589
897
|
return (
|
|
590
898
|
<form className="sc-form sc-plugin-enable" onSubmit={submit} aria-label={settings.enable}>
|
|
591
|
-
{detail.capabilities.length > 0 && (
|
|
899
|
+
{detail.capabilities.length + optional.length > 0 && (
|
|
592
900
|
<div className="sc-plugin-group">
|
|
593
901
|
<h3 className="sc-plugin-group-title">{settings.capabilities}</h3>
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
<
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
902
|
+
{detail.capabilities.length > 0 && (
|
|
903
|
+
<div className="sc-plugin-capabilities">
|
|
904
|
+
<p className="sc-plugin-capabilities-head">
|
|
905
|
+
<span className="sc-plugin-capabilities-name">{settings.required}</span>
|
|
906
|
+
<span className="sc-settings-hint">{settings.requiredHint}</span>
|
|
907
|
+
</p>
|
|
908
|
+
<ul className="sc-plugin-ticks">
|
|
909
|
+
{detail.capabilities.map((capability) => {
|
|
910
|
+
const text = capabilityText(words, capability);
|
|
911
|
+
return (
|
|
912
|
+
<TickRow
|
|
913
|
+
key={capability}
|
|
914
|
+
checked
|
|
915
|
+
locked
|
|
916
|
+
name={text.name}
|
|
917
|
+
under={text.opens}
|
|
918
|
+
tag={tagOf(capability)}
|
|
919
|
+
/>
|
|
920
|
+
);
|
|
921
|
+
})}
|
|
922
|
+
</ul>
|
|
923
|
+
</div>
|
|
924
|
+
)}
|
|
925
|
+
{optional.length > 0 && (
|
|
926
|
+
<div className="sc-plugin-capabilities">
|
|
927
|
+
<p className="sc-plugin-capabilities-head">
|
|
928
|
+
<span className="sc-plugin-capabilities-name">{settings.optional}</span>
|
|
929
|
+
<span className="sc-settings-hint">{settings.optionalHint}</span>
|
|
930
|
+
</p>
|
|
931
|
+
<ul className="sc-plugin-ticks">
|
|
932
|
+
{optional.map((capability) => {
|
|
933
|
+
const text = capabilityText(words, capability);
|
|
934
|
+
return (
|
|
935
|
+
<TickRow
|
|
936
|
+
key={capability}
|
|
937
|
+
checked={consented.has(capability)}
|
|
938
|
+
onChange={(on) => setConsented((current) => toggled(current, capability, on))}
|
|
939
|
+
name={text.name}
|
|
940
|
+
under={text.opens}
|
|
941
|
+
/>
|
|
942
|
+
);
|
|
943
|
+
})}
|
|
944
|
+
</ul>
|
|
945
|
+
</div>
|
|
946
|
+
)}
|
|
608
947
|
</div>
|
|
609
948
|
)}
|
|
610
949
|
{needsBoard && (
|
|
@@ -740,11 +1079,7 @@ function EnableForm(props: {
|
|
|
740
1079
|
</div>
|
|
741
1080
|
<div className="sc-plugin-group">
|
|
742
1081
|
<div className="sc-form-actions">
|
|
743
|
-
<Button
|
|
744
|
-
type="submit"
|
|
745
|
-
variant="primary"
|
|
746
|
-
disabled={props.busy || !everyCapability || !boardReady}
|
|
747
|
-
>
|
|
1082
|
+
<Button type="submit" variant="primary" disabled={props.busy || !boardReady}>
|
|
748
1083
|
{settings.enable}
|
|
749
1084
|
</Button>
|
|
750
1085
|
</div>
|
|
@@ -995,7 +1330,13 @@ export function PluginSettingsView(props: PluginSettingsViewProps) {
|
|
|
995
1330
|
</ul>
|
|
996
1331
|
</Callout>
|
|
997
1332
|
)}
|
|
998
|
-
<Overview
|
|
1333
|
+
<Overview
|
|
1334
|
+
detail={detail}
|
|
1335
|
+
projects={props.projects}
|
|
1336
|
+
language={language}
|
|
1337
|
+
busy={busy}
|
|
1338
|
+
onConsent={(consent) => change(() => plugin.updateConsent(consent))}
|
|
1339
|
+
/>
|
|
999
1340
|
{canEnable && (
|
|
1000
1341
|
<section className="sc-panel sc-panel-form" aria-label={settings.enable}>
|
|
1001
1342
|
<h2 className="sc-section-title">{settings.enable}</h2>
|