Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell. | 1 | /** |
| 2 | * What Customize's pages do on the server besides reading: the connector | |
| 3 | * actions every page with a connector shares (signing in and out, a key, | |
| 4 | * listing tools again, where it reaches, removing it), and what follows | |
| 5 | * adding one: the browser goes to sign in, or to the connector's page. | |
| 6 | */ | |
| 7 | import { type ShouldRevalidateFunctionArgs, redirect } from "react-router"; | |
| 8 | ||
| 9 | import type { ConnectorInput, User, WorkspaceConnector } from "@g1t/contracts"; | |
| 10 | ||
| 11 | import { addresses } from "./addresses.server"; | |
| 12 | import { connectorCallbackPath, connectorPagePath, needsReload, splitTarget } from "./customize"; | |
| 13 | import { workspaceAgents } from "./services.server"; | |
| 14 | import type { ActionResult } from "../components/agents/dialogs"; | |
| 15 | import { answer } from "../components/agents/actions.server"; | |
| 16 | ||
| 17 | /** Where a connector's sign-in comes back to: this g1t's own page. */ | |
| 18 | export function callbackUri(slug: string): string { | |
| 19 | return `${addresses().site}${connectorCallbackPath(slug)}`; | |
| 20 | } | |
| 21 | ||
| 22 | /** A Customize page reads its data again only for another page, the other view, or a posted form (lib/customize.ts). */ | |
| 23 | export function shouldRevalidate({ currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs): boolean { | |
| 24 | return needsReload(currentUrl, nextUrl, formMethod) ? defaultShouldRevalidate : false; | |
| 25 | } | |
| 26 | ||
| 27 | /** The browser leaves for the connector's sign-in page, or lands on the connector's own page with what happened. */ | |
| 28 | async function toSignIn(slug: string, viewer: User, id: string): Promise<Response> { | |
| 29 | const started = await workspaceAgents.startConnectorSignIn(slug, viewer, id, callbackUri(slug)).catch(() => null); | |
| 30 | if (started?.ok) return redirect(started.value.url); | |
| 31 | const problem = started ? started.error.message : "The agents service didn't answer. Try again in a moment."; | |
| 32 | return redirect(`${connectorPagePath(slug, id)}?problem=${encodeURIComponent(problem)}`); | |
| 33 | } | |
| 34 | ||
| 35 | /** | |
| 36 | * Adds a connector and takes the browser where it goes next: to sign in | |
| 37 | * when the server asks for that, or to the connector's page otherwise. An | |
| 38 | * answer (not a redirect) says why it couldn't be added. | |
| 39 | */ | |
| 40 | export async function addConnectorThen(slug: string, viewer: User, input: ConnectorInput): Promise<Response | ActionResult> { | |
| 41 | const added = await workspaceAgents.addConnector(slug, viewer, input).catch(() => null); | |
| 42 | if (!added) return { ok: false, intent: "connect", error: "The agents service didn't answer. Try again in a moment." }; | |
| 43 | if (!added.ok) return { ok: false, intent: "connect", error: added.error.message, code: added.error.code }; | |
| 44 | return afterAdd(slug, viewer, added.value); | |
| 45 | } | |
| 46 | ||
| 47 | /** After a connector is added: the sign-in when it needs one, its page otherwise. */ | |
| 48 | export function afterAdd(slug: string, viewer: User, connector: WorkspaceConnector): Promise<Response> | Response { | |
| 49 | if (connector.state === "needs_sign_in") return toSignIn(slug, viewer, connector.id); | |
| 50 | return redirect(`${connectorPagePath(slug, connector.id)}?added=1`); | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * The actions a connector has wherever it is shown. `intent` names one; | |
| 55 | * null says it isn't one of them, so the page's own action goes on. | |
| 56 | * `afterRemove` is where the browser goes once it is removed. | |
| 57 | */ | |
| 58 | export async function connectorAction(intent: string, form: FormData, slug: string, viewer: User, afterRemove: string | null): Promise<Response | ActionResult | null> { | |
| 59 | const id = String(form.get("connector") ?? ""); | |
| 60 | switch (intent) { | |
| 61 | case "sign_in": | |
| 62 | return toSignIn(slug, viewer, id); | |
| 63 | case "sign_out": | |
| 64 | return answer(intent, workspaceAgents.signOutConnector(slug, viewer, id)); | |
| 65 | case "key": | |
| 66 | return answer(intent, workspaceAgents.setConnectorKey(slug, viewer, id, String(form.get("header_name") ?? "") || null, String(form.get("value") ?? ""))); | |
| 67 | case "refresh": | |
| 68 | return answer(intent, workspaceAgents.refreshConnector(slug, viewer, id)); | |
| 69 | case "grant": { | |
| 70 | const { scope, target } = splitTarget(String(form.get("where") ?? "")); | |
| 71 | return answer(intent, workspaceAgents.grantConnector(slug, viewer, id, scope, target)); | |
| 72 | } | |
| 73 | case "revoke": | |
| 74 | return answer(intent, workspaceAgents.revokeConnector(slug, viewer, id, String(form.get("grant") ?? ""))); | |
| 75 | case "remove": { | |
| 76 | const result = await answer(intent, workspaceAgents.removeConnector(slug, viewer, id)); | |
| 77 | return result.ok && afterRemove ? redirect(afterRemove) : result; | |
| 78 | } | |
| 79 | default: | |
| 80 | return null; | |
| 81 | } | |
| 82 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.