@frockbot/plugin-settings 0.3.6 → 0.3.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-settings",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,15 +21,15 @@
21
21
  "typecheck": "vue-tsc --noEmit -p tsconfig.json"
22
22
  },
23
23
  "dependencies": {
24
- "@frockbot/catalog-core": "0.3.6",
25
- "@frockbot/client-core": "0.3.6",
26
- "@frockbot/client-ui": "0.3.6",
27
- "@frockbot/configuration-core": "0.3.6",
28
- "@frockbot/connection-core": "0.3.6",
29
- "@frockbot/kernel-composition": "0.3.6",
30
- "@frockbot/kernel-contracts": "0.3.6",
31
- "@frockbot/plugin-auth": "0.3.6",
32
- "@frockbot/plugin-shell": "0.3.6",
24
+ "@frockbot/catalog-core": "0.3.7",
25
+ "@frockbot/client-core": "0.3.7",
26
+ "@frockbot/client-ui": "0.3.7",
27
+ "@frockbot/configuration-core": "0.3.7",
28
+ "@frockbot/connection-core": "0.3.7",
29
+ "@frockbot/kernel-composition": "0.3.7",
30
+ "@frockbot/kernel-contracts": "0.3.7",
31
+ "@frockbot/plugin-auth": "0.3.7",
32
+ "@frockbot/plugin-shell": "0.3.7",
33
33
  "cordis": "4.0.0-rc.8",
34
34
  "vue": "3.5.41"
35
35
  },
@@ -28,6 +28,38 @@ const web = providedWeb;
28
28
  // Connections are User-scoped, so the row's link names no Bot.
29
29
  const connectionsLink = settingsLinkV1({ anchor: "user-connections" });
30
30
 
