import type { HttpRequest } from "@yaakapp-internal/models"; import { useAtom } from "jotai"; import { useCallback, useEffect, useMemo } from "react"; import { useLocalStorage } from "react-use"; import { useIntrospectGraphQL } from "../../hooks/useIntrospectGraphQL"; import { useStateWithDeps } from "../../hooks/useStateWithDeps"; import { showDialog } from "../../lib/dialog"; import { Button } from "../core/Button"; import type { DropdownItem } from "../core/Dropdown"; import { Dropdown } from "../core/Dropdown"; import type { EditorProps } from "../core/Editor/Editor"; import { Editor } from "../core/Editor/LazyEditor"; import type { RadioDropdownItem } from "../core/RadioDropdown"; import { RadioDropdown } from "../core/RadioDropdown"; import { Banner, FormattedError, Icon } from "@yaakapp-internal/ui"; import { Separator } from "../core/Separator"; import { tryFormatGraphql } from "../../lib/formatters"; import { parseGraphQLOperationNames } from "../../lib/graphqlOperationNames"; import { normalizeGraphQLBody } from "../../lib/requestBodyConversion"; import { showGraphQLDocExplorerAtom } from "./graphqlAtoms"; type Props = Pick & { baseRequest: HttpRequest; onChange: (body: HttpRequest["body"]) => void; request: HttpRequest; }; const OPERATION_NAME_NOT_SPECIFIED = ""; export function GraphQLEditor(props: Props) { // There's some weirdness with stale onChange being called when switching requests, so we'll // key on the request ID as a workaround for now. return ; } function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProps }: Props) { const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage< Record >("graphQLAutoIntrospectDisabled", {}); const { schema, isLoading, error, refetch, clear } = useIntrospectGraphQL(baseRequest, { disabled: autoIntrospectDisabled?.[baseRequest.id], }); const [currentBody, setCurrentBody] = useStateWithDeps<{ query: string; variables: string | undefined; operationName?: string; }>(() => { // Migrate text bodies to GraphQL format // NOTE: This is how GraphQL used to be stored return normalizeGraphQLBody(request.body); }, [extraEditorProps.forceUpdateKey]); const [isDocOpenRecord, setGraphqlDocStateAtomValue] = useAtom(showGraphQLDocExplorerAtom); const isDocOpen = isDocOpenRecord[request.id] !== undefined; const parsedOperationNames = useMemo( () => parseGraphQLOperationNames(currentBody.query), [currentBody.query], ); const operationNames = useMemo(() => parsedOperationNames ?? [], [parsedOperationNames]); const handleChangeQuery = useCallback( (query: string) => { setCurrentBody(({ variables, operationName }) => { const newBody = buildGraphQLBody({ query, variables, operationName }); onChange(newBody); return newBody; }); }, [onChange, setCurrentBody], ); const handleChangeVariables = useCallback( (variables: string) => { setCurrentBody(({ query, operationName }) => { const newBody = buildGraphQLBody({ query, variables, operationName }); onChange(newBody); return newBody; }); }, [onChange, setCurrentBody], ); const handleChangeOperationName = useCallback( (operationName: string) => { setCurrentBody(({ query, variables }) => { const newBody = buildGraphQLBody({ query, variables, operationName }); onChange(newBody); return newBody; }); }, [onChange, setCurrentBody], ); useEffect(() => { if (parsedOperationNames == null) { return; } if (currentBody.operationName === OPERATION_NAME_NOT_SPECIFIED) { return; } if (currentBody.operationName && operationNames.includes(currentBody.operationName)) { return; } // Keep the saved body aligned with the visible default, so send/copy use the selected operation. const operationName = operationNames[0]; if (currentBody.operationName === operationName) { return; } setCurrentBody(({ query, variables }) => { const newBody = buildGraphQLBody({ query, variables, operationName }); onChange(newBody); return newBody; }); }, [ currentBody.operationName, onChange, operationNames, parsedOperationNames, setCurrentBody, ]); const actions = useMemo( () => [ operationNames.length > 0 ? (
Not specified, value: OPERATION_NAME_NOT_SPECIFIED, }, ...operationNames.map((operationName) => ({ label: operationName, value: operationName, })), ] satisfies RadioDropdownItem[]} >
) : null,
{schema === undefined ? null /* Initializing */ : ( , }, { type: "separator" }, ] : []) satisfies DropdownItem[]), { hidden: !error, label: (

Schema introspection failed

), }); }} > View Error ), type: "content", }, { hidden: schema == null, label: `${isDocOpen ? "Hide" : "Show"} Documentation`, leftSlot: , onSelect: () => { setGraphqlDocStateAtomValue((v) => ({ ...v, [request.id]: isDocOpen ? undefined : null, })); }, }, { label: "Introspect Schema", leftSlot: , keepOpenOnSelect: true, onSelect: refetch, }, { type: "separator", label: "Setting" }, { label: "Automatic Introspection", keepOpenOnSelect: true, onSelect: () => { setAutoIntrospectDisabled({ ...autoIntrospectDisabled, [baseRequest.id]: !autoIntrospectDisabled?.[baseRequest.id], }); }, leftSlot: ( ), }, ]} > )} , ], [ schema, clear, error, currentBody.operationName, handleChangeOperationName, isDocOpen, isLoading, operationNames, refetch, autoIntrospectDisabled, baseRequest.id, setGraphqlDocStateAtomValue, request.id, setAutoIntrospectDisabled, ], ); return (
Variables
); } function buildGraphQLBody(body: { query: string; variables: string | undefined; operationName?: string; }) { const result: { query: string; variables: string | undefined; operationName?: string; } = { query: body.query, variables: body.variables || undefined, }; if (typeof body.operationName === "string") { result.operationName = body.operationName; } return result; }