@startsimpli/integrations 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/integrations",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Provider-agnostic types, API clients, hooks, and UI primitives for email + calendar integrations across StartSimpli apps.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* theme classNames.
|
|
16
16
|
*/
|
|
17
17
|
import { describe, expect, it, vi } from 'vitest';
|
|
18
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
18
|
+
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
19
19
|
import userEvent from '@testing-library/user-event';
|
|
20
20
|
|
|
21
21
|
import { CredentialSchemaForm, schemaFields } from '../components/CredentialSchemaForm';
|
|
@@ -203,6 +203,7 @@ function stubApi(overrides: Partial<ConnectionsApi> = {}): ConnectionsApi {
|
|
|
203
203
|
run_status: 'running', external_ref: null, output: {}, error: '',
|
|
204
204
|
}),
|
|
205
205
|
connectionRuns: vi.fn().mockResolvedValue({ results: [] }),
|
|
206
|
+
updateConnection: vi.fn().mockResolvedValue(CONNECTED.results[0]),
|
|
206
207
|
...overrides,
|
|
207
208
|
} as ConnectionsApi;
|
|
208
209
|
}
|
|
@@ -268,6 +269,120 @@ describe('ConnectionsPage — injected, not contextual', () => {
|
|
|
268
269
|
});
|
|
269
270
|
});
|
|
270
271
|
|
|
272
|
+
// --------------------------------------------------------------------------
|
|
273
|
+
// the capability choice (bd startsim-768w.22.22)
|
|
274
|
+
// --------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* WHAT WENT WRONG ON PRODUCTION. An admin connected Granola on the `tandem`
|
|
278
|
+
* foundry through this very page. The card went green — `check()` really
|
|
279
|
+
* reached the vendor — and `capabilities_enabled` was stored as `[]`. With an
|
|
280
|
+
* empty list NO capability is enabled: not `poll`, not `action`, not
|
|
281
|
+
* `webhook_in`. So `poll_is_enabled()` answered false, no scheduled ingest leg
|
|
282
|
+
* was ever created, and the card sat there connected and completely inert.
|
|
283
|
+
*
|
|
284
|
+
* The server was not the liar. `POST /connections/` has accepted
|
|
285
|
+
* `capabilities_enabled[]` since the endpoint was designed, and
|
|
286
|
+
* `PATCH /connections/<id>/` has accepted edits to it and created the poll leg
|
|
287
|
+
* on the spot. THIS PAGE never sent either one — there was no control for it
|
|
288
|
+
* anywhere, at connect time or after — so the server received nothing, wrote
|
|
289
|
+
* nothing, and the admin had no way to say what they wanted.
|
|
290
|
+
*
|
|
291
|
+
* Both halves are asserted here: the choice is offered when connecting, and it
|
|
292
|
+
* stays changeable afterwards, because the connections that already exist
|
|
293
|
+
* cannot be fixed by anything the connect form does.
|
|
294
|
+
*/
|
|
295
|
+
describe('capabilities are the admin’s choice, and the page has to ask', () => {
|
|
296
|
+
/** The capability picker inside ONE card. Both surfaces render one. */
|
|
297
|
+
function picker(scope: HTMLElement, which: 'catalog' | 'connection') {
|
|
298
|
+
const group = scope.querySelector<HTMLElement>(`[data-capabilities="${which}"]`);
|
|
299
|
+
if (!group) throw new Error(`no ${which} capability picker rendered`);
|
|
300
|
+
return within(group);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function openConnectForm() {
|
|
304
|
+
const api = stubApi();
|
|
305
|
+
const { container } = render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
306
|
+
// The catalog's available row. The unavailable one offers no button at all.
|
|
307
|
+
await userEvent.click(await screen.findByRole('button', { name: /^connect$/i }));
|
|
308
|
+
return { api, container };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
it('offers every capability the connector declares, from the catalog row', async () => {
|
|
312
|
+
const { container } = await openConnectForm();
|
|
313
|
+
// The n8n fixture declares action + poll. Both are offered; nothing else is.
|
|
314
|
+
const boxes = picker(container, 'catalog').getAllByRole('checkbox');
|
|
315
|
+
expect(boxes.map((b) => b.getAttribute('value')).sort()).toEqual(['action', 'poll']);
|
|
316
|
+
for (const box of boxes) expect(box).toBeChecked();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('SENDS the capability list on connect instead of leaving the card inert', async () => {
|
|
320
|
+
const { api } = await openConnectForm();
|
|
321
|
+
await userEvent.type(screen.getByLabelText(/Instance URL/i), 'https://n8n.example.com');
|
|
322
|
+
await userEvent.click(screen.getByRole('button', { name: /test and save/i }));
|
|
323
|
+
|
|
324
|
+
await waitFor(() => expect(api.connect).toHaveBeenCalled());
|
|
325
|
+
const sent = (api.connect as ReturnType<typeof vi.fn>).mock.calls[0][1];
|
|
326
|
+
expect(sent.capabilities_enabled).toEqual(['action', 'poll']);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('honours an admin who unchecks one before saving', async () => {
|
|
330
|
+
const { api, container } = await openConnectForm();
|
|
331
|
+
await userEvent.type(screen.getByLabelText(/Instance URL/i), 'https://n8n.example.com');
|
|
332
|
+
await userEvent.click(picker(container, 'catalog').getByRole('checkbox', { name: /action/i }));
|
|
333
|
+
await userEvent.click(screen.getByRole('button', { name: /test and save/i }));
|
|
334
|
+
|
|
335
|
+
await waitFor(() => expect(api.connect).toHaveBeenCalled());
|
|
336
|
+
const sent = (api.connect as ReturnType<typeof vi.fn>).mock.calls[0][1];
|
|
337
|
+
expect(sent.capabilities_enabled).toEqual(['poll']);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('will not let an admin save a connection that can do nothing', async () => {
|
|
341
|
+
const { container } = await openConnectForm();
|
|
342
|
+
await userEvent.type(screen.getByLabelText(/Instance URL/i), 'https://n8n.example.com');
|
|
343
|
+
for (const box of picker(container, 'catalog').getAllByRole('checkbox')) {
|
|
344
|
+
await userEvent.click(box);
|
|
345
|
+
}
|
|
346
|
+
// The server refuses an empty list with a 400 (bd startsim-768w.22.22);
|
|
347
|
+
// refusing it here means the admin finds out before a round trip.
|
|
348
|
+
expect(screen.getByRole('button', { name: /test and save/i })).toBeDisabled();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('lets an admin turn a capability ON for a connection that already exists', async () => {
|
|
352
|
+
// THE UNBLOCK. The `tandem` Granola card is already there with an empty
|
|
353
|
+
// list; nothing the connect form does can reach it. `Ops n8n` here has only
|
|
354
|
+
// `action` enabled, and the connector declares `poll` too.
|
|
355
|
+
const api = stubApi();
|
|
356
|
+
const { container } = render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
357
|
+
await screen.findByText('Ops n8n');
|
|
358
|
+
|
|
359
|
+
const on = picker(container, 'connection');
|
|
360
|
+
const poll = on.getByRole('checkbox', { name: /poll/i });
|
|
361
|
+
expect(poll).not.toBeChecked();
|
|
362
|
+
await userEvent.click(poll);
|
|
363
|
+
await userEvent.click(on.getByRole('button', { name: /save/i }));
|
|
364
|
+
|
|
365
|
+
await waitFor(() => expect(api.updateConnection).toHaveBeenCalledWith('acme', 'c1', {
|
|
366
|
+
capabilities_enabled: ['action', 'poll'],
|
|
367
|
+
}));
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('shows the capability state read-only when manage is false', async () => {
|
|
371
|
+
const api = stubApi({
|
|
372
|
+
connections: vi.fn().mockResolvedValue({
|
|
373
|
+
...CONNECTED,
|
|
374
|
+
permissions: { view: true, manage: false, issue_tokens: false },
|
|
375
|
+
}),
|
|
376
|
+
});
|
|
377
|
+
const { container } = render(<ConnectionsPage api={api} foundrySlug="acme" />);
|
|
378
|
+
await screen.findByText('Ops n8n');
|
|
379
|
+
|
|
380
|
+
const on = picker(container, 'connection');
|
|
381
|
+
expect(on.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
|
|
382
|
+
for (const box of on.getAllByRole('checkbox')) expect(box).toBeDisabled();
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
271
386
|
describe('the catalog envelope, as the server actually ships it', () => {
|
|
272
387
|
// REGRESSION. The first browser drive of this page died on
|
|
273
388
|
// `(entry.credential_schema ?? []) is not iterable`, because these fixtures
|
|
@@ -60,6 +60,21 @@ const DEFAULTS = {
|
|
|
60
60
|
error: 'text-sm text-red-600',
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* WHAT AN ADMIN IS AGREEING TO, in their words rather than the wire's.
|
|
65
|
+
*
|
|
66
|
+
* Data, not a branch: an unknown key falls through to the key itself, because
|
|
67
|
+
* the capability vocabulary lives in the connector package and will outrun this
|
|
68
|
+
* bundle exactly the way the credential schema does. A capability this build has
|
|
69
|
+
* never heard of must render as a choice, not vanish from the list.
|
|
70
|
+
*/
|
|
71
|
+
const CAPABILITY_COPY: Record<string, string> = {
|
|
72
|
+
poll: 'Fetch new records on a schedule',
|
|
73
|
+
webhook_in: 'Receive events pushed to us',
|
|
74
|
+
action: 'Let us run actions in this account',
|
|
75
|
+
writeback_credential: 'Issue a token this vendor uses to call us',
|
|
76
|
+
};
|
|
77
|
+
|
|
63
78
|
/** A refusal must read as a refusal, not as an empty page. */
|
|
64
79
|
function readFailureMessage(err: unknown): string {
|
|
65
80
|
const status = (err as { status?: number })?.status;
|
|
@@ -120,6 +135,12 @@ export function ConnectionsPage({ api, foundrySlug, classNames = {} }: Connectio
|
|
|
120
135
|
api={api}
|
|
121
136
|
foundrySlug={foundrySlug}
|
|
122
137
|
connection={connection}
|
|
138
|
+
// What the CONNECTOR declares, so the card can offer a capability
|
|
139
|
+
// this connection does not have yet — which is the only way an
|
|
140
|
+
// account connected before the picker existed ever gets one.
|
|
141
|
+
declared={
|
|
142
|
+
catalog.find((entry) => entry.key === connection.connector_key)?.capabilities
|
|
143
|
+
}
|
|
123
144
|
canManage={permissions.manage}
|
|
124
145
|
classNames={cn}
|
|
125
146
|
onChanged={reload}
|
|
@@ -145,6 +166,75 @@ export function ConnectionsPage({ api, foundrySlug, classNames = {} }: Connectio
|
|
|
145
166
|
);
|
|
146
167
|
}
|
|
147
168
|
|
|
169
|
+
// --------------------------------------------------------------------------
|
|
170
|
+
// the capability choice (bd startsim-768w.22.22)
|
|
171
|
+
// --------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* WHICH VERBS THIS ACCOUNT IS ALLOWED — the control this page did not have.
|
|
175
|
+
*
|
|
176
|
+
* `POST /connections/` and `PATCH /connections/<id>/` have both accepted
|
|
177
|
+
* `capabilities_enabled[]` since the endpoints were designed. Nothing here ever
|
|
178
|
+
* sent one, so every connection made through the product was stored with an
|
|
179
|
+
* EMPTY list, and an empty list enables nothing: the card goes green on a real
|
|
180
|
+
* `check()` and can then do none of the things it advertises. That is how a
|
|
181
|
+
* connected Granola account on a live foundry ingested nothing forever.
|
|
182
|
+
*
|
|
183
|
+
* Rendered from `entry.capabilities` — the connector's own declaration, off the
|
|
184
|
+
* catalog row — so a new vendor's verbs appear here with no code change, and a
|
|
185
|
+
* verb this build has never heard of still renders.
|
|
186
|
+
*/
|
|
187
|
+
function CapabilityPicker({
|
|
188
|
+
scope, declared, selected, disabled, idPrefix, onToggle, classNames: cn, children,
|
|
189
|
+
}: {
|
|
190
|
+
scope: 'catalog' | 'connection';
|
|
191
|
+
declared: string[];
|
|
192
|
+
selected: string[];
|
|
193
|
+
disabled: boolean;
|
|
194
|
+
idPrefix: string;
|
|
195
|
+
onToggle: (capability: string, on: boolean) => void;
|
|
196
|
+
classNames: typeof DEFAULTS;
|
|
197
|
+
/** Whatever COMMITS the choice — inline, so the control and the thing that
|
|
198
|
+
saves it are one region for a screen reader and for a test. */
|
|
199
|
+
children?: React.ReactNode;
|
|
200
|
+
}) {
|
|
201
|
+
if (declared.length === 0) return null;
|
|
202
|
+
return (
|
|
203
|
+
<fieldset data-capabilities={scope} className="mt-3 space-y-1 border-0 p-0">
|
|
204
|
+
<legend className="text-sm font-medium text-gray-900">What this account may do</legend>
|
|
205
|
+
{declared.map((capability) => {
|
|
206
|
+
const id = `${idPrefix}-cap-${capability}`;
|
|
207
|
+
return (
|
|
208
|
+
<label key={capability} htmlFor={id} className="flex items-start gap-2 text-sm">
|
|
209
|
+
<input
|
|
210
|
+
id={id}
|
|
211
|
+
type="checkbox"
|
|
212
|
+
value={capability}
|
|
213
|
+
className="mt-0.5"
|
|
214
|
+
checked={selected.includes(capability)}
|
|
215
|
+
disabled={disabled}
|
|
216
|
+
onChange={(event) => onToggle(capability, event.target.checked)}
|
|
217
|
+
/>
|
|
218
|
+
<span>
|
|
219
|
+
<span className="text-gray-900">{capability}</span>
|
|
220
|
+
<span className={`ml-2 ${cn.muted}`}>
|
|
221
|
+
{CAPABILITY_COPY[capability] ?? ''}
|
|
222
|
+
</span>
|
|
223
|
+
</span>
|
|
224
|
+
</label>
|
|
225
|
+
);
|
|
226
|
+
})}
|
|
227
|
+
{children}
|
|
228
|
+
</fieldset>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Declaration order is the render order, and the wire order. */
|
|
233
|
+
function orderCapabilities(declared: string[], selected: Iterable<string>): string[] {
|
|
234
|
+
const set = new Set(selected);
|
|
235
|
+
return declared.filter((capability) => set.has(capability));
|
|
236
|
+
}
|
|
237
|
+
|
|
148
238
|
// --------------------------------------------------------------------------
|
|
149
239
|
// catalog
|
|
150
240
|
// --------------------------------------------------------------------------
|
|
@@ -163,6 +253,13 @@ function CatalogCard({
|
|
|
163
253
|
const [values, setValues] = React.useState<Record<string, string>>({});
|
|
164
254
|
const [busy, setBusy] = React.useState(false);
|
|
165
255
|
const [error, setError] = React.useState('');
|
|
256
|
+
// Every declared capability starts ON. Not because "on" is a safe default in
|
|
257
|
+
// general, but because the alternative here is a connection that can do
|
|
258
|
+
// nothing — and the list is right there, unchecked with one click, before
|
|
259
|
+
// anything is saved. The privacy control that is genuinely opt-in lives one
|
|
260
|
+
// level down (a connector's own config, e.g. whether a poll fetches meeting
|
|
261
|
+
// transcript bytes at all) and is unaffected by this box.
|
|
262
|
+
const [capabilities, setCapabilities] = React.useState<string[]>(entry.capabilities);
|
|
166
263
|
|
|
167
264
|
const credentialFields = schemaFields(entry.credential_schema);
|
|
168
265
|
const configFields = schemaFields(entry.config_schema);
|
|
@@ -186,9 +283,12 @@ function CatalogCard({
|
|
|
186
283
|
}
|
|
187
284
|
await api.connect(foundrySlug, {
|
|
188
285
|
connector_key: entry.key, name: entry.name, credentials, config,
|
|
286
|
+
// Always sent. Leaving it off is what wrote the inert cards.
|
|
287
|
+
capabilities_enabled: orderCapabilities(entry.capabilities, capabilities),
|
|
189
288
|
});
|
|
190
289
|
setOpen(false);
|
|
191
290
|
setValues({});
|
|
291
|
+
setCapabilities(entry.capabilities);
|
|
192
292
|
onConnected();
|
|
193
293
|
} catch (err) {
|
|
194
294
|
// The server's typed answer, verbatim. It already scrubbed itself of the
|
|
@@ -237,11 +337,34 @@ function CatalogCard({
|
|
|
237
337
|
}));
|
|
238
338
|
}}
|
|
239
339
|
/>
|
|
340
|
+
<CapabilityPicker
|
|
341
|
+
scope="catalog"
|
|
342
|
+
declared={entry.capabilities}
|
|
343
|
+
selected={capabilities}
|
|
344
|
+
disabled={busy}
|
|
345
|
+
idPrefix={`connect-${entry.key}`}
|
|
346
|
+
onToggle={(capability, on) => setCapabilities((prev) => (
|
|
347
|
+
on ? [...prev, capability] : prev.filter((c) => c !== capability)
|
|
348
|
+
))}
|
|
349
|
+
classNames={cn}
|
|
350
|
+
/>
|
|
351
|
+
{/* The server refuses an empty list with a 400 rather than storing a
|
|
352
|
+
card that can do nothing. Saying so here costs the admin a round
|
|
353
|
+
trip instead of a confusing error. */}
|
|
354
|
+
{entry.capabilities.length > 0 && capabilities.length === 0 ? (
|
|
355
|
+
<p className={cn.error}>
|
|
356
|
+
Enable at least one — a connection with nothing enabled can do nothing.
|
|
357
|
+
</p>
|
|
358
|
+
) : null}
|
|
240
359
|
{error ? <p className={cn.error} role="alert">{error}</p> : null}
|
|
241
360
|
<button
|
|
242
361
|
type="button"
|
|
243
362
|
className={cn.button}
|
|
244
|
-
disabled={
|
|
363
|
+
disabled={
|
|
364
|
+
busy
|
|
365
|
+
|| missing.length > 0
|
|
366
|
+
|| (entry.capabilities.length > 0 && capabilities.length === 0)
|
|
367
|
+
}
|
|
245
368
|
onClick={() => void submit()}
|
|
246
369
|
>
|
|
247
370
|
{busy ? 'Testing…' : 'Test and save'}
|
|
@@ -257,11 +380,12 @@ function CatalogCard({
|
|
|
257
380
|
// --------------------------------------------------------------------------
|
|
258
381
|
|
|
259
382
|
function ConnectionCard({
|
|
260
|
-
api, foundrySlug, connection, canManage, classNames: cn, onChanged,
|
|
383
|
+
api, foundrySlug, connection, declared, canManage, classNames: cn, onChanged,
|
|
261
384
|
}: {
|
|
262
385
|
api: ConnectionsApi;
|
|
263
386
|
foundrySlug: string;
|
|
264
387
|
connection: Connection;
|
|
388
|
+
declared?: string[];
|
|
265
389
|
canManage: boolean;
|
|
266
390
|
classNames: typeof DEFAULTS;
|
|
267
391
|
onChanged: () => void;
|
|
@@ -273,6 +397,26 @@ function ConnectionCard({
|
|
|
273
397
|
const [error, setError] = React.useState('');
|
|
274
398
|
const [notice, setNotice] = React.useState('');
|
|
275
399
|
|
|
400
|
+
const enabled = React.useMemo(
|
|
401
|
+
() => connection.capabilities_enabled ?? [], [connection.capabilities_enabled]);
|
|
402
|
+
// The catalog's declaration first, then anything this connection holds that
|
|
403
|
+
// the catalog no longer offers. A capability an admin cannot SEE is one they
|
|
404
|
+
// cannot turn off, and a catalog row that will not load must not blank the
|
|
405
|
+
// card's own state.
|
|
406
|
+
const offered = React.useMemo(() => {
|
|
407
|
+
const seen = new Set<string>();
|
|
408
|
+
return [...(declared ?? []), ...enabled].filter((capability) => {
|
|
409
|
+
if (seen.has(capability)) return false;
|
|
410
|
+
seen.add(capability);
|
|
411
|
+
return true;
|
|
412
|
+
});
|
|
413
|
+
}, [declared, enabled]);
|
|
414
|
+
const [selected, setSelected] = React.useState<string[]>(enabled);
|
|
415
|
+
React.useEffect(() => { setSelected(enabled); }, [enabled]);
|
|
416
|
+
|
|
417
|
+
const pending = orderCapabilities(offered, selected);
|
|
418
|
+
const dirty = pending.join(',') !== orderCapabilities(offered, enabled).join(',');
|
|
419
|
+
|
|
276
420
|
async function guard(label: string, run: () => Promise<void>) {
|
|
277
421
|
setBusy(label);
|
|
278
422
|
setError('');
|
|
@@ -330,6 +474,46 @@ function ConnectionCard({
|
|
|
330
474
|
) : null}
|
|
331
475
|
</div>
|
|
332
476
|
|
|
477
|
+
{/* THE FIX FOR THE CARDS THAT ALREADY EXIST. A connection written before
|
|
478
|
+
the connect form asked holds an empty list and can do nothing; the
|
|
479
|
+
connect form can never reach it. Turning `poll` on here is what makes
|
|
480
|
+
the server create the scheduled ingest leg. */}
|
|
481
|
+
<CapabilityPicker
|
|
482
|
+
scope="connection"
|
|
483
|
+
declared={offered}
|
|
484
|
+
selected={selected}
|
|
485
|
+
disabled={!canManage || Boolean(busy)}
|
|
486
|
+
idPrefix={`connection-${connection.id}`}
|
|
487
|
+
onToggle={(capability, on) => setSelected((prev) => (
|
|
488
|
+
on ? [...prev, capability] : prev.filter((c) => c !== capability)
|
|
489
|
+
))}
|
|
490
|
+
classNames={cn}
|
|
491
|
+
>
|
|
492
|
+
{canManage ? (
|
|
493
|
+
<div className="mt-2 flex items-center gap-3">
|
|
494
|
+
<button
|
|
495
|
+
type="button"
|
|
496
|
+
className={cn.button}
|
|
497
|
+
disabled={!dirty || pending.length === 0 || Boolean(busy)}
|
|
498
|
+
onClick={() => void guard('capabilities', async () => {
|
|
499
|
+
await api.updateConnection(foundrySlug, connection.id, {
|
|
500
|
+
capabilities_enabled: pending,
|
|
501
|
+
});
|
|
502
|
+
setNotice('Saved.');
|
|
503
|
+
onChanged();
|
|
504
|
+
})}
|
|
505
|
+
>
|
|
506
|
+
{busy === 'capabilities' ? 'Saving…' : 'Save'}
|
|
507
|
+
</button>
|
|
508
|
+
{pending.length === 0 ? (
|
|
509
|
+
<span className={cn.error}>
|
|
510
|
+
Enable at least one, or disconnect the account.
|
|
511
|
+
</span>
|
|
512
|
+
) : null}
|
|
513
|
+
</div>
|
|
514
|
+
) : null}
|
|
515
|
+
</CapabilityPicker>
|
|
516
|
+
|
|
333
517
|
{notice ? <p className={cn.muted}>{notice}</p> : null}
|
|
334
518
|
{error ? <p className={cn.error} role="alert">{error}</p> : null}
|
|
335
519
|
|
|
@@ -156,6 +156,27 @@ export interface ConnectionsApi {
|
|
|
156
156
|
name?: string;
|
|
157
157
|
credentials?: Record<string, unknown>;
|
|
158
158
|
config?: Record<string, unknown>;
|
|
159
|
+
/**
|
|
160
|
+
* WHICH VERBS THIS ACCOUNT IS ALLOWED. Omitting it used to leave the server
|
|
161
|
+
* to write `[]`, which enables nothing at all — a green card that cannot
|
|
162
|
+
* poll, cannot be invoked and cannot receive a webhook (bd
|
|
163
|
+
* startsim-768w.22.22). The composer always sends it now, and the server
|
|
164
|
+
* refuses an empty list rather than storing the inert card.
|
|
165
|
+
*/
|
|
166
|
+
capabilities_enabled?: string[];
|
|
167
|
+
}): Promise<Connection>;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Edit a connection in place. The only field this page changes today is
|
|
171
|
+
* `capabilities_enabled`, and it must be changeable AFTER connect: every
|
|
172
|
+
* connection that already exists was written before the connect form asked,
|
|
173
|
+
* so nothing the connect form does can reach them. Turning `poll` on here is
|
|
174
|
+
* what creates the scheduled ingest leg, server-side, in the same request.
|
|
175
|
+
*/
|
|
176
|
+
updateConnection(slug: string, id: string, input: {
|
|
177
|
+
name?: string;
|
|
178
|
+
capabilities_enabled?: string[];
|
|
179
|
+
config?: Record<string, unknown>;
|
|
159
180
|
}): Promise<Connection>;
|
|
160
181
|
checkConnection(slug: string, id: string): Promise<{
|
|
161
182
|
ok: boolean; account_label: string; detail: string;
|