@startsimpli/integrations 0.2.24 → 0.2.26
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 +1 -1
- package/src/__tests__/connections.test.tsx +131 -0
- package/src/__tests__/corpus-planning.test.tsx +556 -0
- package/src/api/connections.ts +46 -0
- package/src/components/index.ts +17 -0
- package/src/components/pages/ConnectionsPage.tsx +139 -15
- package/src/components/pages/PlanningPage.tsx +691 -0
- package/src/components/pages/TrackerPage.tsx +22 -134
- package/src/components/pages/types.ts +59 -0
- package/src/components/pages/work-items.tsx +256 -0
- package/src/corpus/client.ts +368 -1
- package/src/corpus/entities.ts +63 -1
- package/src/corpus/index.ts +26 -1
- package/src/corpus/planning.ts +323 -0
- package/src/corpus/types.ts +241 -3
|
@@ -123,6 +123,16 @@ interface BoundApi {
|
|
|
123
123
|
}): Promise<ConnectionInvokeResult>;
|
|
124
124
|
/** Per-member governance (bd startsim-768w.22.25.8). Foundry mode only, and
|
|
125
125
|
absent on an api that predates the endpoints — see `ConnectionsApi`. */
|
|
126
|
+
/** The OAuth hand-off (bd startsim-768w.22.25.14/.23). Optional: an app on an
|
|
127
|
+
older client keeps type-checking, and the card renders the connector as
|
|
128
|
+
not-connectable-here rather than offering a button that cannot fire. */
|
|
129
|
+
startOAuth?(input: {
|
|
130
|
+
connector_key: string;
|
|
131
|
+
name?: string;
|
|
132
|
+
config?: Record<string, unknown>;
|
|
133
|
+
capabilities_enabled?: string[];
|
|
134
|
+
return_to?: string;
|
|
135
|
+
}): Promise<{ authorization_url: string; state: string; redirect_uri: string }>;
|
|
126
136
|
setMemberConnectorPolicy?(connectorKey: string, allowed: boolean): Promise<unknown>;
|
|
127
137
|
memberConnections?(): Promise<{
|
|
128
138
|
results: MemberConnectionRow[]; permissions: ConnectionPermissions;
|
|
@@ -144,6 +154,10 @@ function bindApi(props: ConnectionsPageProps): BoundApi {
|
|
|
144
154
|
? { setMemberConnectorPolicy: (key: string, allowed: boolean) =>
|
|
145
155
|
api.setMemberConnectorPolicy!(slug, key, allowed) }
|
|
146
156
|
: {};
|
|
157
|
+
const oauth = api.startOAuth
|
|
158
|
+
? { startOAuth: (input: Parameters<NonNullable<BoundApi['startOAuth']>>[0]) =>
|
|
159
|
+
api.startOAuth!(slug, input) }
|
|
160
|
+
: {};
|
|
147
161
|
const roster = api.memberConnections
|
|
148
162
|
? { memberConnections: () => api.memberConnections!(slug) }
|
|
149
163
|
: {};
|
|
@@ -153,6 +167,7 @@ function bindApi(props: ConnectionsPageProps): BoundApi {
|
|
|
153
167
|
: {};
|
|
154
168
|
return {
|
|
155
169
|
...memberPolicy,
|
|
170
|
+
...oauth,
|
|
156
171
|
...roster,
|
|
157
172
|
...cutMember,
|
|
158
173
|
connectorCatalog: () => api.connectorCatalog(slug),
|
|
@@ -467,12 +482,76 @@ function CatalogCard({
|
|
|
467
482
|
// transcript bytes at all) and is unaffected by this box.
|
|
468
483
|
const [capabilities, setCapabilities] = React.useState<string[]>(entry.capabilities);
|
|
469
484
|
|
|
470
|
-
|
|
485
|
+
// THE ONE THING THIS CARD BRANCHES ON (bd startsim-768w.22.25.14/.23).
|
|
486
|
+
// `schema.auth_mode_for` derives it server-side and its docstring says so;
|
|
487
|
+
// this component had no reference to it at all, so an OAuth connector got the
|
|
488
|
+
// same credential form as everything else — which on the admin catalog meant
|
|
489
|
+
// the product asking a human to paste a refresh token, the exact thing
|
|
490
|
+
// `auth_mode` exists to prevent.
|
|
491
|
+
const isOAuth = entry.auth_mode === 'oauth';
|
|
492
|
+
// A stale client is not a dead button. The verb is optional on both api
|
|
493
|
+
// shapes, so an app pinned to an older `@startsimpli/api` says the connector
|
|
494
|
+
// cannot be connected from here instead of rendering a hand-off that throws.
|
|
495
|
+
const canHandOff = isOAuth && typeof api.startOAuth === 'function';
|
|
496
|
+
|
|
497
|
+
// NO credential fields in OAuth mode. Not hidden — never collected: the whole
|
|
498
|
+
// point is that the token comes back from the vendor, and a form that
|
|
499
|
+
// "helpfully" accepted one would keep the paste-a-refresh-token path alive
|
|
500
|
+
// beside the consent flow that replaced it.
|
|
501
|
+
const credentialFields = isOAuth ? [] : schemaFields(entry.credential_schema);
|
|
502
|
+
// Config IS still asked for, in both modes. It is the folder a Dropbox card
|
|
503
|
+
// will poll, it is not a secret, and dropping it here would mean asking for it
|
|
504
|
+
// again after the round trip.
|
|
471
505
|
const configFields = schemaFields(entry.config_schema);
|
|
472
506
|
const fields = [...credentialFields, ...configFields];
|
|
473
507
|
const warnings = patternWarnings(fields, values);
|
|
474
508
|
const missing = missingRequired(fields, values);
|
|
475
509
|
|
|
510
|
+
/** Collect the config fields the form did gather. Shared by both submits. */
|
|
511
|
+
function collectConfig(): Record<string, unknown> {
|
|
512
|
+
const config: Record<string, unknown> = {};
|
|
513
|
+
for (const spec of configFields) {
|
|
514
|
+
const v = values[isSecret(spec) ? `${spec.key}__draft` : spec.key];
|
|
515
|
+
if (typeof v === 'boolean' || typeof v === 'number' || v) config[spec.key] = v;
|
|
516
|
+
}
|
|
517
|
+
return config;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Hand the browser to the vendor (bd startsim-768w.22.25.21/.22).
|
|
522
|
+
*
|
|
523
|
+
* ONE call: the server writes the shell card AND returns the consent URL,
|
|
524
|
+
* because neither half is usable alone. The URL is opened VERBATIM — it is
|
|
525
|
+
* already complete, and this client has no `client_id` to add to it and must
|
|
526
|
+
* not have one to choose.
|
|
527
|
+
*
|
|
528
|
+
* `return_to` is where the member should land afterwards. It is sent, not
|
|
529
|
+
* trusted: the server validates it against this tenant's own recorded origins
|
|
530
|
+
* before folding it into the signed state, because the callback is an
|
|
531
|
+
* anonymous browser mid-redirect from the vendor's domain.
|
|
532
|
+
*
|
|
533
|
+
* `location.assign`, not a new tab. A vendor consent screen that opens in a
|
|
534
|
+
* popup is the one an ad blocker eats, and the whole point of the round trip
|
|
535
|
+
* is that the browser comes BACK here.
|
|
536
|
+
*/
|
|
537
|
+
async function handOff() {
|
|
538
|
+
setBusy(true);
|
|
539
|
+
setError('');
|
|
540
|
+
try {
|
|
541
|
+
const { authorization_url: url } = await api.startOAuth!({
|
|
542
|
+
connector_key: entry.key,
|
|
543
|
+
name: entry.name,
|
|
544
|
+
config: collectConfig(),
|
|
545
|
+
capabilities_enabled: orderCapabilities(entry.capabilities, capabilities),
|
|
546
|
+
return_to: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
547
|
+
});
|
|
548
|
+
if (typeof window !== 'undefined') window.location.assign(url);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
setError((err as { message?: string })?.message || 'Could not start sign-in.');
|
|
551
|
+
setBusy(false);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
476
555
|
async function submit() {
|
|
477
556
|
setBusy(true);
|
|
478
557
|
setError('');
|
|
@@ -566,18 +645,56 @@ function CatalogCard({
|
|
|
566
645
|
Connecting here adds a SECOND account.
|
|
567
646
|
</p>
|
|
568
647
|
) : null}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
648
|
+
{/* SIGN IN AT THE VENDOR — never a form asking for token material.
|
|
649
|
+
`auth_mode: oauth` means the credential schema is `access_token` /
|
|
650
|
+
`refresh_token` / `expires_at`, and rendering those as inputs asks
|
|
651
|
+
a human to go and mint a refresh token by hand. That is what this
|
|
652
|
+
card did to every admin who opened Dropbox. */}
|
|
653
|
+
{isOAuth ? (
|
|
654
|
+
<div className="space-y-2" data-oauth-connect={entry.key}>
|
|
655
|
+
<p className={cn.muted}>
|
|
656
|
+
{entry.name} signs you in on its own site. You will be sent there
|
|
657
|
+
to approve access and returned here — no keys to copy, and
|
|
658
|
+
StartSimpli never sees your password.
|
|
659
|
+
</p>
|
|
660
|
+
{!canHandOff ? (
|
|
661
|
+
<p className={cn.error}>
|
|
662
|
+
This app cannot start a {entry.name} sign-in yet. Update it, or
|
|
663
|
+
connect {entry.name} from the foundry control plane.
|
|
664
|
+
</p>
|
|
665
|
+
) : null}
|
|
666
|
+
</div>
|
|
667
|
+
) : (
|
|
668
|
+
<CredentialSchemaForm
|
|
669
|
+
fields={fields}
|
|
670
|
+
values={values}
|
|
671
|
+
warnings={warnings}
|
|
672
|
+
idPrefix={`connect-${entry.key}`}
|
|
673
|
+
onChange={(key, value) => {
|
|
674
|
+
const spec = fields.find((f) => f.key === key);
|
|
675
|
+
setValues((prev) => ({
|
|
676
|
+
...prev, [spec && isSecret(spec) ? `${key}__draft` : key]: value,
|
|
677
|
+
}));
|
|
678
|
+
}}
|
|
679
|
+
/>
|
|
680
|
+
)}
|
|
681
|
+
{/* Config is asked for in BOTH modes — it is not a secret, and it is
|
|
682
|
+
the folder this card will poll. Rendered separately in OAuth mode
|
|
683
|
+
because the credential half of the same form is gone. */}
|
|
684
|
+
{isOAuth && configFields.length > 0 ? (
|
|
685
|
+
<CredentialSchemaForm
|
|
686
|
+
fields={configFields}
|
|
687
|
+
values={values}
|
|
688
|
+
warnings={warnings}
|
|
689
|
+
idPrefix={`connect-${entry.key}`}
|
|
690
|
+
onChange={(key, value) => {
|
|
691
|
+
const spec = configFields.find((f) => f.key === key);
|
|
692
|
+
setValues((prev) => ({
|
|
693
|
+
...prev, [spec && isSecret(spec) ? `${key}__draft` : key]: value,
|
|
694
|
+
}));
|
|
695
|
+
}}
|
|
696
|
+
/>
|
|
697
|
+
) : null}
|
|
581
698
|
<CapabilityPicker
|
|
582
699
|
scope="catalog"
|
|
583
700
|
declared={entry.capabilities}
|
|
@@ -603,12 +720,19 @@ function CatalogCard({
|
|
|
603
720
|
className={cn.button}
|
|
604
721
|
disabled={
|
|
605
722
|
busy
|
|
723
|
+
// `missing` counts only the fields this mode actually collects.
|
|
724
|
+
// In OAuth mode that is config alone — gating the hand-off on a
|
|
725
|
+
// required `access_token` would disable the very button whose job
|
|
726
|
+
// is to go and get one.
|
|
606
727
|
|| missing.length > 0
|
|
728
|
+
|| (isOAuth && !canHandOff)
|
|
607
729
|
|| (entry.capabilities.length > 0 && capabilities.length === 0)
|
|
608
730
|
}
|
|
609
|
-
onClick={() => void submit()}
|
|
731
|
+
onClick={() => void (isOAuth ? handOff() : submit())}
|
|
610
732
|
>
|
|
611
|
-
{
|
|
733
|
+
{isOAuth
|
|
734
|
+
? (busy ? 'Redirecting…' : `Continue to ${entry.name}`)
|
|
735
|
+
: (busy ? 'Testing…' : 'Test and save')}
|
|
612
736
|
</button>
|
|
613
737
|
</div>
|
|
614
738
|
) : null}
|