@skillcdn/console 0.4.1 → 0.4.3
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 +5 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +569 -48
- 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 +283 -7
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +44 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +26 -0
- package/dist/cli/plugin-check.d.ts.map +1 -0
- package/dist/cli/plugin-check.js +396 -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 +189 -0
- package/dist/cli/plugin-cli.js.map +1 -0
- package/dist/cli/plugin-init.d.ts +37 -0
- package/dist/cli/plugin-init.d.ts.map +1 -0
- package/dist/cli/plugin-init.js +206 -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 +100 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +79 -7
- 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/comments.d.ts.map +1 -1
- package/dist/components/comments.js +1 -1
- package/dist/components/comments.js.map +1 -1
- package/dist/components/decision-form.d.ts +2 -1
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +36 -4
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +4 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +29 -3
- package/dist/components/decision-list.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/my-decisions.d.ts +2 -0
- package/dist/components/my-decisions.d.ts.map +1 -1
- package/dist/components/my-decisions.js +2 -2
- package/dist/components/my-decisions.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/task-view.d.ts +2 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.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 +330 -1
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +152 -41
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +9 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +45 -9
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +155 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +166 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +150 -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/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.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 +380 -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 +194 -9
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +98 -17
- 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 +12 -5
- package/docs/specs/plugins.md +488 -0
- package/docs/specs/rest.md +11 -9
- package/package.json +26 -8
- 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 +12 -0
- package/src/cli/cli.ts +737 -55
- package/src/cli/format.ts +368 -10
- package/src/cli/help.ts +50 -6
- package/src/cli/plugin-check.ts +497 -0
- package/src/cli/plugin-cli.ts +237 -0
- package/src/cli/plugin-init.ts +257 -0
- package/src/cli/plugin-main.ts +30 -0
- package/src/client.ts +322 -9
- package/src/compat.ts +2 -2
- package/src/components/board.tsx +62 -14
- package/src/components/comments.tsx +1 -0
- package/src/components/decision-form.tsx +92 -11
- package/src/components/decision-list.tsx +71 -2
- package/src/components/event-feed.tsx +42 -1
- package/src/components/my-decisions.tsx +4 -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/task-view.tsx +2 -1
- package/src/components/workspace.tsx +18 -0
- package/src/console.tsx +304 -31
- package/src/data.ts +59 -11
- package/src/i18n/en.ts +175 -0
- package/src/i18n/ko.ts +155 -0
- package/src/index.ts +12 -0
- package/src/limits.ts +2 -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 +421 -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 +115 -18
- package/src/styles/console.css +330 -1
- 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,437 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { ConsoleClient, PluginClient } from "../client.js";
|
|
3
|
+
import {
|
|
4
|
+
type BridgeCall,
|
|
5
|
+
type BridgeEventKind,
|
|
6
|
+
type BridgeToFrame,
|
|
7
|
+
isBridgePath,
|
|
8
|
+
isBridgeToPage,
|
|
9
|
+
PLUGIN_BRIDGE_VERSION,
|
|
10
|
+
} from "../plugin/bridge.js";
|
|
11
|
+
import { MAX_PLUGIN_WIDGET_HEIGHT } from "../plugin/limits.js";
|
|
12
|
+
import type { Navigation } from "../router.js";
|
|
13
|
+
import type { RestEvent, RestMe, RestPlugin, RestProject } from "../schemas.js";
|
|
14
|
+
import { CONSOLE_TOKENS } from "../tokens.js";
|
|
15
|
+
|
|
16
|
+
// A plugin's page or widget on the console's page (ADR-0054, docs/specs/plugins.md, "Pages"):
|
|
17
|
+
// a sandboxed frame whose document the console serves, and the console's side of the bridge.
|
|
18
|
+
// The frame speaks over postMessage alone; this component accepts the frame's window alone,
|
|
19
|
+
// performs every call under the plugin's own routes with the person's session, and refuses the
|
|
20
|
+
// rest before any request. Nothing the frame says is rendered as HTML.
|
|
21
|
+
|
|
22
|
+
/** How tall a page's frame may grow: far above any page, below what would break the layout. */
|
|
23
|
+
const MAX_PAGE_HEIGHT = 20_000;
|
|
24
|
+
/** How often the plugin's own events are asked for while a frame listens for them. */
|
|
25
|
+
const EVENTS_POLL_MS = 5_000;
|
|
26
|
+
|
|
27
|
+
export interface PluginFrameProps {
|
|
28
|
+
readonly plugin: RestPlugin;
|
|
29
|
+
/** The page shown, or the widget; one of the two. */
|
|
30
|
+
readonly page?: string | undefined;
|
|
31
|
+
readonly widget?: string | undefined;
|
|
32
|
+
/** The project the page is on, or nothing on the workspace. */
|
|
33
|
+
readonly project?: RestProject | undefined;
|
|
34
|
+
/** What follows the page in the address, handed to the frame. */
|
|
35
|
+
readonly path?: string | undefined;
|
|
36
|
+
readonly me: RestMe;
|
|
37
|
+
readonly language: string;
|
|
38
|
+
readonly client: ConsoleClient;
|
|
39
|
+
readonly navigation: Navigation;
|
|
40
|
+
/** The project's feed, for the frame to hear of the plugin's commands; none on the workspace. */
|
|
41
|
+
readonly events?: readonly RestEvent[] | undefined;
|
|
42
|
+
/** What the frame is called, for assistive technology. */
|
|
43
|
+
readonly title: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A refusal of the bridge, as the frame receives it: the console's code and a message. */
|
|
47
|
+
export class BridgeRefusal extends Error {
|
|
48
|
+
readonly code: string;
|
|
49
|
+
|
|
50
|
+
constructor(code: string, message: string) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "BridgeRefusal";
|
|
53
|
+
this.code = code;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const text = (value: unknown, what: string): string => {
|
|
58
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
59
|
+
throw new BridgeRefusal("plugin.invalid_args", `${what} is text`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const record = (value: unknown): Record<string, unknown> =>
|
|
65
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
66
|
+
? (value as Record<string, unknown>)
|
|
67
|
+
: {};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Performs one `call` of the frame through the plugin's own routes and nothing else: a command,
|
|
71
|
+
* a query, the records, the files. The answer, and what to transfer with it; a method the bridge
|
|
72
|
+
* does not know, or a request outside the plugin, is refused before anything is sent.
|
|
73
|
+
*/
|
|
74
|
+
export async function performBridgeCall(
|
|
75
|
+
plugin: PluginClient,
|
|
76
|
+
projectKey: string | undefined,
|
|
77
|
+
call: BridgeCall,
|
|
78
|
+
): Promise<{ readonly value: unknown; readonly transfer?: Transferable[] }> {
|
|
79
|
+
const params = record(call.params);
|
|
80
|
+
const project = projectKey === undefined ? {} : { project: projectKey };
|
|
81
|
+
switch (call.method) {
|
|
82
|
+
case "command": {
|
|
83
|
+
const name = text(params.name, "name");
|
|
84
|
+
return {
|
|
85
|
+
value: await plugin.command(name, { args: record(params.args), ...project }),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
case "query": {
|
|
89
|
+
const name = text(params.name, "name");
|
|
90
|
+
return { value: await plugin.query(name, record(params.args), project) };
|
|
91
|
+
}
|
|
92
|
+
case "records.list": {
|
|
93
|
+
const collection = text(params.collection, "collection");
|
|
94
|
+
return {
|
|
95
|
+
value: await plugin.records(collection, {
|
|
96
|
+
where: params.where === undefined ? undefined : record(params.where),
|
|
97
|
+
order: typeof params.order === "string" ? params.order : undefined,
|
|
98
|
+
limit: typeof params.limit === "number" ? params.limit : undefined,
|
|
99
|
+
after: typeof params.after === "string" ? params.after : undefined,
|
|
100
|
+
q: typeof params.q === "string" ? params.q : undefined,
|
|
101
|
+
project: typeof params.project === "string" ? params.project : undefined,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case "records.get": {
|
|
106
|
+
const collection = text(params.collection, "collection");
|
|
107
|
+
const id = text(params.id, "id");
|
|
108
|
+
return {
|
|
109
|
+
value: await plugin.record(
|
|
110
|
+
collection,
|
|
111
|
+
id,
|
|
112
|
+
typeof params.version === "number" ? params.version : undefined,
|
|
113
|
+
),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
case "records.versions": {
|
|
117
|
+
const collection = text(params.collection, "collection");
|
|
118
|
+
const id = text(params.id, "id");
|
|
119
|
+
return { value: (await plugin.recordVersions(collection, id)).items };
|
|
120
|
+
}
|
|
121
|
+
case "files.get": {
|
|
122
|
+
const id = text(params.id, "id");
|
|
123
|
+
const file = await plugin.readFile(id);
|
|
124
|
+
const bytes = new Uint8Array(file.bytes).buffer;
|
|
125
|
+
return {
|
|
126
|
+
value: { name: file.name, type: file.contentType, bytes },
|
|
127
|
+
transfer: [bytes],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
case "files.put": {
|
|
131
|
+
const name = text(params.name, "name");
|
|
132
|
+
const type = text(params.type, "type");
|
|
133
|
+
const bytes = params.bytes;
|
|
134
|
+
if (!(bytes instanceof ArrayBuffer)) {
|
|
135
|
+
throw new BridgeRefusal("plugin.invalid_args", "bytes is an ArrayBuffer");
|
|
136
|
+
}
|
|
137
|
+
const handle = await plugin.upload({ name, bytes: new Uint8Array(bytes), contentType: type });
|
|
138
|
+
return { value: { handle: handle.handle } };
|
|
139
|
+
}
|
|
140
|
+
default:
|
|
141
|
+
throw new BridgeRefusal(
|
|
142
|
+
"plugin.forbidden",
|
|
143
|
+
`${String(call.method)} is not a method of the bridge`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The console's computed design tokens, as the page's root has them: what a frame is told to look like. */
|
|
149
|
+
export function readTheme(): Record<string, string> {
|
|
150
|
+
if (typeof document === "undefined" || typeof getComputedStyle === "undefined") {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
const style = getComputedStyle(document.documentElement);
|
|
154
|
+
const theme: Record<string, string> = {};
|
|
155
|
+
for (const token of CONSOLE_TOKENS) {
|
|
156
|
+
const value = style.getPropertyValue(token).trim();
|
|
157
|
+
if (value.length > 0) {
|
|
158
|
+
theme[token] = value.slice(0, 200);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return theme;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** What a failure of a call says to the frame: the console's code and words, or the console's own failure. */
|
|
165
|
+
function errorOf(error: unknown): { readonly code: string; readonly message: string } {
|
|
166
|
+
if (error instanceof BridgeRefusal) {
|
|
167
|
+
return { code: error.code, message: error.message };
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
typeof error === "object" &&
|
|
171
|
+
error !== null &&
|
|
172
|
+
"code" in error &&
|
|
173
|
+
typeof error.code === "string" &&
|
|
174
|
+
"message" in error &&
|
|
175
|
+
typeof error.message === "string"
|
|
176
|
+
) {
|
|
177
|
+
return { code: error.code, message: error.message };
|
|
178
|
+
}
|
|
179
|
+
return { code: "plugin.failed", message: "the call failed" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function PluginFrame(props: PluginFrameProps) {
|
|
183
|
+
const { plugin, page, widget, project, me, language, client, navigation, events } = props;
|
|
184
|
+
const frame = useRef<HTMLIFrameElement>(null);
|
|
185
|
+
const isWidget = widget !== undefined;
|
|
186
|
+
const [height, setHeight] = useState(isWidget ? 160 : 480);
|
|
187
|
+
const subscribed = useRef<Set<BridgeEventKind>>(new Set());
|
|
188
|
+
const ready = useRef(false);
|
|
189
|
+
const lastPluginEvent = useRef(0);
|
|
190
|
+
const seenEvents = useRef(0);
|
|
191
|
+
const path = props.path ?? "";
|
|
192
|
+
const scoped = useMemo(() => client.plugin(plugin.key), [client, plugin.key]);
|
|
193
|
+
const src = useMemo(
|
|
194
|
+
() =>
|
|
195
|
+
scoped.frameUrl({
|
|
196
|
+
...(page === undefined ? {} : { page }),
|
|
197
|
+
...(widget === undefined ? {} : { widget }),
|
|
198
|
+
...(project === undefined ? {} : { project: project.key }),
|
|
199
|
+
}),
|
|
200
|
+
[scoped, page, widget, project],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const post = useCallback((message: BridgeToFrame, transfer?: Transferable[]) => {
|
|
204
|
+
// The frame's origin is opaque, so the target is any: the frame itself checks the sender.
|
|
205
|
+
frame.current?.contentWindow?.postMessage(message, "*", transfer ?? []);
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
// The frame's messages: from its own window alone, each as the bridge spells it.
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (typeof window === "undefined") {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const onMessage = (event: MessageEvent) => {
|
|
214
|
+
const target = frame.current?.contentWindow;
|
|
215
|
+
if (target === null || target === undefined || event.source !== target) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!isBridgeToPage(event.data)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const message = event.data;
|
|
222
|
+
switch (message.t) {
|
|
223
|
+
case "ready":
|
|
224
|
+
ready.current = true;
|
|
225
|
+
post({
|
|
226
|
+
v: PLUGIN_BRIDGE_VERSION,
|
|
227
|
+
t: "init",
|
|
228
|
+
plugin: { key: plugin.key, version: plugin.version },
|
|
229
|
+
...(page === undefined ? {} : { page }),
|
|
230
|
+
...(widget === undefined ? {} : { widget }),
|
|
231
|
+
project: project === undefined ? null : { key: project.key, name: project.name },
|
|
232
|
+
person: {
|
|
233
|
+
id: me.person?.id ?? "",
|
|
234
|
+
login: me.person?.login ?? "",
|
|
235
|
+
name: me.person?.name ?? null,
|
|
236
|
+
language,
|
|
237
|
+
admin: me.person?.role === "admin",
|
|
238
|
+
},
|
|
239
|
+
roles: plugin.roles,
|
|
240
|
+
path,
|
|
241
|
+
language,
|
|
242
|
+
theme: readTheme(),
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
case "call":
|
|
246
|
+
void performBridgeCall(scoped, project?.key, message)
|
|
247
|
+
.then(({ value, transfer }) =>
|
|
248
|
+
post(
|
|
249
|
+
{ v: PLUGIN_BRIDGE_VERSION, t: "result", id: message.id, ok: true, value },
|
|
250
|
+
transfer,
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
.catch((error: unknown) =>
|
|
254
|
+
post({
|
|
255
|
+
v: PLUGIN_BRIDGE_VERSION,
|
|
256
|
+
t: "result",
|
|
257
|
+
id: message.id,
|
|
258
|
+
ok: false,
|
|
259
|
+
error: errorOf(error),
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
return;
|
|
263
|
+
case "navigate":
|
|
264
|
+
if (isBridgePath(message.path)) {
|
|
265
|
+
navigation.go(message.path);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
case "resize":
|
|
269
|
+
setHeight(
|
|
270
|
+
Math.max(
|
|
271
|
+
40,
|
|
272
|
+
Math.min(
|
|
273
|
+
Math.ceil(message.height),
|
|
274
|
+
isWidget ? MAX_PLUGIN_WIDGET_HEIGHT : MAX_PAGE_HEIGHT,
|
|
275
|
+
),
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
case "title":
|
|
280
|
+
if (!isWidget && typeof document !== "undefined") {
|
|
281
|
+
document.title = message.text;
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
case "download": {
|
|
285
|
+
const anchor = document.createElement("a");
|
|
286
|
+
anchor.href = scoped.fileUrl(message.id);
|
|
287
|
+
anchor.download = "";
|
|
288
|
+
anchor.rel = "noopener";
|
|
289
|
+
anchor.click();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
case "subscribe":
|
|
293
|
+
subscribed.current = new Set(message.kinds);
|
|
294
|
+
return;
|
|
295
|
+
default:
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
window.addEventListener("message", onMessage);
|
|
300
|
+
return () => window.removeEventListener("message", onMessage);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// The console's look and language reach the frame as they change.
|
|
304
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: the language is the trigger; the tokens change with it
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (ready.current) {
|
|
307
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "theme", theme: readTheme() });
|
|
308
|
+
}
|
|
309
|
+
}, [language, post]);
|
|
310
|
+
|
|
311
|
+
// The address moved within the page: the frame is told what follows the page now.
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
if (ready.current) {
|
|
314
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "path", path });
|
|
315
|
+
}
|
|
316
|
+
}, [path, post]);
|
|
317
|
+
|
|
318
|
+
// The plugin's commands, as the project's feed tells of them: a frame that listens hears of each.
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (events === undefined) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const fresh = events.slice(seenEvents.current);
|
|
324
|
+
seenEvents.current = events.length;
|
|
325
|
+
if (!ready.current) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
for (const event of fresh) {
|
|
329
|
+
if (event.kind === "plugin.command" && event.plugin === plugin.key) {
|
|
330
|
+
if (subscribed.current.has("command")) {
|
|
331
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "event", kind: "command", data: event.data });
|
|
332
|
+
}
|
|
333
|
+
if (subscribed.current.has("records")) {
|
|
334
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "event", kind: "records", data: event.data });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}, [events, plugin.key, post]);
|
|
339
|
+
|
|
340
|
+
// The plugin's own events, asked for while a frame listens for them.
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
if (typeof window === "undefined") {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const timer = setInterval(() => {
|
|
346
|
+
if (!ready.current || !subscribed.current.has("plugin")) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
scoped
|
|
350
|
+
.pluginEvents({ after: lastPluginEvent.current, limit: 50 })
|
|
351
|
+
.then((page) => {
|
|
352
|
+
for (const event of page.items) {
|
|
353
|
+
lastPluginEvent.current = Math.max(lastPluginEvent.current, event.id);
|
|
354
|
+
post({ v: PLUGIN_BRIDGE_VERSION, t: "event", kind: "plugin", data: event });
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
.catch(() => undefined);
|
|
358
|
+
}, EVENTS_POLL_MS);
|
|
359
|
+
return () => clearInterval(timer);
|
|
360
|
+
}, [scoped, post]);
|
|
361
|
+
|
|
362
|
+
// What the plugin emitted before the frame came is not replayed: the stream is read from now.
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
scoped
|
|
365
|
+
.pluginEvents({ after: 0, limit: 1 })
|
|
366
|
+
.then((page) => {
|
|
367
|
+
if (page.more || page.items.length > 0) {
|
|
368
|
+
// The newest number is past the first page; a frame hears of what comes after it listens.
|
|
369
|
+
return scoped.pluginEvents({ after: 0, limit: 200 }).then((all) => {
|
|
370
|
+
lastPluginEvent.current = all.items.at(-1)?.id ?? 0;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
})
|
|
375
|
+
.catch(() => undefined);
|
|
376
|
+
}, [scoped]);
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<iframe
|
|
380
|
+
ref={frame}
|
|
381
|
+
className={isWidget ? "sc-plugin-frame sc-plugin-frame-widget" : "sc-plugin-frame"}
|
|
382
|
+
src={src}
|
|
383
|
+
title={props.title}
|
|
384
|
+
// As the document's own policy says: scripts, and forms firing their submit event, which
|
|
385
|
+
// the policy's form-action 'none' keeps from going anywhere.
|
|
386
|
+
sandbox="allow-scripts allow-forms"
|
|
387
|
+
referrerPolicy="origin"
|
|
388
|
+
style={{ height: `${height}px` }}
|
|
389
|
+
/>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export interface PluginWidgetsProps {
|
|
394
|
+
readonly plugins: readonly RestPlugin[];
|
|
395
|
+
readonly place: "home:top" | "home:bottom";
|
|
396
|
+
readonly me: RestMe;
|
|
397
|
+
readonly language: string;
|
|
398
|
+
readonly client: ConsoleClient;
|
|
399
|
+
readonly navigation: Navigation;
|
|
400
|
+
/** The widget's name in the person's language. */
|
|
401
|
+
readonly nameOf: (name: Readonly<Record<string, string>>) => string;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The widgets at a place of the front page, in a grid of six columns: a third takes two, a half three, the full six. */
|
|
405
|
+
export function PluginWidgets(props: PluginWidgetsProps) {
|
|
406
|
+
const shown = props.plugins.flatMap((plugin) =>
|
|
407
|
+
plugin.widgets
|
|
408
|
+
.filter((widget) => widget.place === props.place)
|
|
409
|
+
.map((widget) => ({ plugin, widget })),
|
|
410
|
+
);
|
|
411
|
+
if (shown.length === 0) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
return (
|
|
415
|
+
<section
|
|
416
|
+
className={`sc-widgets ${props.place === "home:top" ? "sc-widgets-top" : "sc-widgets-bottom"}`}
|
|
417
|
+
>
|
|
418
|
+
{shown.map(({ plugin, widget }) => (
|
|
419
|
+
<article
|
|
420
|
+
key={`${plugin.key}:${widget.key}`}
|
|
421
|
+
className={`sc-widget sc-widget-${widget.size}`}
|
|
422
|
+
>
|
|
423
|
+
<h2 className="sc-widget-title">{props.nameOf(widget.name)}</h2>
|
|
424
|
+
<PluginFrame
|
|
425
|
+
plugin={plugin}
|
|
426
|
+
widget={widget.key}
|
|
427
|
+
me={props.me}
|
|
428
|
+
language={props.language}
|
|
429
|
+
client={props.client}
|
|
430
|
+
navigation={props.navigation}
|
|
431
|
+
title={props.nameOf(widget.name)}
|
|
432
|
+
/>
|
|
433
|
+
</article>
|
|
434
|
+
))}
|
|
435
|
+
</section>
|
|
436
|
+
);
|
|
437
|
+
}
|