import { useCallback, useMemo, useState } from "react"; import { useAtomSet } from "@effect/atom-react"; import * as Exit from "effect/Exit"; import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { integrationDisplayNameFromUrl, slugifyNamespace, useIntegrationIdentity, } from "@executor-js/react/plugins/integration-identity"; import { Button } from "@executor-js/react/components/button"; import { AuthMethodListEditor, useAuthMethodList, type AuthMethodRow, type AuthMethodSeed, } from "@executor-js/react/components/auth-method-list-editor"; import { FloatActions } from "@executor-js/react/components/float-actions"; import { addIntegrationErrorMessage, FormErrorAlert, SlugCollisionAlert, useSlugAlreadyExists, } from "@executor-js/react/lib/integration-add"; import { createGraphqlIntegrationOptimistic } from "./atoms"; import { GraphqlIntegrationFields } from "./GraphqlIntegrationFields"; import { graphqlAuthMethodInputsFromPlacements } from "./auth-method-config"; import type { GraphqlAuthMethodInput } from "../sdk/types"; // v2 GraphQL add flow: register the integration with its declared auth-method // LIST (the shared `AuthMethodListEditor` — GraphQL stays header/query apiKey; // OAuth is hidden), then route to the integration's detail hub. Connection // creation is no longer part of the add flow — accounts are added from the hub // (P6: add without auth, connect later). // GraphQL has no add-time detection, so the list starts empty (module constant // — a fresh [] every render would re-seed the list each render). const NO_SEEDS: readonly AuthMethodSeed[] = []; export default function AddGraphqlIntegration(props: { onComplete: (slug?: string) => void; onCancel: () => void; initialUrl?: string; }) { const [endpoint, setEndpoint] = useState(props.initialUrl ?? ""); const [description, setDescription] = useState(""); const identity = useIntegrationIdentity({ fallbackName: integrationDisplayNameFromUrl(endpoint, "GraphQL") ?? "", }); const authMethodList = useAuthMethodList(NO_SEEDS); const [adding, setAdding] = useState(false); const [addError, setAddError] = useState(null); const doAddIntegration = useAtomSet(createGraphqlIntegrationOptimistic, { mode: "promiseExit", }); // The methods to register: each apikey row declares ONE method carrying // every named placement (header + query mix in a single method). Inputs // omit slugs — the backend assigns carrier-derived ones. `none` rows // register nothing. const authenticationTemplate = useMemo( () => authMethodList.rows.flatMap((row: AuthMethodRow) => row.value.kind === "apikey" ? graphqlAuthMethodInputsFromPlacements(row.value.placements) : [], ), [authMethodList.rows], ); // Every apikey row needs at least one named placement; `none` rows are // always valid. const apiKeyComplete = authMethodList.rows.every( (row: AuthMethodRow) => row.value.kind !== "apikey" || row.value.placements.some((placement) => placement.name.trim().length > 0), ); const resolvedSlug = useMemo( () => slugifyNamespace(identity.namespace) || slugifyNamespace(integrationDisplayNameFromUrl(endpoint.trim(), "GraphQL") ?? "") || "graphql", [endpoint, identity.namespace], ); // Pre-empt the API's `IntegrationAlreadyExistsError`: adding an integration // whose slug already exists clobbers the existing one's connections/policies, // so the API blocks it. Surface that here from the tenant-scoped catalog list. const slugAlreadyExists = useSlugAlreadyExists(resolvedSlug); const canAdd = endpoint.trim().length > 0 && apiKeyComplete && !adding && !slugAlreadyExists; const integrationIdentity = useCallback(() => { const trimmedEndpoint = endpoint.trim(); const slug = resolvedSlug; const displayName = identity.name.trim() || integrationDisplayNameFromUrl(trimmedEndpoint, "GraphQL") || slug; return { trimmedEndpoint, slug, displayName }; }, [endpoint, identity.name, resolvedSlug]); const handleAdd = async (): Promise => { setAdding(true); setAddError(null); const { trimmedEndpoint, slug, displayName } = integrationIdentity(); const integrationExit = await doAddIntegration({ payload: { endpoint: trimmedEndpoint, slug, name: displayName, ...(description.trim().length > 0 ? { description: description.trim() } : {}), ...(authenticationTemplate.length > 0 ? { authenticationTemplate: [...authenticationTemplate] } : {}), }, reactivityKeys: integrationWriteKeys, }); if (Exit.isFailure(integrationExit)) { setAddError(addIntegrationErrorMessage(integrationExit, slug, "Failed to add integration")); setAdding(false); return; } const registeredSlug = integrationExit.value.slug; props.onComplete(String(registeredSlug)); }; return (

Add GraphQL integration

{slugAlreadyExists && !adding && } {addError && }
); }