31
+ /**
32
+ * What the browser came back from an external grant with. Read at boot from the
33
+ * return URL; without this the User lands back in the app with no confirmation
34
+ * and a cancelled grant is silently discarded.
35
+ */
36
+ const connectionReturn = computed(() => web.value.connectionReturn);
37
+
38
+ const connectionReturnMessage = computed(() => {
39
+ const result = connectionReturn.value;
40
+ if (!result) return "";
41
+ const name = connectorDisplayName(result.packageId);
42
+ if (result.status === "ready") return `${name} is connected.`;
43
+ if (result.status === "pending") {
44
+ return `${name} is finishing connecting. This page will show it when it is ready.`;
45
+ }
46
+ return result.reason
47
+ ? `${name} did not connect: ${result.reason}`
48
+ : `${name} did not connect.`;
49
+ });
50
+
51
+ /** The Package's display name when the catalog knows it, its id otherwise. */
52
+ function connectorDisplayName(packageId: string): string {
53
+ return (
54
+ web.value.pluginCatalog.find((item) => item.packageId === packageId)
55
+ ?.displayName ?? packageId
56
+ );
57
+ }
58
+
59
+ function dismissConnectionReturn(): void {
60
+ web.value.connectionReturn = undefined;
61
+ }
62
+
31
63
  const connectors = computed(() =>
32
64
  configurablePackages({
33
65
  catalog: web.value.pluginCatalog,
@@ -331,6 +363,18 @@ async function connect(
331
363
  Connect an account or service once and every Bot you own can use it.
332
364
  </p>
333
365
 
366
+ <p
367
+ v-if="connectionReturn"
368
+ class="connection-return"
369
+ :class="{
370
+ 'connection-return--failed': connectionReturn.status === 'failed',
371
+ }"
372
+ role="status"
373
+ >
374
+ {{ connectionReturnMessage }}
375
+ <UiButton @click="dismissConnectionReturn">Dismiss</UiButton>
376
+ </p>
377
+
334
378
  <section
335
379
  v-if="pendingAuthorizations.length"
336
380
  class="connect-cards"
@@ -722,6 +766,21 @@ async function connect(
722
766
  font-size: var(--frock-text-sm);
723
767
  }
724
768
 
769
+ .connection-return {
770
+ display: flex;
771
+ align-items: center;
772
+ justify-content: space-between;
773
+ gap: 12px;
774
+ margin: 8px 0;
775
+ padding: 8px 12px;
776
+ border-radius: var(--frock-radius-card);
777
+ background: var(--frock-surface-accent);
778
+ }
779
+
780
+ .connection-return--failed {
781
+ background: var(--frock-surface-danger, var(--frock-surface-accent));
782
+ }
783
+
725
784
  .connections-empty {
726
785
  display: flex;
727
786
  flex-direction: column;
@@ -78,7 +78,10 @@ function capabilityNoun(kind: string): string {
78
78
  /** What a plugin offers, in plain words. */
79
79
  function capabilitySummary(item: PluginCatalogItem): string {
80
80
  const kinds = [...new Set(item.capabilities.map((entry) => entry.kind))];
81
- if (kinds.length === 0) return "No features yet";
81
+ // A plugin can be worth turning on without contributing a capability of its
82
+ // own — Custom models is exactly that — so saying "no features" tells the
83
+ // User the one plugin they must enable does nothing.
84
+ if (kinds.length === 0) return "Adds settings";
82
85
  return kinds.map(capabilityNoun).join(", ");
83
86
  }
84
87
 
@@ -126,13 +129,71 @@ function openPackageCatalog(): void {
126
129
  surfaces.open("package-catalog");
127
130
  }
128
131
 
132
+ /**
133
+ * Refusals live on the row that was clicked, not in one panel-wide slot.
134
+ *
135
+ * The grid scrolls, so a message rendered above the list is off-screen for
136
+ * every row past the fold: the User sees the row unchanged and no explanation
137
+ * anywhere.
138
+ */
139
+ const rowErrors = ref<Record<string, string>>({});
140
+
141
+ function rowError(packageId: string): string | undefined {
142
+ return rowErrors.value[packageId];
143
+ }
144
+
145
+ function setRowError(packageId: string, message: string): void {
146
+ rowErrors.value = { ...rowErrors.value, [packageId]: message };
147
+ }
148
+
149
+ function clearRowError(packageId: string): void {
150
+ const { [packageId]: _cleared, ...rest } = rowErrors.value;
151
+ rowErrors.value = rest;
152
+ }
153
+
154
+ /**
155
+ * Say what a Package is called, not what its id is.
156
+ *
157
+ * The backend refuses in terms of ids because that is what it holds; the
158
+ * surface knows the display names, so it substitutes them before the User
159
+ * reads the message.
160
+ */
161
+ function inPlainNames(message: string): string {
162
+ const dependency = message.match(
163
+ /^Package "([^"]+)" requires Package "([^"]+)" to be installed and enabled/,
164
+ );
165
+ if (dependency) {
166
+ return `${displayName(dependency[1]!)} needs ${displayName(
167
+ dependency[2]!,
168
+ )} turned on first.`;
169
+ }
170
+ return message.replace(/"([a-z0-9][a-z0-9-]*)"/g, (quoted, packageId) =>
171
+ displayName(packageId) === packageId
172
+ ? quoted
173
+ : `"${displayName(packageId)}"`,
174
+ );
175
+ }
176
+
177
+ function displayName(packageId: string): string {
178
+ return (
179
+ web.value.pluginCatalog.find(
180
+ (candidate) => candidate.packageId === packageId,
181
+ )?.displayName ?? packageId
182
+ );
183
+ }
184
+
129
185
  async function install(item: PluginCatalogItem): Promise<void> {
130
186
  busyPackageId.value = item.packageId;
187
+ clearRowError(item.packageId);
131
188
  try {
132
189
  await web.value.installPackage(item.packageId, item.version);
133
190
  } catch (error) {
134
- web.value.settingsError =
135
- error instanceof Error ? error.message : "Couldn't add that plugin.";
191
+ setRowError(
192
+ item.packageId,
193
+ inPlainNames(
194
+ error instanceof Error ? error.message : "Couldn't add that plugin.",
195
+ ),
196
+ );
136
197
  } finally {
137
198
  busyPackageId.value = undefined;
138
199
  }
@@ -140,13 +201,18 @@ async function install(item: PluginCatalogItem): Promise<void> {
140
201
 
141
202
  async function setEnabled(packageId: string, next: boolean): Promise<void> {
142
203
  busyPackageId.value = packageId;
204
+ clearRowError(packageId);
143
205
  try {
144
206
  await web.value.setPackageEnabled(packageId, next);
145
207
  } catch (error) {
146
- web.value.settingsError =
147
- error instanceof Error
148
- ? error.message
149
- : "Couldn't turn that plugin on or off.";
208
+ setRowError(
209
+ packageId,
210
+ inPlainNames(
211
+ error instanceof Error
212
+ ? error.message
213
+ : "Couldn't turn that plugin on or off.",
214
+ ),
215
+ );
150
216
  } finally {
151
217
  busyPackageId.value = undefined;
152
218
  }
@@ -161,14 +227,6 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
161
227
  </span>
162
228
  {{ installedPluginCount }} installed
163
229
  </div>
164
- <!--
165
- A refused command is the answer to the click that caused it, so it is
166
- shown above the list where the click happened, not below a long grid
167
- where it scrolls out of view.
168
- -->
169
- <p v-if="web.settingsError" class="settings-error" role="alert">
170
- {{ web.settingsError }}
171
- </p>
172
230
  <label class="plugin-search">
173
231
  <UiIcon name="search" />
174
232
  <input
@@ -242,6 +300,9 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
242
300
  </UiButton>
243
301
  </span>
244
302
  </div>
303
+ <p v-if="rowError(item.packageId)" class="plugin-failure" role="alert">
304
+ {{ rowError(item.packageId) }}
305
+ </p>
245
306
  <p v-if="failure(item.packageId)" class="plugin-failure" role="alert">
246
307
  {{ failure(item.packageId) }}
247
308
  </p>