Resolves the slug against the tenant catalog and forwards into the integration detail page's existing add-connection flow. Unknown slugs get a not-found state. Makes the link survive sign-in: reserve connect as an org slug so cloud's callback stops reading it as an org selector, and return self-host's login to the requested path (its gate swaps the page in without ever writing a returnTo).
Showing the changes to packages (10 of 17 files). Show all
Unified · Split
| @@ -17,6 +17,7 @@ import { Route as DotDotDotDotDotDotReactSrcRoutesPoliciesRouteImport } from './ · 16 unmodified lines | |||
| 17 | import { Route as DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../react/src/routes/toolkits.$toolkitSlug' | 17 | import { Route as DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRouteImport } from './../../react/src/routes/toolkits.$toolkitSlug' |
| 18 | import { Route as DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRouteImport } from './../../react/src/routes/resume.$executionId' | 18 | import { Route as DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRouteImport } from './../../react/src/routes/resume.$executionId' |
| 19 | import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../react/src/routes/integrations.$namespace' | 19 | import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRouteImport } from './../../react/src/routes/integrations.$namespace' |
| 20 | import { Route as DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRouteImport } from './../../react/src/routes/connect.$integrationSlug' | ||
| 20 | import { Route as DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport } from './../../react/src/routes/plugins.$pluginId.$' | 21 | import { Route as DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport } from './../../react/src/routes/plugins.$pluginId.$' |
| 21 | import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../react/src/routes/integrations.add.$pluginKey' | 22 | import { Route as DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRouteImport } from './../../react/src/routes/integrations.add.$pluginKey' |
| 22 | 23 | ||
| @@ -67,6 +68,12 @@ const DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute = · 44 unmodified lines | |||
| 67 | path: '/{-$orgSlug}/integrations/$namespace', | 68 | path: '/{-$orgSlug}/integrations/$namespace', |
| 68 | getParentRoute: () => rootRouteImport, | 69 | getParentRoute: () => rootRouteImport, |
| 69 | } as any) | 70 | } as any) |
| 71 | const DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRoute = | ||
| 72 | DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRouteImport.update({ | ||
| 73 | id: '/{-$orgSlug}/connect/$integrationSlug', | ||
| 74 | path: '/{-$orgSlug}/connect/$integrationSlug', | ||
| 75 | getParentRoute: () => rootRouteImport, | ||
| 76 | } as any) | ||
| 70 | const DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute = | 77 | const DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRoute = |
| 71 | DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport.update({ | 78 | DotDotDotDotDotDotReactSrcRoutesPluginsDotpluginIdDotsplatRouteImport.update({ |
| 72 | id: '/{-$orgSlug}/plugins/$pluginId/$', | 79 | id: '/{-$orgSlug}/plugins/$pluginId/$', |
| @@ -88,6 +95,7 @@ export interface FileRoutesByFullPath { · 15 unmodified lines | |||
| 88 | '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren | 95 | '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren |
| 89 | '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute | 96 | '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute |
| 90 | '/{-$orgSlug}/': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute | 97 | '/{-$orgSlug}/': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute |
| 98 | '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRoute | ||
| 91 | '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute | 99 | '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute |
| 92 | '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute | 100 | '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute |
| 93 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute | 101 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute |
| @@ -100,6 +108,7 @@ export interface FileRoutesByTo { · 6 unmodified lines | |||
| 100 | '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren | 108 | '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren |
| 101 | '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute | 109 | '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute |
| 102 | '/{-$orgSlug}': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute | 110 | '/{-$orgSlug}': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute |
| 111 | '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRoute | ||
| 103 | '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute | 112 | '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute |
| 104 | '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute | 113 | '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute |
| 105 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute | 114 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute |
| @@ -113,6 +122,7 @@ export interface FileRoutesById { · 7 unmodified lines | |||
| 113 | '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren | 122 | '/{-$orgSlug}/toolkits': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren |
| 114 | '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute | 123 | '/{-$orgSlug}/tools': typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute |
| 115 | '/{-$orgSlug}/': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute | 124 | '/{-$orgSlug}/': typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute |
| 125 | '/{-$orgSlug}/connect/$integrationSlug': typeof DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRoute | ||
| 116 | '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute | 126 | '/{-$orgSlug}/integrations/$namespace': typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute |
| 117 | '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute | 127 | '/{-$orgSlug}/resume/$executionId': typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute |
| 118 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute | 128 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotDotDotDotDotDotReactSrcRoutesToolkitsDottoolkitSlugRoute |
| @@ -127,6 +137,7 @@ export interface FileRouteTypes { · 8 unmodified lines | |||
| 127 | | '/{-$orgSlug}/toolkits' | 137 | | '/{-$orgSlug}/toolkits' |
| 128 | | '/{-$orgSlug}/tools' | 138 | | '/{-$orgSlug}/tools' |
| 129 | | '/{-$orgSlug}/' | 139 | | '/{-$orgSlug}/' |
| 140 | | '/{-$orgSlug}/connect/$integrationSlug' | ||
| 130 | | '/{-$orgSlug}/integrations/$namespace' | 141 | | '/{-$orgSlug}/integrations/$namespace' |
| 131 | | '/{-$orgSlug}/resume/$executionId' | 142 | | '/{-$orgSlug}/resume/$executionId' |
| 132 | | '/{-$orgSlug}/toolkits/$toolkitSlug' | 143 | | '/{-$orgSlug}/toolkits/$toolkitSlug' |
| @@ -139,6 +150,7 @@ export interface FileRouteTypes { · 6 unmodified lines | |||
| 139 | | '/{-$orgSlug}/toolkits' | 150 | | '/{-$orgSlug}/toolkits' |
| 140 | | '/{-$orgSlug}/tools' | 151 | | '/{-$orgSlug}/tools' |
| 141 | | '/{-$orgSlug}' | 152 | | '/{-$orgSlug}' |
| 153 | | '/{-$orgSlug}/connect/$integrationSlug' | ||
| 142 | | '/{-$orgSlug}/integrations/$namespace' | 154 | | '/{-$orgSlug}/integrations/$namespace' |
| 143 | | '/{-$orgSlug}/resume/$executionId' | 155 | | '/{-$orgSlug}/resume/$executionId' |
| 144 | | '/{-$orgSlug}/toolkits/$toolkitSlug' | 156 | | '/{-$orgSlug}/toolkits/$toolkitSlug' |
| @@ -151,6 +163,7 @@ export interface FileRouteTypes { · 6 unmodified lines | |||
| 151 | | '/{-$orgSlug}/toolkits' | 163 | | '/{-$orgSlug}/toolkits' |
| 152 | | '/{-$orgSlug}/tools' | 164 | | '/{-$orgSlug}/tools' |
| 153 | | '/{-$orgSlug}/' | 165 | | '/{-$orgSlug}/' |
| 166 | | '/{-$orgSlug}/connect/$integrationSlug' | ||
| 154 | | '/{-$orgSlug}/integrations/$namespace' | 167 | | '/{-$orgSlug}/integrations/$namespace' |
| 155 | | '/{-$orgSlug}/resume/$executionId' | 168 | | '/{-$orgSlug}/resume/$executionId' |
| 156 | | '/{-$orgSlug}/toolkits/$toolkitSlug' | 169 | | '/{-$orgSlug}/toolkits/$toolkitSlug' |
| @@ -164,6 +177,7 @@ export interface RootRouteChildren { · 7 unmodified lines | |||
| 164 | DotDotDotDotDotDotReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren | 177 | DotDotDotDotDotDotReactSrcRoutesToolkitsRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolkitsRouteWithChildren |
| 165 | DotDotDotDotDotDotReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute | 178 | DotDotDotDotDotDotReactSrcRoutesToolsRoute: typeof DotDotDotDotDotDotReactSrcRoutesToolsRoute |
| 166 | DotDotDotDotDotDotReactSrcRoutesIndexRoute: typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute | 179 | DotDotDotDotDotDotReactSrcRoutesIndexRoute: typeof DotDotDotDotDotDotReactSrcRoutesIndexRoute |
| 180 | DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRoute: typeof DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRoute | ||
| 167 | DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute: typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute | 181 | DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute: typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute |
| 168 | DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute: typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute | 182 | DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute: typeof DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute |
| 169 | DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute: typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute | 183 | DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute: typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotaddDotpluginKeyRoute |
| @@ -228,6 +242,13 @@ declare module '@tanstack/react-router' { · 58 unmodified lines | |||
| 228 | preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRouteImport | 242 | preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRouteImport |
| 229 | parentRoute: typeof rootRouteImport | 243 | parentRoute: typeof rootRouteImport |
| 230 | } | 244 | } |
| 245 | '/{-$orgSlug}/connect/$integrationSlug': { | ||
| 246 | id: '/{-$orgSlug}/connect/$integrationSlug' | ||
| 247 | path: '/{-$orgSlug}/connect/$integrationSlug' | ||
| 248 | fullPath: '/{-$orgSlug}/connect/$integrationSlug' | ||
| 249 | preLoaderRoute: typeof DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRouteImport | ||
| 250 | parentRoute: typeof rootRouteImport | ||
| 251 | } | ||
| 231 | '/{-$orgSlug}/plugins/$pluginId/$': { | 252 | '/{-$orgSlug}/plugins/$pluginId/$': { |
| 232 | id: '/{-$orgSlug}/plugins/$pluginId/$' | 253 | id: '/{-$orgSlug}/plugins/$pluginId/$' |
| 233 | path: '/{-$orgSlug}/plugins/$pluginId/$' | 254 | path: '/{-$orgSlug}/plugins/$pluginId/$' |
| @@ -270,6 +291,8 @@ const rootRouteChildren: RootRouteChildren = { · 36 unmodified lines | |||
| 270 | DotDotDotDotDotDotReactSrcRoutesToolsRoute, | 291 | DotDotDotDotDotDotReactSrcRoutesToolsRoute, |
| 271 | DotDotDotDotDotDotReactSrcRoutesIndexRoute: | 292 | DotDotDotDotDotDotReactSrcRoutesIndexRoute: |
| 272 | DotDotDotDotDotDotReactSrcRoutesIndexRoute, | 293 | DotDotDotDotDotDotReactSrcRoutesIndexRoute, |
| 294 | DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRoute: | ||
| 295 | DotDotDotDotDotDotReactSrcRoutesConnectDotintegrationSlugRoute, | ||
| 273 | DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute: | 296 | DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute: |
| 274 | DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute, | 297 | DotDotDotDotDotDotReactSrcRoutesIntegrationsDotnamespaceRoute, |
| 275 | DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute: | 298 | DotDotDotDotDotDotReactSrcRoutesResumeDotexecutionIdRoute: |
| @@ -57,7 +57,20 @@ describe("isValidOrgSlug", () => { · 56 unmodified lines | |||
| 57 | }); | 57 | }); |
| 58 | 58 | ||
| 59 | it("reserves the segments routing depends on", () => { | 59 | it("reserves the segments routing depends on", () => { |
| 60 | for (const critical of ["api", "mcp", "integrations", "policies", "login", "cdn-cgi"]) { | 60 | for (const critical of [ |
| 61 | "api", | ||
| 62 | "mcp", | ||
| 63 | "integrations", | ||
| 64 | "policies", | ||
| 65 | "login", | ||
| 66 | "cdn-cgi", | ||
| 67 | // The connect deep link's root. Cloud's post-auth callback reads the | ||
| 68 | // first path segment as an org selector, so leaving `connect` claimable | ||
| 69 | // made `/connect/<slug>` look like a request for an org named "connect" | ||
| 70 | // — which suppressed the active-membership fallback and could drop the | ||
| 71 | // deep link into org onboarding after sign-in. | ||
| 72 | "connect", | ||
| 73 | ]) { | ||
| 61 | expect(RESERVED_ORG_SLUGS.has(critical), critical).toBe(true); | 74 | expect(RESERVED_ORG_SLUGS.has(critical), critical).toBe(true); |
| 62 | } | 75 | } |
| 63 | }); | 76 | }); |
| @@ -17,8 +17,8 @@ const ORG_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){1,47}$/; · 16 unmodified lines | |||
| 17 | * predictably will): | 17 | * predictably will): |
| 18 | * - App planes: api, mcp, .well-known (cloud `app-paths.ts`, selfhost | 18 | * - App planes: api, mcp, .well-known (cloud `app-paths.ts`, selfhost |
| 19 | * envelope, cloudflare `run_worker_first`) | 19 | * envelope, cloudflare `run_worker_first`) |
| 20 | * - Console routes: integrations, policies, secrets, tools, resume, | 20 | * - Console routes: connect, integrations, policies, secrets, tools, |
| 21 | * plugins (the shared contract), plus host extras: | 21 | * resume, plugins (the shared contract), plus host extras: |
| 22 | * api-keys, org, billing, create-org, setup-mcp (cloud), | 22 | * api-keys, org, billing, create-org, setup-mcp (cloud), |
| 23 | * admin, join, docs (selfhost), login (Better Auth | 23 | * admin, join, docs (selfhost), login (Better Auth |
| 24 | * `mcp({ loginPage })` + cloud/selfhost login UX) | 24 | * `mcp({ loginPage })` + cloud/selfhost login UX) |
| @@ -41,6 +41,7 @@ export const RESERVED_ORG_SLUGS: ReadonlySet<string> = new Set([ · 16 unmodified lines | |||
| 41 | "mcp", | 41 | "mcp", |
| 42 | ".well-known", | 42 | ".well-known", |
| 43 | // console + host routes | 43 | // console + host routes |
| 44 | "connect", | ||
| 44 | "integrations", | 45 | "integrations", |
| 45 | "policies", | 46 | "policies", |
| 46 | "secrets", | 47 | "secrets", |
| @@ -34,6 +34,7 @@ export const ORG_SLUG_SEGMENT = "{-$orgSlug}"; · 33 unmodified lines | |||
| 34 | * are these paths prefixed with `/{-$orgSlug}`. */ | 34 | * are these paths prefixed with `/{-$orgSlug}`. */ |
| 35 | export const CONSOLE_ROUTE_PATHS = [ | 35 | export const CONSOLE_ROUTE_PATHS = [ |
| 36 | "/", | 36 | "/", |
| 37 | "/connect/$integrationSlug", | ||
| 37 | "/integrations/$namespace", | 38 | "/integrations/$namespace", |
| 38 | "/integrations/add/$pluginKey", | 39 | "/integrations/add/$pluginKey", |
| 39 | "/policies", | 40 | "/policies", |
| @@ -62,6 +63,10 @@ export const consoleRoutes = (options: ConsoleRoutesOptions): Array<VirtualRoute · 22 unmodified lines | |||
| 62 | const file = (name: string): string => `${options.dir}/${name}`; | 63 | const file = (name: string): string => `${options.dir}/${name}`; |
| 63 | const entries: ReadonlyArray<readonly [ConsoleRoutePath, VirtualRouteNode]> = [ | 64 | const entries: ReadonlyArray<readonly [ConsoleRoutePath, VirtualRouteNode]> = [ |
| 64 | ["/", index(file("index.tsx"))], | 65 | ["/", index(file("index.tsx"))], |
| 66 | [ | ||
| 67 | "/connect/$integrationSlug", | ||
| 68 | route("/connect/$integrationSlug", file("connect.$integrationSlug.tsx")), | ||
| 69 | ], | ||
| 65 | [ | 70 | [ |
| 66 | "/integrations/$namespace", | 71 | "/integrations/$namespace", |
| 67 | route("/integrations/$namespace", file("integrations.$namespace.tsx")), | 72 | route("/integrations/$namespace", file("integrations.$namespace.tsx")), |
| @@ -0,0 +1,109 @@ | |||
| 1 | import { useEffect } from "react"; | ||
| 2 | import { Link, useNavigate } from "@tanstack/react-router"; | ||
| 3 | import { useAtomValue, useAtomRefresh } from "@effect/atom-react"; | ||
| 4 | import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; | ||
| 5 | import { IntegrationSlug } from "@executor-js/sdk/shared"; | ||
| 6 | |||
| 7 | import { integrationAtom, integrationsOptimisticAtom } from "../api/atoms"; | ||
| 8 | import { ErrorState } from "../components/error-state"; | ||
| 9 | import { Skeleton } from "../components/skeleton"; | ||
| 10 | import { isAsyncResultLoading } from "../lib/async-result"; | ||
| 11 | import { useExecutorDocumentTitle } from "../lib/document-title"; | ||
| 12 | |||
| 13 | // --------------------------------------------------------------------------- | ||
| 14 | // `/connect/<integration slug>` — the stable, shareable deep link that drops a | ||
| 15 | // person straight into the connect flow for ONE integration ("connect your | ||
| 16 | // Linear") instead of the dashboard. | ||
| 17 | // | ||
| 18 | // This page owns no connect UI of its own. It resolves the slug against the | ||
| 19 | // tenant's catalog and forwards into the integration detail page's existing | ||
| 20 | // account handoff (`?addAccount=1`), which is the same surface the detail | ||
| 21 | // page's own "Add connection" button opens — multi-method tab strips, OAuth | ||
| 22 | // popups, existing-connection lists and "add another account" all come for | ||
| 23 | // free, and stay correct as that flow evolves. | ||
| 24 | // | ||
| 25 | // Deliberately keyed on the INTEGRATION SLUG, not a plugin id: the slug is the | ||
| 26 | // catalog identity a customer can write down, whereas plugin routes are on | ||
| 27 | // their way out. The redirect is `replace: true` so the deep link does not | ||
| 28 | // linger in history and Back leaves the connect flow rather than bouncing. | ||
| 29 | // --------------------------------------------------------------------------- | ||
| 30 | |||
| 31 | export function ConnectIntegrationPage(props: { readonly integrationSlug: string }) { | ||
| 32 | const { integrationSlug } = props; | ||
| 33 | const slug = IntegrationSlug.make(integrationSlug); | ||
| 34 | const integration = useAtomValue(integrationAtom(slug)); | ||
| 35 | const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom); | ||
| 36 | const navigate = useNavigate(); | ||
| 37 | |||
| 38 | useExecutorDocumentTitle("Connect"); | ||
| 39 | |||
| 40 | // The catalog read is a client-side find over the integrations list, so an | ||
| 41 | // unknown slug resolves to Success(null) rather than a failure — the missing | ||
| 42 | // case is a not-found state, and only a genuine fetch failure is an error. | ||
| 43 | const resolved = AsyncResult.isSuccess(integration) ? integration.value : null; | ||
| 44 | const loading = isAsyncResultLoading(integration); | ||
| 45 | const failed = AsyncResult.isFailure(integration); | ||
| 46 | const missing = !loading && !failed && resolved === null; | ||
| 47 | |||
| 48 | useEffect(() => { | ||
| 49 | if (resolved === null) return; | ||
| 50 | void navigate({ | ||
| 51 | to: "/{-$orgSlug}/integrations/$namespace", | ||
| 52 | params: { namespace: String(resolved.slug) }, | ||
| 53 | // The NUMBER 1, so the router serializes the canonical `?addAccount=1` | ||
| 54 | // that agent-generated handoff links already use (a string would be | ||
| 55 | // quoted as `%221%22`). | ||
| 56 | search: { tab: "accounts", addAccount: 1 }, | ||
| 57 | replace: true, | ||
| 58 | }); | ||
| 59 | }, [resolved, navigate]); | ||
| 60 | |||
| 61 | if (failed) { | ||
| 62 | return ( | ||
| 63 | <ConnectStateFrame> | ||
| 64 | <ErrorState message="Failed to load integrations" onRetry={refreshIntegrations} /> | ||
| 65 | </ConnectStateFrame> | ||
| 66 | ); | ||
| 67 | } | ||
| 68 | |||
| 69 | if (missing) { | ||
| 70 | return ( | ||
| 71 | <ConnectStateFrame> | ||
| 72 | <div className="flex flex-col items-center justify-center rounded-2xl border border-dashed border-border py-20"> | ||
| 73 | <p className="mb-1 text-sm font-medium text-foreground/70"> | ||
| 74 | Unknown integration: {integrationSlug} | ||
| 75 | </p> | ||
| 76 | <p className="mb-5 text-xs text-muted-foreground"> | ||
| 77 | This integration is not in your workspace. | ||
| 78 | </p> | ||
| 79 | <Link | ||
| 80 | to="/{-$orgSlug}" | ||
| 81 | className="inline-flex items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90" | ||
| 82 | > | ||
| 83 | Back to integrations | ||
| 84 | </Link> | ||
| 85 | </div> | ||
| 86 | </ConnectStateFrame> | ||
| 87 | ); | ||
| 88 | } | ||
| 89 | |||
| 90 | // Loading, or resolved and mid-redirect: the same quiet placeholder, so the | ||
| 91 | // hand-off never flashes an empty frame between catalog load and navigation. | ||
| 92 | return ( | ||
| 93 | <ConnectStateFrame> | ||
| 94 | <div className="space-y-3"> | ||
| 95 | <Skeleton className="h-8 w-56" /> | ||
| 96 | <Skeleton className="h-4 w-80" /> | ||
| 97 | <Skeleton className="h-32 w-full" /> | ||
| 98 | </div> | ||
| 99 | </ConnectStateFrame> | ||
| 100 | ); | ||
| 101 | } | ||
| 102 | |||
| 103 | function ConnectStateFrame(props: { readonly children: React.ReactNode }) { | ||
| 104 | return ( | ||
| 105 | <div className="relative min-h-0 flex-1 overflow-y-auto"> | ||
| 106 | <div className="mx-auto max-w-4xl px-6 py-10 lg:px-10 lg:py-14">{props.children}</div> | ||
| 107 | </div> | ||
| 108 | ); | ||
| 109 | } | ||
| @@ -60,6 +60,10 @@ type ToolRow = { · 59 unmodified lines | |||
| 60 | export function IntegrationDetailPage(props: { | 60 | export function IntegrationDetailPage(props: { |
| 61 | namespace: string; | 61 | namespace: string; |
| 62 | tab?: IntegrationDetailSearchTab; | 62 | tab?: IntegrationDetailSearchTab; |
| 63 | /** Route-validated `addAccount=1`: open the add-connection flow on arrival. | ||
| 64 | * Set by the `/connect/<slug>` deep link, which navigates client-side and so | ||
| 65 | * cannot rely on the raw location search below. */ | ||
| 66 | addAccount?: boolean; | ||
| 63 | }) { | 67 | }) { |
| 64 | const { namespace } = props; | 68 | const { namespace } = props; |
| 65 | const slug = IntegrationSlug.make(namespace); | 69 | const slug = IntegrationSlug.make(namespace); |
| @@ -122,9 +126,11 @@ export function IntegrationDetailPage(props: { · 56 unmodified lines | |||
| 122 | const canRefresh = integrationData?.canRefresh ?? false; | 126 | const canRefresh = integrationData?.canRefresh ?? false; |
| 123 | const canRemove = integrationData?.canRemove ?? false; | 127 | const canRemove = integrationData?.canRemove ?? false; |
| 124 | const urlAccountHandoff = useMemo<IntegrationAccountHandoff | null>(() => { | 128 | const urlAccountHandoff = useMemo<IntegrationAccountHandoff | null>(() => { |
| 125 | if (locationSearch.length === 0) return null; | ||
| 126 | const search = new URLSearchParams(locationSearch); | 129 | const search = new URLSearchParams(locationSearch); |
| 127 | if (search.get("addAccount") !== "1") return null; | 130 | // The route-validated flag and the raw `addAccount=1` are the same request; |
| 131 | // either one opens the flow. The extra prefill fields below are read from | ||
| 132 | // the raw search, which is populated on the full page loads that carry them. | ||
| 133 | if (!props.addAccount && search.get("addAccount") !== "1") return null; | ||
| 128 | const owner = search.get("owner"); | 134 | const owner = search.get("owner"); |
| 129 | const template = search.get("template"); | 135 | const template = search.get("template"); |
| 130 | const label = search.get("label"); | 136 | const label = search.get("label"); |
| @@ -149,13 +155,13 @@ export function IntegrationDetailPage(props: { · 18 unmodified lines | |||
| 149 | }; | 155 | }; |
| 150 | })(); | 156 | })(); |
| 151 | return { | 157 | return { |
| 152 | key: locationSearch, | 158 | key: `${String(props.addAccount)}:${locationSearch}`, |
| 153 | ...(owner === "org" || owner === "user" ? { owner } : {}), | 159 | ...(owner === "org" || owner === "user" ? { owner } : {}), |
| 154 | ...(template != null && template.length > 0 ? { template } : {}), | 160 | ...(template != null && template.length > 0 ? { template } : {}), |
| 155 | ...(label != null && label.length > 0 ? { label } : {}), | 161 | ...(label != null && label.length > 0 ? { label } : {}), |
| 156 | ...(oauthClient !== undefined ? { oauthClient } : {}), | 162 | ...(oauthClient !== undefined ? { oauthClient } : {}), |
| 157 | }; | 163 | }; |
| 158 | }, [locationSearch]); | 164 | }, [locationSearch, props.addAccount]); |
| 159 | const accountHandoff = manualAccountHandoff ?? urlAccountHandoff; | 165 | const accountHandoff = manualAccountHandoff ?? urlAccountHandoff; |
| 160 | 166 | ||
| 161 | useEffect(() => { | 167 | useEffect(() => { |
| @@ -0,0 +1,121 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | ||
| 2 | import { | ||
| 3 | createMemoryHistory, | ||
| 4 | createRootRoute, | ||
| 5 | createRoute, | ||
| 6 | createRouter, | ||
| 7 | } from "@tanstack/react-router"; | ||
| 8 | |||
| 9 | import { CONSOLE_ROUTE_PATHS } from "../console-routes"; | ||
| 10 | import { isAddAccountRequested } from "./integrations.$namespace"; | ||
| 11 | |||
| 12 | // Routing semantics of the `/connect/<integration slug>` deep link — the URL a | ||
| 13 | // customer hands to an end user ("connect your Linear"). The page component is | ||
| 14 | // exercised end-to-end in e2e/scenarios/connect-deep-link.test.ts; this locks | ||
| 15 | // the URL contract itself: which shapes match, what the params resolve to, and | ||
| 16 | // that the org-slug prefix round-trips. | ||
| 17 | |||
| 18 | type RouteMatch = { readonly routeId: string; readonly params: Record<string, string> }; | ||
| 19 | type SpikeRouter = { | ||
| 20 | matchRoutes(pathname: string): ReadonlyArray<RouteMatch>; | ||
| 21 | buildLocation(options: unknown): { readonly href: string }; | ||
| 22 | }; | ||
| 23 | |||
| 24 | const CONNECT_ROUTE_ID = "/{-$orgSlug}/connect/$integrationSlug"; | ||
| 25 | |||
| 26 | const make = (): SpikeRouter => { | ||
| 27 | const rootRoute = createRootRoute(); | ||
| 28 | const orgScope = createRoute({ getParentRoute: () => rootRoute, path: "/{-$orgSlug}" }); | ||
| 29 | const indexRoute = createRoute({ getParentRoute: () => orgScope, path: "/" }); | ||
| 30 | const connectRoute = createRoute({ | ||
| 31 | getParentRoute: () => orgScope, | ||
| 32 | path: "/connect/$integrationSlug", | ||
| 33 | }); | ||
| 34 | const nsRoute = createRoute({ | ||
| 35 | getParentRoute: () => orgScope, | ||
| 36 | path: "/integrations/$namespace", | ||
| 37 | }); | ||
| 38 | const routeTree = rootRoute.addChildren([ | ||
| 39 | orgScope.addChildren([indexRoute, connectRoute, nsRoute]), | ||
| 40 | ]); | ||
| 41 | // A test-local tree is not the package's registered console tree, so widen | ||
| 42 | // away the global Register types — this asserts runtime semantics. | ||
| 43 | // oxlint-disable-next-line executor/no-double-cast -- boundary: a test-local route tree must escape the package-global router Register types | ||
| 44 | return createRouter({ | ||
| 45 | routeTree, | ||
| 46 | history: createMemoryHistory({ initialEntries: ["/"] }), | ||
| 47 | }) as unknown as SpikeRouter; | ||
| 48 | }; | ||
| 49 | |||
| 50 | describe("connect deep link", () => { | ||
| 51 | it("is part of the shared console route contract", () => { | ||
| 52 | // Every host mounts the contract, so the deep link exists on cloud, | ||
| 53 | // self-host, cloudflare and local alike. | ||
| 54 | expect(CONSOLE_ROUTE_PATHS).toContain("/connect/$integrationSlug"); | ||
| 55 | }); | ||
| 56 | |||
| 57 | it("matches the bare link owner.com would generate", () => { | ||
| 58 | const matches = make().matchRoutes("/connect/linear"); | ||
| 59 | const match = matches.find((m) => m.routeId === CONNECT_ROUTE_ID); | ||
| 60 | expect(match?.params).toEqual({ integrationSlug: "linear" }); | ||
| 61 | }); | ||
| 62 | |||
| 63 | it("matches the org-prefixed form without eating the slug", () => { | ||
| 64 | const matches = make().matchRoutes("/acme/connect/linear"); | ||
| 65 | const match = matches.find((m) => m.routeId === CONNECT_ROUTE_ID); | ||
| 66 | expect(match?.params).toEqual({ orgSlug: "acme", integrationSlug: "linear" }); | ||
| 67 | }); | ||
| 68 | |||
| 69 | it("keeps slugs that look like other console words intact", () => { | ||
| 70 | // The integration slug is a free-form catalog id; "tools" or "integrations" | ||
| 71 | // as a slug must still land on connect, not on those pages. | ||
| 72 | for (const slug of ["tools", "integrations", "connect"]) { | ||
| 73 | const match = make() | ||
| 74 | .matchRoutes(`/connect/${slug}`) | ||
| 75 | .find((m) => m.routeId === CONNECT_ROUTE_ID); | ||
| 76 | expect(match?.params).toEqual({ integrationSlug: slug }); | ||
| 77 | } | ||
| 78 | }); | ||
| 79 | |||
| 80 | it("builds the link with and without an org slug", () => { | ||
| 81 | const router = make(); | ||
| 82 | expect( | ||
| 83 | router.buildLocation({ to: CONNECT_ROUTE_ID, params: { integrationSlug: "linear" } }).href, | ||
| 84 | ).toBe("/connect/linear"); | ||
| 85 | expect( | ||
| 86 | router.buildLocation({ | ||
| 87 | to: CONNECT_ROUTE_ID, | ||
| 88 | params: { orgSlug: "acme", integrationSlug: "linear" }, | ||
| 89 | }).href, | ||
| 90 | ).toBe("/acme/connect/linear"); | ||
| 91 | }); | ||
| 92 | |||
| 93 | it("forwards into the integration's existing connect flow", () => { | ||
| 94 | // The page redirects here rather than rendering its own connect UI; the | ||
| 95 | // handoff is carried as validated search so it survives client navigation. | ||
| 96 | // The number 1 keeps the canonical `?addAccount=1` that agent-generated | ||
| 97 | // handoff links already use — a string would serialize as `%221%22`. | ||
| 98 | const href = make().buildLocation({ | ||
| 99 | to: "/{-$orgSlug}/integrations/$namespace", | ||
| 100 | params: { namespace: "linear" }, | ||
| 101 | search: { tab: "accounts", addAccount: 1 }, | ||
| 102 | }).href; | ||
| 103 | expect(href).toBe("/integrations/linear?tab=accounts&addAccount=1"); | ||
| 104 | }); | ||
| 105 | }); | ||
| 106 | |||
| 107 | describe("addAccount flag", () => { | ||
| 108 | it("accepts every spelling the two link sources produce", () => { | ||
| 109 | // A full page load parses `?addAccount=1` to the number; a client-side | ||
| 110 | // navigation may carry the string. Both mean "open the connect flow". | ||
| 111 | expect(isAddAccountRequested(1)).toBe(true); | ||
| 112 | expect(isAddAccountRequested("1")).toBe(true); | ||
| 113 | expect(isAddAccountRequested(true)).toBe(true); | ||
| 114 | }); | ||
| 115 | |||
| 116 | it("ignores anything else instead of failing the page", () => { | ||
| 117 | for (const value of [undefined, null, 0, "0", "", "banana", {}]) { | ||
| 118 | expect(isAddAccountRequested(value)).toBe(false); | ||
| 119 | } | ||
| 120 | }); | ||
| 121 | }); | ||
| @@ -0,0 +1,10 @@ | |||
| 1 | import { createFileRoute } from "@tanstack/react-router"; | ||
| 2 | |||
| 3 | import { ConnectIntegrationPage } from "../pages/connect-integration"; | ||
| 4 | |||
| 5 | export const Route = createFileRoute("/{-$orgSlug}/connect/$integrationSlug")({ | ||
| 6 | component: () => { | ||
| 7 | const { integrationSlug } = Route.useParams(); | ||
| 8 | return <ConnectIntegrationPage integrationSlug={integrationSlug} />; | ||
| 9 | }, | ||
| 10 | }); | ||
| @@ -4,21 +4,40 @@ import { createFileRoute } from "@tanstack/react-router"; · 3 unmodified lines | |||
| 4 | import { IntegrationDetailPage } from "../pages/integration-detail"; | 4 | import { IntegrationDetailPage } from "../pages/integration-detail"; |
| 5 | import type { IntegrationDetailSearchTab } from "../lib/integration-detail-tabs"; | 5 | import type { IntegrationDetailSearchTab } from "../lib/integration-detail-tabs"; |
| 6 | 6 | ||
| 7 | // `addAccount=1` opens the add-connection flow on arrival. It is declared here | ||
| 8 | // (rather than only read off `window.location`) because unlisted keys do not | ||
| 9 | // survive validation: a CLIENT-SIDE navigation carrying the handoff — the | ||
| 10 | // `/connect/<slug>` deep link — would otherwise have it stripped before the | ||
| 11 | // page could see it. The richer handoff fields stay URL-only; they arrive on | ||
| 12 | // full page loads from agent-generated links. | ||
| 13 | // | ||
| 14 | // Typed as unknown and coerced, not as a literal, for two reasons. A URL flag | ||
| 15 | // must never hard-fail a page: a hand-edited `?addAccount=banana` should be | ||
| 16 | // ignored, not rendered as a validation error. And the router's search | ||
| 17 | // serializer quotes a string "1" (`addAccount=%221%22`) while leaving the | ||
| 18 | // number 1 bare, so accepting both is what keeps ONE canonical URL — | ||
| 19 | // `?addAccount=1` — shared by deep links and agent-generated handoff links. | ||
| 7 | const SearchParams = Schema.toStandardSchemaV1( | 20 | const SearchParams = Schema.toStandardSchemaV1( |
| 8 | Schema.Struct({ | 21 | Schema.Struct({ |
| 9 | tab: Schema.optional(Schema.Literals(["accounts", "source", "tools"])), | 22 | tab: Schema.optional(Schema.Literals(["accounts", "source", "tools"])), |
| 23 | addAccount: Schema.optional(Schema.Unknown), | ||
| 10 | }), | 24 | }), |
| 11 | ); | 25 | ); |
| 12 | 26 | ||
| 27 | /** The truthy spellings of the `addAccount` URL flag. */ | ||
| 28 | export const isAddAccountRequested = (value: unknown): boolean => | ||
| 29 | value === 1 || value === "1" || value === true; | ||
| 30 | |||
| 13 | export const Route = createFileRoute("/{-$orgSlug}/integrations/$namespace")({ | 31 | export const Route = createFileRoute("/{-$orgSlug}/integrations/$namespace")({ |
| 14 | validateSearch: SearchParams, | 32 | validateSearch: SearchParams, |
| 15 | component: () => { | 33 | component: () => { |
| 16 | const { namespace } = Route.useParams(); | 34 | const { namespace } = Route.useParams(); |
| 17 | const { tab } = Route.useSearch(); | 35 | const { tab, addAccount } = Route.useSearch(); |
| 18 | return ( | 36 | return ( |
| 19 | <IntegrationDetailPage | 37 | <IntegrationDetailPage |
| 20 | namespace={namespace} | 38 | namespace={namespace} |
| 21 | tab={tab as IntegrationDetailSearchTab | undefined} | 39 | tab={tab as IntegrationDetailSearchTab | undefined} |
| 40 | addAccount={isAddAccountRequested(addAccount)} | ||
| 22 | /> | 41 | /> |
| 23 | ); | 42 | ); |
| 24 | }, | 43 | }, |
| @@ -17,6 +17,7 @@ import { Route as DotPoliciesRouteImport } from './policies' · 16 unmodified lines | |||
| 17 | import { Route as DotToolkitsDottoolkitSlugRouteImport } from './toolkits.$toolkitSlug' | 17 | import { Route as DotToolkitsDottoolkitSlugRouteImport } from './toolkits.$toolkitSlug' |
| 18 | import { Route as DotResumeDotexecutionIdRouteImport } from './resume.$executionId' | 18 | import { Route as DotResumeDotexecutionIdRouteImport } from './resume.$executionId' |
| 19 | import { Route as DotIntegrationsDotnamespaceRouteImport } from './integrations.$namespace' | 19 | import { Route as DotIntegrationsDotnamespaceRouteImport } from './integrations.$namespace' |
| 20 | import { Route as DotConnectDotintegrationSlugRouteImport } from './connect.$integrationSlug' | ||
| 20 | import { Route as DotPluginsDotpluginIdDotsplatRouteImport } from './plugins.$pluginId.$' | 21 | import { Route as DotPluginsDotpluginIdDotsplatRouteImport } from './plugins.$pluginId.$' |
| 21 | import { Route as DotIntegrationsDotaddDotpluginKeyRouteImport } from './integrations.add.$pluginKey' | 22 | import { Route as DotIntegrationsDotaddDotpluginKeyRouteImport } from './integrations.add.$pluginKey' |
| 22 | 23 | ||
| @@ -62,6 +63,12 @@ const DotIntegrationsDotnamespaceRoute = · 39 unmodified lines | |||
| 62 | path: '/{-$orgSlug}/integrations/$namespace', | 63 | path: '/{-$orgSlug}/integrations/$namespace', |
| 63 | getParentRoute: () => rootRouteImport, | 64 | getParentRoute: () => rootRouteImport, |
| 64 | } as any) | 65 | } as any) |
| 66 | const DotConnectDotintegrationSlugRoute = | ||
| 67 | DotConnectDotintegrationSlugRouteImport.update({ | ||
| 68 | id: '/{-$orgSlug}/connect/$integrationSlug', | ||
| 69 | path: '/{-$orgSlug}/connect/$integrationSlug', | ||
| 70 | getParentRoute: () => rootRouteImport, | ||
| 71 | } as any) | ||
| 65 | const DotPluginsDotpluginIdDotsplatRoute = | 72 | const DotPluginsDotpluginIdDotsplatRoute = |
| 66 | DotPluginsDotpluginIdDotsplatRouteImport.update({ | 73 | DotPluginsDotpluginIdDotsplatRouteImport.update({ |
| 67 | id: '/{-$orgSlug}/plugins/$pluginId/$', | 74 | id: '/{-$orgSlug}/plugins/$pluginId/$', |
| @@ -81,6 +88,7 @@ export interface FileRoutesByFullPath { · 13 unmodified lines | |||
| 81 | '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren | 88 | '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren |
| 82 | '/{-$orgSlug}/tools': typeof DotToolsRoute | 89 | '/{-$orgSlug}/tools': typeof DotToolsRoute |
| 83 | '/{-$orgSlug}/': typeof DotIndexRoute | 90 | '/{-$orgSlug}/': typeof DotIndexRoute |
| 91 | '/{-$orgSlug}/connect/$integrationSlug': typeof DotConnectDotintegrationSlugRoute | ||
| 84 | '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute | 92 | '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute |
| 85 | '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute | 93 | '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute |
| 86 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute | 94 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute |
| @@ -93,6 +101,7 @@ export interface FileRoutesByTo { · 6 unmodified lines | |||
| 93 | '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren | 101 | '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren |
| 94 | '/{-$orgSlug}/tools': typeof DotToolsRoute | 102 | '/{-$orgSlug}/tools': typeof DotToolsRoute |
| 95 | '/{-$orgSlug}': typeof DotIndexRoute | 103 | '/{-$orgSlug}': typeof DotIndexRoute |
| 104 | '/{-$orgSlug}/connect/$integrationSlug': typeof DotConnectDotintegrationSlugRoute | ||
| 96 | '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute | 105 | '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute |
| 97 | '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute | 106 | '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute |
| 98 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute | 107 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute |
| @@ -106,6 +115,7 @@ export interface FileRoutesById { · 7 unmodified lines | |||
| 106 | '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren | 115 | '/{-$orgSlug}/toolkits': typeof DotToolkitsRouteWithChildren |
| 107 | '/{-$orgSlug}/tools': typeof DotToolsRoute | 116 | '/{-$orgSlug}/tools': typeof DotToolsRoute |
| 108 | '/{-$orgSlug}/': typeof DotIndexRoute | 117 | '/{-$orgSlug}/': typeof DotIndexRoute |
| 118 | '/{-$orgSlug}/connect/$integrationSlug': typeof DotConnectDotintegrationSlugRoute | ||
| 109 | '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute | 119 | '/{-$orgSlug}/integrations/$namespace': typeof DotIntegrationsDotnamespaceRoute |
| 110 | '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute | 120 | '/{-$orgSlug}/resume/$executionId': typeof DotResumeDotexecutionIdRoute |
| 111 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute | 121 | '/{-$orgSlug}/toolkits/$toolkitSlug': typeof DotToolkitsDottoolkitSlugRoute |
| @@ -120,6 +130,7 @@ export interface FileRouteTypes { · 8 unmodified lines | |||
| 120 | | '/{-$orgSlug}/toolkits' | 130 | | '/{-$orgSlug}/toolkits' |
| 121 | | '/{-$orgSlug}/tools' | 131 | | '/{-$orgSlug}/tools' |
| 122 | | '/{-$orgSlug}/' | 132 | | '/{-$orgSlug}/' |
| 133 | | '/{-$orgSlug}/connect/$integrationSlug' | ||
| 123 | | '/{-$orgSlug}/integrations/$namespace' | 134 | | '/{-$orgSlug}/integrations/$namespace' |
| 124 | | '/{-$orgSlug}/resume/$executionId' | 135 | | '/{-$orgSlug}/resume/$executionId' |
| 125 | | '/{-$orgSlug}/toolkits/$toolkitSlug' | 136 | | '/{-$orgSlug}/toolkits/$toolkitSlug' |
| @@ -132,6 +143,7 @@ export interface FileRouteTypes { · 6 unmodified lines | |||
| 132 | | '/{-$orgSlug}/toolkits' | 143 | | '/{-$orgSlug}/toolkits' |
| 133 | | '/{-$orgSlug}/tools' | 144 | | '/{-$orgSlug}/tools' |
| 134 | | '/{-$orgSlug}' | 145 | | '/{-$orgSlug}' |
| 146 | | '/{-$orgSlug}/connect/$integrationSlug' | ||
| 135 | | '/{-$orgSlug}/integrations/$namespace' | 147 | | '/{-$orgSlug}/integrations/$namespace' |
| 136 | | '/{-$orgSlug}/resume/$executionId' | 148 | | '/{-$orgSlug}/resume/$executionId' |
| 137 | | '/{-$orgSlug}/toolkits/$toolkitSlug' | 149 | | '/{-$orgSlug}/toolkits/$toolkitSlug' |
| @@ -144,6 +156,7 @@ export interface FileRouteTypes { · 6 unmodified lines | |||
| 144 | | '/{-$orgSlug}/toolkits' | 156 | | '/{-$orgSlug}/toolkits' |
| 145 | | '/{-$orgSlug}/tools' | 157 | | '/{-$orgSlug}/tools' |
| 146 | | '/{-$orgSlug}/' | 158 | | '/{-$orgSlug}/' |
| 159 | | '/{-$orgSlug}/connect/$integrationSlug' | ||
| 147 | | '/{-$orgSlug}/integrations/$namespace' | 160 | | '/{-$orgSlug}/integrations/$namespace' |
| 148 | | '/{-$orgSlug}/resume/$executionId' | 161 | | '/{-$orgSlug}/resume/$executionId' |
| 149 | | '/{-$orgSlug}/toolkits/$toolkitSlug' | 162 | | '/{-$orgSlug}/toolkits/$toolkitSlug' |
| @@ -157,6 +170,7 @@ export interface RootRouteChildren { · 7 unmodified lines | |||
| 157 | DotToolkitsRoute: typeof DotToolkitsRouteWithChildren | 170 | DotToolkitsRoute: typeof DotToolkitsRouteWithChildren |
| 158 | DotToolsRoute: typeof DotToolsRoute | 171 | DotToolsRoute: typeof DotToolsRoute |
| 159 | DotIndexRoute: typeof DotIndexRoute | 172 | DotIndexRoute: typeof DotIndexRoute |
| 173 | DotConnectDotintegrationSlugRoute: typeof DotConnectDotintegrationSlugRoute | ||
| 160 | DotIntegrationsDotnamespaceRoute: typeof DotIntegrationsDotnamespaceRoute | 174 | DotIntegrationsDotnamespaceRoute: typeof DotIntegrationsDotnamespaceRoute |
| 161 | DotResumeDotexecutionIdRoute: typeof DotResumeDotexecutionIdRoute | 175 | DotResumeDotexecutionIdRoute: typeof DotResumeDotexecutionIdRoute |
| 162 | DotIntegrationsDotaddDotpluginKeyRoute: typeof DotIntegrationsDotaddDotpluginKeyRoute | 176 | DotIntegrationsDotaddDotpluginKeyRoute: typeof DotIntegrationsDotaddDotpluginKeyRoute |
| @@ -221,6 +235,13 @@ declare module '@tanstack/react-router' { · 58 unmodified lines | |||
| 221 | preLoaderRoute: typeof DotIntegrationsDotnamespaceRouteImport | 235 | preLoaderRoute: typeof DotIntegrationsDotnamespaceRouteImport |
| 222 | parentRoute: typeof rootRouteImport | 236 | parentRoute: typeof rootRouteImport |
| 223 | } | 237 | } |
| 238 | '/{-$orgSlug}/connect/$integrationSlug': { | ||
| 239 | id: '/{-$orgSlug}/connect/$integrationSlug' | ||
| 240 | path: '/{-$orgSlug}/connect/$integrationSlug' | ||
| 241 | fullPath: '/{-$orgSlug}/connect/$integrationSlug' | ||
| 242 | preLoaderRoute: typeof DotConnectDotintegrationSlugRouteImport | ||
| 243 | parentRoute: typeof rootRouteImport | ||
| 244 | } | ||
| 224 | '/{-$orgSlug}/plugins/$pluginId/$': { | 245 | '/{-$orgSlug}/plugins/$pluginId/$': { |
| 225 | id: '/{-$orgSlug}/plugins/$pluginId/$' | 246 | id: '/{-$orgSlug}/plugins/$pluginId/$' |
| 226 | path: '/{-$orgSlug}/plugins/$pluginId/$' | 247 | path: '/{-$orgSlug}/plugins/$pluginId/$' |
| @@ -256,6 +277,7 @@ const rootRouteChildren: RootRouteChildren = { · 29 unmodified lines | |||
| 256 | DotToolkitsRoute: DotToolkitsRouteWithChildren, | 277 | DotToolkitsRoute: DotToolkitsRouteWithChildren, |
| 257 | DotToolsRoute: DotToolsRoute, | 278 | DotToolsRoute: DotToolsRoute, |
| 258 | DotIndexRoute: DotIndexRoute, | 279 | DotIndexRoute: DotIndexRoute, |
| 280 | DotConnectDotintegrationSlugRoute: DotConnectDotintegrationSlugRoute, | ||
| 259 | DotIntegrationsDotnamespaceRoute: DotIntegrationsDotnamespaceRoute, | 281 | DotIntegrationsDotnamespaceRoute: DotIntegrationsDotnamespaceRoute, |
| 260 | DotResumeDotexecutionIdRoute: DotResumeDotexecutionIdRoute, | 282 | DotResumeDotexecutionIdRoute: DotResumeDotexecutionIdRoute, |
| 261 | DotIntegrationsDotaddDotpluginKeyRoute: | 283 | DotIntegrationsDotaddDotpluginKeyRoute: |