@frockbot/plugin-bot-template 0.0.0 → 0.1.1

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.
@@ -0,0 +1,210 @@
1
+ /// <reference path="../env.d.ts" />
2
+
3
+ // The Bot Template hosted client Contribution.
4
+ //
5
+ // Export mounts as the last primary row, while import stays under Advanced.
6
+ // Nothing in the settings surface has to know this Package exists. Every write
7
+ // is one `POST` of one versioned command
8
+ // with its own idempotency key, and every read is decoded at the seam before a
9
+ // component sees it: "The hosted client renders backend state and submits
10
+ // commands. It does not become an alternate authority."
11
+ import type { ClientPlugin } from "@frockbot/client-core";
12
+ import type { TemplateVisibilityV1 } from "@frockbot/template-core";
13
+ import { ref } from "vue";
14
+ import {
15
+ decodeTemplateImportListViewV1,
16
+ decodeTemplateImportRecordV1,
17
+ decodeTemplateShareListViewV1,
18
+ decodeTemplateShareReceiptV1,
19
+ } from "../shared.js";
20
+ import BotTemplateImportSection from "./BotTemplateImportSection.vue";
21
+ import BotTemplateSection from "./BotTemplateSection.vue";
22
+ import { botTemplateStateKey, type BotTemplateClientState } from "./state.js";
23
+
24
+ const SHARES_PATH = "/api/bot-templates";
25
+ const IMPORTS_PATH = "/api/bot-template-imports";
26
+
27
+ function message(error: unknown, fallback: string): string {
28
+ return error instanceof Error ? error.message : fallback;
29
+ }
30
+
31
+ export const botTemplateClientPlugin: ClientPlugin = (ctx) => {
32
+ const call = (
33
+ path: string,
34
+ method?: "GET" | "POST",
35
+ body?: string,
36
+ ): Promise<unknown> => {
37
+ if (!ctx.transport.hostedRequest) {
38
+ throw new Error("Bot templates are unavailable on this client");
39
+ }
40
+ return ctx.transport.hostedRequest(path, method, body);
41
+ };
42
+ const request = (method?: "GET" | "POST", body?: string): Promise<unknown> =>
43
+ call(SHARES_PATH, method, body);
44
+
45
+ const state = ref<BotTemplateClientState>({
46
+ shares: [],
47
+ imports: [],
48
+ loaded: false,
49
+ busy: false,
50
+ async load() {
51
+ try {
52
+ state.value.shares = decodeTemplateShareListViewV1(
53
+ await request(),
54
+ ).shares;
55
+ state.value.loaded = true;
56
+ state.value.error = undefined;
57
+ } catch (error) {
58
+ state.value.error = message(error, "Could not load template shares");
59
+ }
60
+ },
61
+ async stage(botId: string) {
62
+ state.value.busy = true;
63
+ try {
64
+ const receipt = decodeTemplateShareReceiptV1(
65
+ await request(
66
+ "POST",
67
+ JSON.stringify({
68
+ schemaVersion: 1,
69
+ type: "template/stage",
70
+ commandId: crypto.randomUUID(),
71
+ botId,
72
+ }),
73
+ ),
74
+ );
75
+ state.value.summary = receipt.summary;
76
+ state.value.openShareId = receipt.share.shareId;
77
+ await state.value.load();
78
+ } catch (error) {
79
+ state.value.error = message(error, "Could not stage the template");
80
+ } finally {
81
+ state.value.busy = false;
82
+ }
83
+ },
84
+ async setVisibility(shareId: string, visibility: TemplateVisibilityV1) {
85
+ state.value.busy = true;
86
+ try {
87
+ decodeTemplateShareReceiptV1(
88
+ await request(
89
+ "POST",
90
+ JSON.stringify({
91
+ schemaVersion: 1,
92
+ type: "template/set-visibility",
93
+ commandId: crypto.randomUUID(),
94
+ shareId,
95
+ visibility,
96
+ }),
97
+ ),
98
+ );
99
+ await state.value.load();
100
+ } catch (error) {
101
+ state.value.error = message(error, "Could not change the visibility");
102
+ } finally {
103
+ state.value.busy = false;
104
+ }
105
+ },
106
+ async revoke(shareId: string) {
107
+ state.value.busy = true;
108
+ try {
109
+ decodeTemplateShareReceiptV1(
110
+ await request(
111
+ "POST",
112
+ JSON.stringify({
113
+ schemaVersion: 1,
114
+ type: "template/revoke",
115
+ commandId: crypto.randomUUID(),
116
+ shareId,
117
+ }),
118
+ ),
119
+ );
120
+ await state.value.load();
121
+ } catch (error) {
122
+ state.value.error = message(error, "Could not revoke the share");
123
+ } finally {
124
+ state.value.busy = false;
125
+ }
126
+ },
127
+ async loadImports() {
128
+ try {
129
+ state.value.imports = decodeTemplateImportListViewV1(
130
+ await call(IMPORTS_PATH),
131
+ ).imports;
132
+ state.value.importError = undefined;
133
+ } catch (error) {
134
+ state.value.importError = message(error, "Could not load imports");
135
+ }
136
+ },
137
+ async planImport(shareId: string) {
138
+ state.value.busy = true;
139
+ try {
140
+ // Planning is a read. It opens the review card and applies nothing:
141
+ // only `applyImport` below writes, and only the User calls it.
142
+ const record = decodeTemplateImportRecordV1(
143
+ await call(
144
+ IMPORTS_PATH,
145
+ "POST",
146
+ JSON.stringify({
147
+ schemaVersion: 1,
148
+ type: "template/plan-import",
149
+ commandId: crypto.randomUUID(),
150
+ shareId,
151
+ }),
152
+ ),
153
+ );
154
+ state.value.reviewing = record;
155
+ state.value.importError = undefined;
156
+ await state.value.loadImports();
157
+ } catch (error) {
158
+ state.value.importError = message(
159
+ error,
160
+ "Could not read that template",
161
+ );
162
+ } finally {
163
+ state.value.busy = false;
164
+ }
165
+ },
166
+ async applyImport(importId: string) {
167
+ state.value.busy = true;
168
+ try {
169
+ const record = decodeTemplateImportRecordV1(
170
+ await call(
171
+ IMPORTS_PATH,
172
+ "POST",
173
+ JSON.stringify({
174
+ schemaVersion: 1,
175
+ type: "template/apply-import",
176
+ commandId: crypto.randomUUID(),
177
+ importId,
178
+ }),
179
+ ),
180
+ );
181
+ state.value.reviewing = record;
182
+ state.value.importError = undefined;
183
+ await state.value.loadImports();
184
+ } catch (error) {
185
+ state.value.importError = message(error, "Could not import the Bot");
186
+ } finally {
187
+ state.value.busy = false;
188
+ }
189
+ },
190
+ dismissReview() {
191
+ state.value.reviewing = undefined;
192
+ },
193
+ });
194
+
195
+ return [
196
+ ctx.provide(botTemplateStateKey, state),
197
+ ctx.slot({
198
+ slot: "frockbot.bot-settings-primary-sections",
199
+ order: 20,
200
+ component: BotTemplateSection,
201
+ }),
202
+ ctx.slot({
203
+ slot: "frockbot.bot-settings-sections",
204
+ order: 21,
205
+ component: BotTemplateImportSection,
206
+ }),
207
+ ];
208
+ };
209
+
210
+ export default botTemplateClientPlugin;
@@ -0,0 +1,43 @@
1
+ import type { InjectionKey, Ref } from "vue";
2
+ import type {
3
+ TemplateShareRecordV1,
4
+ TemplateVisibilityV1,
5
+ } from "@frockbot/template-core";
6
+ import type {
7
+ TemplateExportSummaryV1,
8
+ TemplateImportRecordV1,
9
+ } from "../shared.js";
10
+
11
+ export interface BotTemplateClientState {
12
+ /** Every share this User holds; the section filters to the active Bot. */
13
+ shares: TemplateShareRecordV1[];
14
+ /** The summary of the most recent export, for the confirmation dialog. */
15
+ summary?: TemplateExportSummaryV1;
16
+ /** The share the dialog is open on, if any. */
17
+ openShareId?: string;
18
+ loaded: boolean;
19
+ busy: boolean;
20
+ error?: string;
21
+ load(): Promise<void>;
22
+ stage(botId: string): Promise<void>;
23
+ setVisibility(
24
+ shareId: string,
25
+ visibility: TemplateVisibilityV1,
26
+ ): Promise<void>;
27
+ revoke(shareId: string): Promise<void>;
28
+
29
+ /** Imports this User has planned or applied, newest last. */
30
+ imports: TemplateImportRecordV1[];
31
+ /** The card currently open for review; nothing is applied while it is. */
32
+ reviewing?: TemplateImportRecordV1;
33
+ importError?: string;
34
+ loadImports(): Promise<void>;
35
+ /** Reads the shared template and opens a review card. Applies nothing. */
36
+ planImport(shareId: string): Promise<void>;
37
+ /** The User's confirmation, and the only thing that applies anything. */
38
+ applyImport(importId: string): Promise<void>;
39
+ dismissReview(): void;
40
+ }
41
+
42
+ export const botTemplateStateKey: InjectionKey<Ref<BotTemplateClientState>> =
43
+ Symbol("bot-template-state");
package/src/env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ declare module "*.vue" {
2
+ import type { DefineComponent } from "vue";
3
+
4
+ const component: DefineComponent;
5
+ export default component;
6
+ }