@skillcdn/console 0.4.1 → 0.4.2
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 +19 -2
- package/SKILLCDN.md +7 -5
- package/dist/api.d.ts +4 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +543 -34
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +34 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +277 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +35 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +21 -0
- package/dist/cli/plugin-check.d.ts.map +1 -0
- package/dist/cli/plugin-check.js +374 -0
- package/dist/cli/plugin-check.js.map +1 -0
- package/dist/cli/plugin-cli.d.ts +20 -0
- package/dist/cli/plugin-cli.d.ts.map +1 -0
- package/dist/cli/plugin-cli.js +123 -0
- package/dist/cli/plugin-cli.js.map +1 -0
- package/dist/cli/plugin-init.d.ts +20 -0
- package/dist/cli/plugin-init.d.ts.map +1 -0
- package/dist/cli/plugin-init.js +74 -0
- package/dist/cli/plugin-init.js.map +1 -0
- package/dist/cli/plugin-main.d.ts +3 -0
- package/dist/cli/plugin-main.d.ts.map +1 -0
- package/dist/cli/plugin-main.js +28 -0
- package/dist/cli/plugin-main.js.map +1 -0
- package/dist/client.d.ts +97 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +63 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +2 -2
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +59 -15
- package/dist/components/board.js.map +1 -1
- package/dist/components/event-feed.d.ts +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +33 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +52 -0
- package/dist/components/plugin-frame.d.ts.map +1 -0
- package/dist/components/plugin-frame.js +314 -0
- package/dist/components/plugin-frame.js.map +1 -0
- package/dist/components/plugin-settings.d.ts +38 -0
- package/dist/components/plugin-settings.d.ts.map +1 -0
- package/dist/components/plugin-settings.js +348 -0
- package/dist/components/plugin-settings.js.map +1 -0
- package/dist/components/shell.d.ts +2 -0
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +26 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/workspace.d.ts +7 -1
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +1 -1
- package/dist/components/workspace.js.map +1 -1
- package/dist/console.css +327 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +136 -26
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +7 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +19 -2
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +149 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +160 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +144 -0
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin/args.d.ts +148 -0
- package/dist/plugin/args.d.ts.map +1 -0
- package/dist/plugin/args.js +369 -0
- package/dist/plugin/args.js.map +1 -0
- package/dist/plugin/bridge.d.ts +146 -0
- package/dist/plugin/bridge.d.ts.map +1 -0
- package/dist/plugin/bridge.js +125 -0
- package/dist/plugin/bridge.js.map +1 -0
- package/dist/plugin/capabilities.d.ts +20 -0
- package/dist/plugin/capabilities.d.ts.map +1 -0
- package/dist/plugin/capabilities.js +133 -0
- package/dist/plugin/capabilities.js.map +1 -0
- package/dist/plugin/context.d.ts +374 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +2 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +208 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +482 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/definition-error.d.ts +11 -0
- package/dist/plugin/definition-error.d.ts.map +1 -0
- package/dist/plugin/definition-error.js +17 -0
- package/dist/plugin/definition-error.js.map +1 -0
- package/dist/plugin/limits.d.ts +67 -0
- package/dist/plugin/limits.d.ts.map +1 -0
- package/dist/plugin/limits.js +70 -0
- package/dist/plugin/limits.js.map +1 -0
- package/dist/plugin/load-worker.d.ts +7 -0
- package/dist/plugin/load-worker.d.ts.map +1 -0
- package/dist/plugin/load-worker.js +41 -0
- package/dist/plugin/load-worker.js.map +1 -0
- package/dist/plugin/load.d.ts +18 -0
- package/dist/plugin/load.d.ts.map +1 -0
- package/dist/plugin/load.js +62 -0
- package/dist/plugin/load.js.map +1 -0
- package/dist/plugin/manifest.d.ts +237 -0
- package/dist/plugin/manifest.d.ts.map +1 -0
- package/dist/plugin/manifest.js +411 -0
- package/dist/plugin/manifest.js.map +1 -0
- package/dist/plugin/refusals.d.ts +49 -0
- package/dist/plugin/refusals.d.ts.map +1 -0
- package/dist/plugin/refusals.js +79 -0
- package/dist/plugin/refusals.js.map +1 -0
- package/dist/plugin/rest-schemas.d.ts +556 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -0
- package/dist/plugin/rest-schemas.js +262 -0
- package/dist/plugin/rest-schemas.js.map +1 -0
- package/dist/plugin/sandbox.d.ts +23 -0
- package/dist/plugin/sandbox.d.ts.map +1 -0
- package/dist/plugin/sandbox.js +99 -0
- package/dist/plugin/sandbox.js.map +1 -0
- package/dist/plugin/testing.d.ts +45 -0
- package/dist/plugin/testing.d.ts.map +1 -0
- package/dist/plugin/testing.js +420 -0
- package/dist/plugin/testing.js.map +1 -0
- package/dist/plugin/web-react.d.ts +11 -0
- package/dist/plugin/web-react.d.ts.map +1 -0
- package/dist/plugin/web-react.js +34 -0
- package/dist/plugin/web-react.js.map +1 -0
- package/dist/plugin/web.d.ts +95 -0
- package/dist/plugin/web.d.ts.map +1 -0
- package/dist/plugin/web.js +245 -0
- package/dist/plugin/web.js.map +1 -0
- package/dist/plugin-nav.d.ts +29 -0
- package/dist/plugin-nav.d.ts.map +1 -0
- package/dist/plugin-nav.js +92 -0
- package/dist/plugin-nav.js.map +1 -0
- package/dist/plugin-testing.d.ts +2 -0
- package/dist/plugin-testing.d.ts.map +1 -0
- package/dist/plugin-testing.js +4 -0
- package/dist/plugin-testing.js.map +1 -0
- package/dist/plugin-web-react.d.ts +3 -0
- package/dist/plugin-web-react.d.ts.map +1 -0
- package/dist/plugin-web-react.js +3 -0
- package/dist/plugin-web-react.js.map +1 -0
- package/dist/plugin-web.d.ts +4 -0
- package/dist/plugin-web.d.ts.map +1 -0
- package/dist/plugin-web.js +5 -0
- package/dist/plugin-web.js.map +1 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +15 -0
- package/dist/plugin.js.map +1 -0
- package/dist/router.d.ts +28 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +47 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +4 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +6 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +117 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +40 -1
- package/dist/schemas.js.map +1 -1
- package/dist/tokens.d.ts +2 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +101 -0
- package/dist/tokens.js.map +1 -0
- package/dist/vocabulary.d.ts +11 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +24 -0
- package/dist/vocabulary.js.map +1 -1
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +2 -0
- package/dist/web.js.map +1 -1
- package/docs/specs/cli.md +11 -4
- package/docs/specs/plugins.md +487 -0
- package/docs/specs/rest.md +3 -2
- package/package.json +20 -2
- package/skills/building-a-console/SKILL.md +25 -26
- package/skills/building-a-console/references/configuring.md +40 -0
- package/skills/building-a-console/references/the-default-console.md +1 -1
- package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
- package/skills/building-a-plugin/SKILL.md +137 -0
- package/skills/deploying-a-console/SKILL.md +24 -26
- package/skills/deploying-a-console/references/access.md +13 -9
- package/skills/deploying-a-console/references/resources.md +25 -10
- package/skills/deploying-a-console/references/running-it-again.md +15 -0
- package/skills/deploying-a-console/references/sizing.md +2 -0
- package/skills/planning-the-board/SKILL.md +96 -0
- package/skills/planning-the-board/references/a-task.md +36 -0
- package/skills/planning-the-board/references/setting-up.md +44 -0
- package/skills/working-the-board/SKILL.md +43 -34
- package/skills/working-the-board/references/the-pack.md +19 -0
- package/src/api.ts +8 -0
- package/src/cli/cli.ts +697 -33
- package/src/cli/format.ts +356 -9
- package/src/cli/help.ts +41 -2
- package/src/cli/plugin-check.ts +470 -0
- package/src/cli/plugin-cli.ts +148 -0
- package/src/cli/plugin-init.ts +90 -0
- package/src/cli/plugin-main.ts +30 -0
- package/src/client.ts +294 -0
- package/src/compat.ts +2 -2
- package/src/components/board.tsx +62 -14
- package/src/components/event-feed.tsx +42 -1
- package/src/components/plugin-frame.tsx +437 -0
- package/src/components/plugin-settings.tsx +1111 -0
- package/src/components/shell.tsx +82 -14
- package/src/components/workspace.tsx +18 -0
- package/src/console.tsx +286 -19
- package/src/data.ts +27 -2
- package/src/i18n/en.ts +168 -0
- package/src/i18n/ko.ts +148 -0
- package/src/index.ts +12 -0
- package/src/plugin/args.ts +526 -0
- package/src/plugin/bridge.ts +314 -0
- package/src/plugin/capabilities.ts +150 -0
- package/src/plugin/context.ts +416 -0
- package/src/plugin/define.ts +792 -0
- package/src/plugin/definition-error.ts +19 -0
- package/src/plugin/limits.ts +70 -0
- package/src/plugin/load-worker.ts +50 -0
- package/src/plugin/load.ts +82 -0
- package/src/plugin/manifest.ts +561 -0
- package/src/plugin/refusals.ts +94 -0
- package/src/plugin/rest-schemas.ts +354 -0
- package/src/plugin/sandbox.ts +123 -0
- package/src/plugin/testing.ts +612 -0
- package/src/plugin/web-react.ts +45 -0
- package/src/plugin/web.ts +403 -0
- package/src/plugin-nav.ts +132 -0
- package/src/plugin-testing.ts +10 -0
- package/src/plugin-web-react.ts +4 -0
- package/src/plugin-web.ts +35 -0
- package/src/plugin.ts +126 -0
- package/src/router.ts +73 -0
- package/src/routes.ts +7 -0
- package/src/schemas.ts +43 -0
- package/src/styles/console.css +327 -0
- package/src/tokens.ts +101 -0
- package/src/vocabulary.ts +30 -0
- package/src/web.ts +2 -0
- package/template/plugin/.gitignore +2 -0
- package/template/plugin/README.md +26 -0
- package/template/plugin/SKILLCDN.md +13 -0
- package/template/plugin/build.mjs +40 -0
- package/template/plugin/console-plugin.json +50 -0
- package/template/plugin/package.json +27 -0
- package/template/plugin/skills/__key__/SKILL.md +23 -0
- package/template/plugin/src/check.test.ts +34 -0
- package/template/plugin/src/server/index.test.ts +41 -0
- package/template/plugin/src/server/index.ts +72 -0
- package/template/plugin/src/web/index.css +51 -0
- package/template/plugin/src/web/index.ts +115 -0
- package/template/plugin/tsconfig.json +17 -0
- package/skills/working-the-board/references/setting-up.md +0 -3
- /package/skills/{working-the-board → planning-the-board}/references/project-rules.md +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// `console-plugin init` (docs/specs/plugins.md, "Authoring"): writes a plugin from the template
|
|
2
|
+
// the package ships, the words `__key__`, `__name__` and `__console__` in the template's files and
|
|
3
|
+
// their names replaced. `examples/plugin-hello` in the console's repository is what it writes for
|
|
4
|
+
// the key `hello`, and a test holds the two equal.
|
|
5
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join, relative, sep } from "node:path";
|
|
7
|
+
|
|
8
|
+
export interface InitOptions {
|
|
9
|
+
readonly key: string;
|
|
10
|
+
/** The plugin's name for people, in English. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** What the plugin's devDependency on `@skillcdn/console` says: `^<version>` of the command, unless said. */
|
|
13
|
+
readonly consoleDependency: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The words the template uses, and what each becomes. */
|
|
17
|
+
export function substitutions(options: InitOptions): readonly [string, string][] {
|
|
18
|
+
return [
|
|
19
|
+
["__key__", options.key],
|
|
20
|
+
["__name__", options.name],
|
|
21
|
+
["__console__", options.consoleDependency],
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function substitute(text: string, options: InitOptions): string {
|
|
26
|
+
let result = text;
|
|
27
|
+
for (const [word, value] of substitutions(options)) {
|
|
28
|
+
result = result.replaceAll(word, value);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A name for people from a key: `leave-requests` is "Leave Requests". */
|
|
34
|
+
export function nameFromKey(key: string): string {
|
|
35
|
+
return key
|
|
36
|
+
.split("-")
|
|
37
|
+
.filter((word) => word.length > 0)
|
|
38
|
+
.map((word) => `${word[0]?.toUpperCase() ?? ""}${word.slice(1)}`)
|
|
39
|
+
.join(" ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function* walk(dir: string): AsyncGenerator<string> {
|
|
43
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
44
|
+
const path = join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
yield* walk(path);
|
|
47
|
+
} else if (entry.isFile()) {
|
|
48
|
+
yield path;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The files of the template, as paths relative to it with forward slashes. */
|
|
54
|
+
export async function templateFiles(templateDir: string): Promise<string[]> {
|
|
55
|
+
const files: string[] = [];
|
|
56
|
+
for await (const path of walk(templateDir)) {
|
|
57
|
+
files.push(relative(templateDir, path).split(sep).join("/"));
|
|
58
|
+
}
|
|
59
|
+
return files.sort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Writes the template into `targetDir`, which must not exist or be an empty directory, and
|
|
64
|
+
* answers the files written, relative to it.
|
|
65
|
+
*/
|
|
66
|
+
export async function writePluginFromTemplate(
|
|
67
|
+
templateDir: string,
|
|
68
|
+
targetDir: string,
|
|
69
|
+
options: InitOptions,
|
|
70
|
+
): Promise<string[]> {
|
|
71
|
+
const found = await stat(targetDir).catch(() => null);
|
|
72
|
+
if (found !== null) {
|
|
73
|
+
if (!found.isDirectory()) {
|
|
74
|
+
throw new Error(`${targetDir} exists and is not a directory`);
|
|
75
|
+
}
|
|
76
|
+
if ((await readdir(targetDir)).length > 0) {
|
|
77
|
+
throw new Error(`${targetDir} is not empty`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const written: string[] = [];
|
|
81
|
+
for (const file of await templateFiles(templateDir)) {
|
|
82
|
+
const target = substitute(file, options);
|
|
83
|
+
const text = await readFile(join(templateDir, file), "utf8");
|
|
84
|
+
const path = join(targetDir, ...target.split("/"));
|
|
85
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
86
|
+
await writeFile(path, substitute(text, options), "utf8");
|
|
87
|
+
written.push(target);
|
|
88
|
+
}
|
|
89
|
+
return written;
|
|
90
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import * as z from "zod/mini";
|
|
7
|
+
import { loadServerModule } from "../plugin/load.js";
|
|
8
|
+
import { runPluginCli } from "./plugin-cli.js";
|
|
9
|
+
|
|
10
|
+
// The `console-plugin` command, the package's second bin: the only place it reads the process.
|
|
11
|
+
// Everything it does is in plugin-cli.ts, which takes what it needs and is what the tests run.
|
|
12
|
+
|
|
13
|
+
/** The package's own directory, two levels above dist/cli: where the template is. */
|
|
14
|
+
const packageDir = fileURLToPath(new URL("../../", import.meta.url));
|
|
15
|
+
const manifest = z
|
|
16
|
+
.object({ version: z.string() })
|
|
17
|
+
.safeParse(createRequire(import.meta.url)("../../package.json"));
|
|
18
|
+
|
|
19
|
+
process.exitCode = await runPluginCli(process.argv.slice(2), {
|
|
20
|
+
stdout: (text) => {
|
|
21
|
+
process.stdout.write(text);
|
|
22
|
+
},
|
|
23
|
+
stderr: (text) => {
|
|
24
|
+
process.stderr.write(text);
|
|
25
|
+
},
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
templateDir: join(packageDir, "template", "plugin"),
|
|
28
|
+
version: manifest.success ? manifest.data.version : undefined,
|
|
29
|
+
loadServer: (file) => loadServerModule(file),
|
|
30
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
connectPath,
|
|
6
6
|
personTokensPath,
|
|
7
7
|
personWorkPath,
|
|
8
|
+
pluginPath,
|
|
8
9
|
projectPath,
|
|
9
10
|
REST_ROUTES,
|
|
10
11
|
restPath,
|
|
@@ -52,6 +53,21 @@ import {
|
|
|
52
53
|
type RestPerson,
|
|
53
54
|
type RestPersonPatch,
|
|
54
55
|
type RestPersonWork,
|
|
56
|
+
type RestPluginCommandInput,
|
|
57
|
+
type RestPluginDetail,
|
|
58
|
+
type RestPluginEnableInput,
|
|
59
|
+
type RestPluginEvents,
|
|
60
|
+
type RestPluginFileHandle,
|
|
61
|
+
type RestPluginGrant,
|
|
62
|
+
type RestPluginGrantInput,
|
|
63
|
+
type RestPluginGrants,
|
|
64
|
+
type RestPluginInvocation,
|
|
65
|
+
type RestPluginInvokeInput,
|
|
66
|
+
type RestPluginRecordPage,
|
|
67
|
+
type RestPluginRecordRead,
|
|
68
|
+
type RestPluginRecordVersions,
|
|
69
|
+
type RestPluginSettingsPatch,
|
|
70
|
+
type RestPlugins,
|
|
55
71
|
type RestProject,
|
|
56
72
|
type RestProjectInput,
|
|
57
73
|
type RestProjectPatch,
|
|
@@ -108,6 +124,16 @@ import {
|
|
|
108
124
|
restPeopleSchema,
|
|
109
125
|
restPersonSchema,
|
|
110
126
|
restPersonWorkSchema,
|
|
127
|
+
restPluginDetailSchema,
|
|
128
|
+
restPluginEventsSchema,
|
|
129
|
+
restPluginFileHandleSchema,
|
|
130
|
+
restPluginGrantSchema,
|
|
131
|
+
restPluginGrantsSchema,
|
|
132
|
+
restPluginInvocationSchema,
|
|
133
|
+
restPluginRecordPageSchema,
|
|
134
|
+
restPluginRecordReadSchema,
|
|
135
|
+
restPluginRecordVersionsSchema,
|
|
136
|
+
restPluginsSchema,
|
|
111
137
|
restProjectRulesSchema,
|
|
112
138
|
restProjectSchema,
|
|
113
139
|
restProjectsSchema,
|
|
@@ -404,6 +430,126 @@ export interface ProjectClient {
|
|
|
404
430
|
updateGoal(id: string, patch: RestGoalPatch): Promise<RestGoal>;
|
|
405
431
|
}
|
|
406
432
|
|
|
433
|
+
/** What a read of a plugin's collection is narrowed by (docs/specs/plugins.md, "Records"). */
|
|
434
|
+
export interface RecordFilter {
|
|
435
|
+
/** Conditions on indexed fields, as `ctx.records().list` takes them. */
|
|
436
|
+
readonly where?: Readonly<Record<string, unknown>> | undefined;
|
|
437
|
+
/** A field, with `-` before it for descending. */
|
|
438
|
+
readonly order?: string | undefined;
|
|
439
|
+
readonly limit?: number | undefined;
|
|
440
|
+
/** The cursor the last page answered with. */
|
|
441
|
+
readonly after?: string | undefined;
|
|
442
|
+
/** Words to search the collection's search fields for. */
|
|
443
|
+
readonly q?: string | undefined;
|
|
444
|
+
/** The key of a project, for the records linked to it. */
|
|
445
|
+
readonly project?: string | undefined;
|
|
446
|
+
readonly archived?: boolean | undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* The client of one plugin (docs/specs/plugins.md, "REST"): its commands and queries run as the
|
|
451
|
+
* person the client acts as, its records read within their roles, its skills, its events; and,
|
|
452
|
+
* for an administrator, enabling, configuring and granting. Every answer is in the console's
|
|
453
|
+
* generic shapes; what a command or a query itself answers is the plugin's own.
|
|
454
|
+
*/
|
|
455
|
+
export interface PluginClient {
|
|
456
|
+
readonly key: string;
|
|
457
|
+
/** The plugin whole: its commands and queries with their arguments, its skills, its settings. */
|
|
458
|
+
get(signal?: AbortSignal): Promise<RestPluginDetail>;
|
|
459
|
+
/** Runs a command or a query by the words as typed, which the console parses against the definition. */
|
|
460
|
+
invoke(input: RestPluginInvokeInput): Promise<RestPluginInvocation>;
|
|
461
|
+
/** Runs a command with its arguments as JSON, by name, for a page. */
|
|
462
|
+
command(name: string, input: RestPluginCommandInput): Promise<RestPluginInvocation>;
|
|
463
|
+
/** Runs a query with its arguments as JSON; the answer is the query's own JSON. */
|
|
464
|
+
query(
|
|
465
|
+
name: string,
|
|
466
|
+
args: Readonly<Record<string, unknown>>,
|
|
467
|
+
options?: { readonly project?: string | undefined },
|
|
468
|
+
signal?: AbortSignal,
|
|
469
|
+
): Promise<unknown>;
|
|
470
|
+
/** Where a query's answer is read as bytes, for one that answers a file. */
|
|
471
|
+
queryUrl(
|
|
472
|
+
name: string,
|
|
473
|
+
args: Readonly<Record<string, unknown>>,
|
|
474
|
+
options?: { readonly project?: string | undefined },
|
|
475
|
+
): string;
|
|
476
|
+
/** Uploads a file for a command, with the `files` capability: the handle a `file` argument takes, good for an hour. */
|
|
477
|
+
upload(input: FileUpload): Promise<RestPluginFileHandle>;
|
|
478
|
+
/** A page of a collection's records, as the person may read them. */
|
|
479
|
+
records(
|
|
480
|
+
collection: string,
|
|
481
|
+
filter?: RecordFilter,
|
|
482
|
+
signal?: AbortSignal,
|
|
483
|
+
): Promise<RestPluginRecordPage>;
|
|
484
|
+
/** One record, or one version of it. */
|
|
485
|
+
record(
|
|
486
|
+
collection: string,
|
|
487
|
+
id: string,
|
|
488
|
+
version?: number,
|
|
489
|
+
signal?: AbortSignal,
|
|
490
|
+
): Promise<RestPluginRecordRead>;
|
|
491
|
+
recordVersions(
|
|
492
|
+
collection: string,
|
|
493
|
+
id: string,
|
|
494
|
+
signal?: AbortSignal,
|
|
495
|
+
): Promise<RestPluginRecordVersions>;
|
|
496
|
+
/** Where a record's file is read, by its id. */
|
|
497
|
+
fileUrl(fileId: string): string;
|
|
498
|
+
/** The bytes of a record's file, with the name and the type the console says. */
|
|
499
|
+
readFile(fileId: string, signal?: AbortSignal): Promise<FileRead>;
|
|
500
|
+
/** Where the frame of a page or a widget is (ADR-0054), on a project when its key is given. */
|
|
501
|
+
frameUrl(options: {
|
|
502
|
+
readonly page?: string | undefined;
|
|
503
|
+
readonly widget?: string | undefined;
|
|
504
|
+
readonly project?: string | undefined;
|
|
505
|
+
}): string;
|
|
506
|
+
/** The events the plugin emitted after number `after`, oldest first: what a frame hears. */
|
|
507
|
+
pluginEvents(
|
|
508
|
+
page: { readonly after: number; readonly limit: number },
|
|
509
|
+
signal?: AbortSignal,
|
|
510
|
+
): Promise<RestPluginEvents>;
|
|
511
|
+
/** Removes everything the console keeps of a disabled plugin, the key named again; for an administrator. */
|
|
512
|
+
purge(confirm: string): Promise<void>;
|
|
513
|
+
/** The plugin's skills, a pack of its own read from its package, as a project's five reads. */
|
|
514
|
+
skills(signal?: AbortSignal): Promise<RestSkills>;
|
|
515
|
+
skill(path: string, cursor?: string, signal?: AbortSignal): Promise<RestSkillLoad>;
|
|
516
|
+
skillFile(
|
|
517
|
+
path: string,
|
|
518
|
+
page?: { readonly offset?: number | undefined; readonly limit?: number | undefined },
|
|
519
|
+
signal?: AbortSignal,
|
|
520
|
+
): Promise<RestSkillFile>;
|
|
521
|
+
skillBrowse(
|
|
522
|
+
page?: {
|
|
523
|
+
readonly path?: string | undefined;
|
|
524
|
+
readonly cursor?: string | undefined;
|
|
525
|
+
readonly limit?: number | undefined;
|
|
526
|
+
},
|
|
527
|
+
signal?: AbortSignal,
|
|
528
|
+
): Promise<RestSkillBrowse>;
|
|
529
|
+
skillFind(
|
|
530
|
+
query: string,
|
|
531
|
+
options?: {
|
|
532
|
+
readonly path?: string | undefined;
|
|
533
|
+
readonly cursor?: string | undefined;
|
|
534
|
+
readonly limit?: number | undefined;
|
|
535
|
+
},
|
|
536
|
+
signal?: AbortSignal,
|
|
537
|
+
): Promise<RestSkillFind>;
|
|
538
|
+
/** The plugin's events, newest first, a page at a time. */
|
|
539
|
+
events(page?: Page, signal?: AbortSignal): Promise<RestEvents>;
|
|
540
|
+
/** Enables the plugin, with consent to every capability it asks; for an administrator. */
|
|
541
|
+
enable(input: RestPluginEnableInput): Promise<RestPluginDetail>;
|
|
542
|
+
disable(): Promise<RestPluginDetail>;
|
|
543
|
+
/** Changes the settings' values, without secrets; `null` clears one. */
|
|
544
|
+
updateSettings(patch: RestPluginSettingsPatch): Promise<RestPluginDetail>;
|
|
545
|
+
/** Seals a secret for the plugin; it is answered by nothing from then on. */
|
|
546
|
+
setSecret(name: string, value: string): Promise<void>;
|
|
547
|
+
removeSecret(name: string): Promise<void>;
|
|
548
|
+
grants(signal?: AbortSignal): Promise<RestPluginGrants>;
|
|
549
|
+
grant(input: RestPluginGrantInput): Promise<RestPluginGrant>;
|
|
550
|
+
revoke(grantId: string): Promise<RestPluginGrant>;
|
|
551
|
+
}
|
|
552
|
+
|
|
407
553
|
export interface ConsoleClient {
|
|
408
554
|
me(signal?: AbortSignal): Promise<RestMe>;
|
|
409
555
|
/** Gives up the token this client acts with, so that it is good for nothing from now on: what `console logout` does. For a token; a session has none. */
|
|
@@ -443,6 +589,10 @@ export interface ConsoleClient {
|
|
|
443
589
|
createProject(input: RestProjectInput): Promise<RestProject>;
|
|
444
590
|
/** The board of one project. */
|
|
445
591
|
project(key: string): ProjectClient;
|
|
592
|
+
/** The plugins the asker may know of: the enabled ones, every one for an administrator. */
|
|
593
|
+
plugins(signal?: AbortSignal): Promise<RestPlugins>;
|
|
594
|
+
/** One plugin, by its key. */
|
|
595
|
+
plugin(key: string): PluginClient;
|
|
446
596
|
/** The workspace's own events, the ones about no project, after event number `after`. */
|
|
447
597
|
events(after: number, signal?: AbortSignal): Promise<RestEvents>;
|
|
448
598
|
/** Where a picture from elsewhere is read through the console (ADR-0048), given the https URL that names it. */
|
|
@@ -915,6 +1065,147 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
915
1065
|
};
|
|
916
1066
|
};
|
|
917
1067
|
|
|
1068
|
+
/** A schema that takes whatever JSON came: a query's own answer. */
|
|
1069
|
+
const anything: Schema<unknown> = { safeParse: (data) => ({ success: true, data }) };
|
|
1070
|
+
|
|
1071
|
+
const pluginClient = (key: string): PluginClient => {
|
|
1072
|
+
const at = (...parts: readonly string[]) => pluginPath(key, ...parts);
|
|
1073
|
+
const skillsQuery = (part: string, params: Record<string, string | undefined>): string =>
|
|
1074
|
+
withQuery(at("skills", part), params);
|
|
1075
|
+
const queryUrl = (
|
|
1076
|
+
name: string,
|
|
1077
|
+
args: Readonly<Record<string, unknown>>,
|
|
1078
|
+
options: { readonly project?: string | undefined } = {},
|
|
1079
|
+
): string =>
|
|
1080
|
+
withQuery(at("queries", name), { args: JSON.stringify(args), project: options.project });
|
|
1081
|
+
return {
|
|
1082
|
+
key,
|
|
1083
|
+
get: (signal) => request("GET", `${base}${at()}`, undefined, restPluginDetailSchema, signal),
|
|
1084
|
+
invoke: (input) =>
|
|
1085
|
+
request("POST", `${base}${at("invoke")}`, input, restPluginInvocationSchema),
|
|
1086
|
+
command: (name, input) =>
|
|
1087
|
+
request("POST", `${base}${at("commands", name)}`, input, restPluginInvocationSchema),
|
|
1088
|
+
query: (name, args, options = {}, signal) =>
|
|
1089
|
+
request("GET", queryUrl(name, args, options), undefined, anything, signal),
|
|
1090
|
+
queryUrl,
|
|
1091
|
+
upload: (input) =>
|
|
1092
|
+
request("POST", `${base}${at("files")}`, formOf(input), restPluginFileHandleSchema),
|
|
1093
|
+
records: (collection, filter = {}, signal) =>
|
|
1094
|
+
request(
|
|
1095
|
+
"GET",
|
|
1096
|
+
withQuery(at("records", collection), {
|
|
1097
|
+
where: filter.where === undefined ? undefined : JSON.stringify(filter.where),
|
|
1098
|
+
order: filter.order,
|
|
1099
|
+
limit: number(filter.limit),
|
|
1100
|
+
after: filter.after,
|
|
1101
|
+
q: filter.q,
|
|
1102
|
+
project: filter.project,
|
|
1103
|
+
archived: flag(filter.archived),
|
|
1104
|
+
}),
|
|
1105
|
+
undefined,
|
|
1106
|
+
restPluginRecordPageSchema,
|
|
1107
|
+
signal,
|
|
1108
|
+
),
|
|
1109
|
+
record: (collection, id, version, signal) =>
|
|
1110
|
+
request(
|
|
1111
|
+
"GET",
|
|
1112
|
+
withQuery(at("records", collection, id), { version: number(version) }),
|
|
1113
|
+
undefined,
|
|
1114
|
+
restPluginRecordReadSchema,
|
|
1115
|
+
signal,
|
|
1116
|
+
),
|
|
1117
|
+
recordVersions: (collection, id, signal) =>
|
|
1118
|
+
request(
|
|
1119
|
+
"GET",
|
|
1120
|
+
`${base}${at("records", collection, id, "versions")}`,
|
|
1121
|
+
undefined,
|
|
1122
|
+
restPluginRecordVersionsSchema,
|
|
1123
|
+
signal,
|
|
1124
|
+
),
|
|
1125
|
+
fileUrl: (fileId) => `${base}${at("files", fileId)}`,
|
|
1126
|
+
readFile: (fileId, signal) => read(`${base}${at("files", fileId)}`, fileId, signal),
|
|
1127
|
+
frameUrl: (options) =>
|
|
1128
|
+
withQuery(at("frame"), {
|
|
1129
|
+
page: options.page,
|
|
1130
|
+
widget: options.widget,
|
|
1131
|
+
project: options.project,
|
|
1132
|
+
}),
|
|
1133
|
+
pluginEvents: (page, signal) =>
|
|
1134
|
+
request(
|
|
1135
|
+
"GET",
|
|
1136
|
+
withQuery(at("plugin-events"), { after: String(page.after), limit: String(page.limit) }),
|
|
1137
|
+
undefined,
|
|
1138
|
+
restPluginEventsSchema,
|
|
1139
|
+
signal,
|
|
1140
|
+
),
|
|
1141
|
+
purge: (confirm) => request("POST", `${base}${at("purge")}`, { confirm }, undefined),
|
|
1142
|
+
skills: (signal) =>
|
|
1143
|
+
request("GET", `${base}${at("skills")}`, undefined, restSkillsSchema, signal),
|
|
1144
|
+
skill: (path, cursor, signal) =>
|
|
1145
|
+
request(
|
|
1146
|
+
"GET",
|
|
1147
|
+
skillsQuery("one", { path, cursor }),
|
|
1148
|
+
undefined,
|
|
1149
|
+
restSkillLoadSchema,
|
|
1150
|
+
signal,
|
|
1151
|
+
),
|
|
1152
|
+
skillFile: (path, page = {}, signal) =>
|
|
1153
|
+
request(
|
|
1154
|
+
"GET",
|
|
1155
|
+
skillsQuery("file", { path, offset: number(page.offset), limit: number(page.limit) }),
|
|
1156
|
+
undefined,
|
|
1157
|
+
restSkillFileSchema,
|
|
1158
|
+
signal,
|
|
1159
|
+
),
|
|
1160
|
+
skillBrowse: (page = {}, signal) =>
|
|
1161
|
+
request(
|
|
1162
|
+
"GET",
|
|
1163
|
+
skillsQuery("browse", {
|
|
1164
|
+
path: page.path,
|
|
1165
|
+
cursor: page.cursor,
|
|
1166
|
+
limit: number(page.limit),
|
|
1167
|
+
}),
|
|
1168
|
+
undefined,
|
|
1169
|
+
restSkillBrowseSchema,
|
|
1170
|
+
signal,
|
|
1171
|
+
),
|
|
1172
|
+
skillFind: (query, options = {}, signal) =>
|
|
1173
|
+
request(
|
|
1174
|
+
"GET",
|
|
1175
|
+
skillsQuery("find", {
|
|
1176
|
+
query,
|
|
1177
|
+
path: options.path,
|
|
1178
|
+
cursor: options.cursor,
|
|
1179
|
+
limit: number(options.limit),
|
|
1180
|
+
}),
|
|
1181
|
+
undefined,
|
|
1182
|
+
restSkillFindSchema,
|
|
1183
|
+
signal,
|
|
1184
|
+
),
|
|
1185
|
+
events: (page = {}, signal) =>
|
|
1186
|
+
request(
|
|
1187
|
+
"GET",
|
|
1188
|
+
withQuery(at("events"), { limit: number(page.limit), offset: number(page.offset) }),
|
|
1189
|
+
undefined,
|
|
1190
|
+
restEventsSchema,
|
|
1191
|
+
signal,
|
|
1192
|
+
),
|
|
1193
|
+
enable: (input) => request("POST", `${base}${at("enable")}`, input, restPluginDetailSchema),
|
|
1194
|
+
disable: () => request("POST", `${base}${at("disable")}`, undefined, restPluginDetailSchema),
|
|
1195
|
+
updateSettings: (patch) =>
|
|
1196
|
+
request("PATCH", `${base}${at("settings")}`, patch, restPluginDetailSchema),
|
|
1197
|
+
setSecret: (name, value) =>
|
|
1198
|
+
request("PUT", `${base}${at("secrets", name)}`, { value }, undefined),
|
|
1199
|
+
removeSecret: (name) =>
|
|
1200
|
+
request("DELETE", `${base}${at("secrets", name)}`, undefined, undefined),
|
|
1201
|
+
grants: (signal) =>
|
|
1202
|
+
request("GET", `${base}${at("grants")}`, undefined, restPluginGrantsSchema, signal),
|
|
1203
|
+
grant: (input) => request("POST", `${base}${at("grants")}`, input, restPluginGrantSchema),
|
|
1204
|
+
revoke: (grantId) =>
|
|
1205
|
+
request("DELETE", `${base}${at("grants", grantId)}`, undefined, restPluginGrantSchema),
|
|
1206
|
+
};
|
|
1207
|
+
};
|
|
1208
|
+
|
|
918
1209
|
return {
|
|
919
1210
|
me: (signal) => request("GET", `${base}${REST_ROUTES.me}`, undefined, restMeSchema, signal),
|
|
920
1211
|
giveUpToken: () => request("DELETE", `${base}${REST_ROUTES.meToken}`, undefined, undefined),
|
|
@@ -972,6 +1263,9 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
|
|
|
972
1263
|
createProject: (input) =>
|
|
973
1264
|
request("POST", `${base}${REST_ROUTES.projects}`, input, restProjectSchema),
|
|
974
1265
|
project: projectClient,
|
|
1266
|
+
plugins: (signal) =>
|
|
1267
|
+
request("GET", `${base}${REST_ROUTES.plugins}`, undefined, restPluginsSchema, signal),
|
|
1268
|
+
plugin: pluginClient,
|
|
975
1269
|
events: (after, signal) =>
|
|
976
1270
|
request(
|
|
977
1271
|
"GET",
|
package/src/compat.ts
CHANGED
|
@@ -113,7 +113,7 @@ export interface ClientPolicy {
|
|
|
113
113
|
* last changed what the command reads or sends (ADR-0038), whose changelog entry begins with
|
|
114
114
|
* `Breaking:`; `pnpm check` holds it to a version the package has, or is about to have.
|
|
115
115
|
*/
|
|
116
|
-
export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.
|
|
116
|
+
export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.2" };
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
119
|
* The oldest console this command works with (ADR-0038): raised by the release that last
|
|
@@ -122,7 +122,7 @@ export const CLIENT_POLICY: ClientPolicy = { atLeast: "0.4.1" };
|
|
|
122
122
|
* itself, naming the command to install instead; `pnpm check` holds it as it holds the
|
|
123
123
|
* console's floor.
|
|
124
124
|
*/
|
|
125
|
-
export const CONSOLE_AT_LEAST = "0.4.
|
|
125
|
+
export const CONSOLE_AT_LEAST = "0.4.2";
|
|
126
126
|
|
|
127
127
|
/** `common` from `atLeast` on, where that floor is within it; `common` as it is otherwise. */
|
|
128
128
|
function narrowed(common: VersionRange, atLeast: string | undefined): VersionRange {
|
package/src/components/board.tsx
CHANGED
|
@@ -80,7 +80,28 @@ const HOLD_MS = 300;
|
|
|
80
80
|
const HOLD_SLACK = 8;
|
|
81
81
|
/** How near the pointer comes to the board's edge before the board scrolls to show more columns. */
|
|
82
82
|
const EDGE = 48;
|
|
83
|
-
|
|
83
|
+
/** How fast the board scrolls, in pixels a second, with the pointer at the edge itself; slower further in. */
|
|
84
|
+
const EDGE_SPEED = 720;
|
|
85
|
+
/** The most a frame counts for, so that a tab that was away does not leap when it is back. */
|
|
86
|
+
const FRAME_AT_MOST_MS = 64;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* How far the board scrolls in one frame for a pointer at `x`, the board spanning `left` to
|
|
90
|
+
* `right` on the screen: nothing away from the edges; towards the edge the pointer is near, the
|
|
91
|
+
* nearer the faster, and no faster beyond it. Asked every frame while a card is held, not only
|
|
92
|
+
* when the pointer moves, so that a card held still at the edge carries the board on, as an icon
|
|
93
|
+
* held at the edge of a phone's screen turns the page. Negative is leftwards.
|
|
94
|
+
*/
|
|
95
|
+
export function edgeScrollOf(x: number, left: number, right: number, elapsedMs: number): number {
|
|
96
|
+
const ms = Math.min(Math.max(elapsedMs, 0), FRAME_AT_MOST_MS);
|
|
97
|
+
const depth = (distance: number) => Math.min(1, Math.max(0, (EDGE - distance) / EDGE));
|
|
98
|
+
const leftward = depth(x - left);
|
|
99
|
+
const rightward = depth(right - x);
|
|
100
|
+
if (leftward === 0 && rightward === 0) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
return ((leftward > rightward ? -leftward : rightward) * EDGE_SPEED * ms) / 1000;
|
|
104
|
+
}
|
|
84
105
|
|
|
85
106
|
/** The state a column stands for, read off its `data-column`; nothing for a value that is not one. */
|
|
86
107
|
export function columnStateOf(value: string | null | undefined): TaskState | undefined {
|
|
@@ -422,10 +443,10 @@ export function Board(props: BoardProps) {
|
|
|
422
443
|
}, []);
|
|
423
444
|
|
|
424
445
|
// While a card is held, the pointer is followed over the whole document: the column under it
|
|
425
|
-
// is marked, the board scrolls
|
|
426
|
-
// A finger's hold is timed here, and once the card has come loose the
|
|
427
|
-
// under the finger, nor asks for the menu a long press would otherwise
|
|
428
|
-
// follows a drag opens nothing.
|
|
446
|
+
// is marked, the board scrolls while the pointer is near its edge, moving or still, and the
|
|
447
|
+
// release moves the task. A finger's hold is timed here, and once the card has come loose the
|
|
448
|
+
// page no longer scrolls under the finger, nor asks for the menu a long press would otherwise
|
|
449
|
+
// bring up; the click that follows a drag opens nothing.
|
|
429
450
|
const held = drag !== null;
|
|
430
451
|
useEffect(() => {
|
|
431
452
|
if (!held) {
|
|
@@ -435,8 +456,42 @@ export function Board(props: BoardProps) {
|
|
|
435
456
|
columnStateOf(
|
|
436
457
|
document.elementFromPoint(x, y)?.closest<HTMLElement>("[data-column]")?.dataset.column,
|
|
437
458
|
);
|
|
459
|
+
// Where the pointer last was, for the frames between its moves.
|
|
460
|
+
const pointer = { x: current.current?.x ?? 0, y: current.current?.y ?? 0 };
|
|
461
|
+
// The board is carried on every frame the loose card is near its edge; what a frame's share
|
|
462
|
+
// comes to short of a whole pixel is kept for the next, since a scroll position holds whole
|
|
463
|
+
// ones, and the column under the pointer is marked again where the board moved under it.
|
|
464
|
+
let frame = 0;
|
|
465
|
+
let at: number | undefined;
|
|
466
|
+
let rest = 0;
|
|
467
|
+
const carry = (now: number) => {
|
|
468
|
+
frame = window.requestAnimationFrame(carry);
|
|
469
|
+
const elapsed = at === undefined ? 0 : now - at;
|
|
470
|
+
at = now;
|
|
471
|
+
const element = board.current;
|
|
472
|
+
if (element === null || !loose.current) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const rect = element.getBoundingClientRect();
|
|
476
|
+
const share = edgeScrollOf(pointer.x, rect.left, rect.right, elapsed) + rest;
|
|
477
|
+
const whole = Math.trunc(share);
|
|
478
|
+
rest = share - whole;
|
|
479
|
+
if (whole === 0) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const was = element.scrollLeft;
|
|
483
|
+
element.scrollLeft = was + whole;
|
|
484
|
+
if (element.scrollLeft !== was) {
|
|
485
|
+
const over = columnUnder(pointer.x, pointer.y);
|
|
486
|
+
setDrag((before) =>
|
|
487
|
+
before === null || before.over === over ? before : { ...before, over },
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
438
491
|
const follow = (event: PointerEvent) => {
|
|
439
492
|
const { clientX: x, clientY: y } = event;
|
|
493
|
+
pointer.x = x;
|
|
494
|
+
pointer.y = y;
|
|
440
495
|
setDrag((before) => {
|
|
441
496
|
if (before === null) {
|
|
442
497
|
return before;
|
|
@@ -455,15 +510,6 @@ export function Board(props: BoardProps) {
|
|
|
455
510
|
}
|
|
456
511
|
return { ...before, x, y, moving: true, over: columnUnder(x, y) };
|
|
457
512
|
});
|
|
458
|
-
const element = board.current;
|
|
459
|
-
if (element !== null && loose.current) {
|
|
460
|
-
const rect = element.getBoundingClientRect();
|
|
461
|
-
if (x < rect.left + EDGE) {
|
|
462
|
-
element.scrollLeft -= EDGE_STEP;
|
|
463
|
-
} else if (x > rect.right - EDGE) {
|
|
464
|
-
element.scrollLeft += EDGE_STEP;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
513
|
};
|
|
468
514
|
const loosen = () => {
|
|
469
515
|
setDrag((before) => {
|
|
@@ -502,6 +548,7 @@ export function Board(props: BoardProps) {
|
|
|
502
548
|
};
|
|
503
549
|
const quiet = (event: Event) => event.preventDefault();
|
|
504
550
|
const timer = current.current?.hold === true ? window.setTimeout(loosen, HOLD_MS) : undefined;
|
|
551
|
+
frame = window.requestAnimationFrame(carry);
|
|
505
552
|
window.addEventListener("pointermove", follow);
|
|
506
553
|
window.addEventListener("pointerup", release);
|
|
507
554
|
window.addEventListener("pointercancel", release);
|
|
@@ -511,6 +558,7 @@ export function Board(props: BoardProps) {
|
|
|
511
558
|
if (timer !== undefined) {
|
|
512
559
|
window.clearTimeout(timer);
|
|
513
560
|
}
|
|
561
|
+
window.cancelAnimationFrame(frame);
|
|
514
562
|
window.removeEventListener("pointermove", follow);
|
|
515
563
|
window.removeEventListener("pointerup", release);
|
|
516
564
|
window.removeEventListener("pointercancel", release);
|
|
@@ -10,8 +10,15 @@ import { formatInstant, PersonChip } from "./ui.js";
|
|
|
10
10
|
// as which agent when through one, and what. It takes its data as props and nothing from the
|
|
11
11
|
// network; the sentence is the language's (ADR-0012).
|
|
12
12
|
|
|
13
|
-
/** What an event says, without its actor: the words after the name, in the words given. */
|
|
13
|
+
/** What an event says, without its actor: the words after the name, in the words given; a board's event a plugin caused says so (ADR-0052). */
|
|
14
14
|
export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
15
|
+
const sentence = sentenceOf(event, words);
|
|
16
|
+
return event.plugin !== null && !event.kind.startsWith("plugin.")
|
|
17
|
+
? `${sentence} ${words.feed.throughPlugin(event.plugin)}`
|
|
18
|
+
: sentence;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sentenceOf(event: RestEvent, words: Messages): string {
|
|
15
22
|
const { data } = event;
|
|
16
23
|
const { feed, vocabulary } = words;
|
|
17
24
|
const task =
|
|
@@ -34,6 +41,15 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
|
34
41
|
const question = data.question ?? feed.aQuestion;
|
|
35
42
|
const goal = data.title ?? feed.aGoal;
|
|
36
43
|
const category = data.name ?? data.key ?? feed.aCategory;
|
|
44
|
+
const plugin = event.plugin ?? feed.aPlugin;
|
|
45
|
+
const grantee =
|
|
46
|
+
data.everyone === true
|
|
47
|
+
? feed.everyone
|
|
48
|
+
: data.login !== undefined
|
|
49
|
+
? data.login
|
|
50
|
+
: data.key !== undefined
|
|
51
|
+
? feed.membersOf(data.name ?? data.key)
|
|
52
|
+
: feed.someone;
|
|
37
53
|
switch (event.kind) {
|
|
38
54
|
case "person.joined":
|
|
39
55
|
return feed.event.personJoined();
|
|
@@ -115,6 +131,31 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
|
|
|
115
131
|
return feed.event.documentRestored(page);
|
|
116
132
|
case "document.file_attached":
|
|
117
133
|
return feed.event.fileAttached(data.label ?? feed.aFile, page);
|
|
134
|
+
case "plugin.enabled":
|
|
135
|
+
return feed.event.pluginEnabled(plugin);
|
|
136
|
+
case "plugin.disabled":
|
|
137
|
+
return feed.event.pluginDisabled(plugin);
|
|
138
|
+
case "plugin.configured":
|
|
139
|
+
return feed.event.pluginConfigured(fields, plugin);
|
|
140
|
+
case "plugin.granted":
|
|
141
|
+
return feed.event.pluginGranted(data.role ?? feed.role.other, grantee, plugin);
|
|
142
|
+
case "plugin.revoked":
|
|
143
|
+
return feed.event.pluginRevoked(data.role ?? feed.role.other, grantee, plugin);
|
|
144
|
+
case "plugin.upgraded":
|
|
145
|
+
return feed.event.pluginUpgraded(
|
|
146
|
+
plugin,
|
|
147
|
+
data.version === undefined ? feed.something : String(data.version),
|
|
148
|
+
);
|
|
149
|
+
case "plugin.command":
|
|
150
|
+
return feed.event.pluginCommand(
|
|
151
|
+
data.command ?? feed.something,
|
|
152
|
+
plugin,
|
|
153
|
+
feed.outcome[data.outcome ?? ""] ?? data.outcome ?? feed.something,
|
|
154
|
+
);
|
|
155
|
+
case "plugin.purged":
|
|
156
|
+
return feed.event.pluginPurged(plugin);
|
|
157
|
+
case "plugin.failed":
|
|
158
|
+
return feed.event.pluginFailed(plugin, data.problem ?? feed.something);
|
|
118
159
|
}
|
|
119
160
|
}
|
|
120
161
|
|