import type { Color } from "@yaakapp-internal/plugins"; import classNames from "classnames"; import { fuzzyMatch } from "fuzzbunny"; import type { GraphQLField, GraphQLInputField, GraphQLNamedType, GraphQLSchema, GraphQLType, } from "graphql"; import { getNamedType, isEnumType, isInputObjectType, isInterfaceType, isListType, isNamedType, isNonNullType, isObjectType, isScalarType, isUnionType, } from "graphql"; import { useAtomValue } from "jotai"; import type { CSSProperties, HTMLAttributes, KeyboardEvent, ReactNode } from "react"; import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useClickOutside } from "../../hooks/useClickOutside"; import { useContainerSize } from "../../hooks/useContainerQuery"; import { useDebouncedValue } from "../../hooks/useDebouncedValue"; import { useStateWithDeps } from "../../hooks/useStateWithDeps"; import { jotaiStore } from "../../lib/jotai"; import { Banner } from "../core/Banner"; import { CountBadge } from "../core/CountBadge"; import { Icon } from "../core/Icon"; import { IconButton } from "../core/IconButton"; import { PlainInput } from "../core/PlainInput"; import { Markdown } from "../Markdown"; import { showGraphQLDocExplorerAtom } from "./graphqlAtoms"; interface Props { style?: CSSProperties; schema: GraphQLSchema; requestId: string; className?: string; } type ExplorerItem = | { kind: "type"; type: GraphQLType; from: ExplorerItem } // oxlint-disable-next-line no-explicit-any | { kind: "field"; type: GraphQLField; from: ExplorerItem } | { kind: "input_field"; type: GraphQLInputField; from: ExplorerItem } | null; export const GraphQLDocsExplorer = memo(function GraphQLDocsExplorer({ style, schema, requestId, className, }: Props) { const [activeItem, setActiveItem] = useState(null); const qryType = schema.getQueryType(); const mutType = schema.getMutationType(); const subType = schema.getSubscriptionType(); const showField = useAtomValue(showGraphQLDocExplorerAtom)[requestId] ?? null; useEffect(() => { if (showField === null) { setActiveItem(null); } else { const isRootParentType = showField.parentType === "Query" || showField.parentType === "Mutation" || showField.parentType === "Subscription"; walkTypeGraph(schema, null, (t, from) => { if ( showField.field === t.name && // For input fields, CodeMirror seems to set parentType to the root type of the field they belong to. (isRootParentType || from?.name === showField.parentType) ) { setActiveItem(toExplorerItem(t, toExplorerItem(from, null))); return false; } if (showField.type === t.name && from?.name === showField.parentType) { setActiveItem(toExplorerItem(t, toExplorerItem(from, null))); return false; } return true; }); } }, [schema, showField]); const qryItem: ExplorerItem = qryType ? { kind: "type", type: qryType, from: null } : null; const mutItem: ExplorerItem = mutType ? { kind: "type", type: mutType, from: null } : null; const subItem: ExplorerItem = subType ? { kind: "type", type: subType, from: null } : null; const allTypes = schema.getTypeMap(); const containerRef = useRef(null); const containerSize = useContainerSize(containerRef); return (
{ jotaiStore.set(showGraphQLDocExplorerAtom, (v) => ({ ...v, [requestId]: undefined })); }} setItem={setActiveItem} schema={schema} /> {activeItem == null ? (
Root Types All Schema Types {schema.description ?? null}
{Object.values(allTypes).map((t) => { return ( ); })}
) : (
)}
); }); function GraphQLExplorerHeader({ item, setItem, schema, onClose, containerHeight, }: { item: ExplorerItem; setItem: (t: ExplorerItem) => void; schema: GraphQLSchema; onClose: () => void; containerHeight: number; }) { const findIt = (t: ExplorerItem): ExplorerItem[] => { if (t == null) return [null]; return [...findIt(t.from), t]; }; const crumbs = findIt(item); return ( ); } function GqlTypeInfo({ item, setItem, schema, }: { item: ExplorerItem | null; setItem: (t: ExplorerItem) => void; schema: GraphQLSchema; }) { if (item == null) return null; const description = item.kind === "type" ? getNamedType(item.type).description : item.type.description; const heading = (
{description || "No description"} {"deprecationReason" in item.type && item.type.deprecationReason && ( {item.type.deprecationReason} )}
); if (isScalarType(item.type)) { return heading; } if (isNonNullType(item.type) || isListType(item.type)) { // kinda a hack, but we'll just unwrap there and show the named type return ( ); } if (isInterfaceType(item.type)) { const fields = item.type.getFields(); const possibleTypes = schema.getPossibleTypes(item.type) ?? []; return (
{heading} Fields {Object.entries(fields).map(([fieldName, field]) => { const fieldItem: ExplorerItem = toExplorerItem(field, item); return (
); })} {possibleTypes.length > 0 && ( <> Implemented By {possibleTypes.map((t) => ( ))} )}
); } if (isUnionType(item.type)) { const types = item.type.getTypes(); return (
{heading} Possible Types {types.map((t) => ( ))}
); } if (isEnumType(item.type)) { const values = item.type.getValues(); return (
{heading} Values {values.map((v) => (
{v.value} {v.description ?? null}
))}
); } if (item.kind === "input_field") { return (
{heading} {item.type.defaultValue !== undefined && (
Default Value
{JSON.stringify(item.type.defaultValue)}
)}
Type
); } if (item.kind === "field") { return (
{heading}
Type
{item.type.args.length > 0 && (
Arguments {item.type.args.map((a) => { return (
); })}
)}
); } if (isInputObjectType(item.type)) { const fields = item.type.getFields(); return (
{heading} Fields {Object.keys(fields).map((fieldName) => { const field = fields[fieldName]; if (field == null) return null; const fieldItem: ExplorerItem = { kind: "input_field", type: field, from: item, }; return (
); })}
); } if (isObjectType(item.type)) { const fields = item.type.getFields(); const interfaces = item.type.getInterfaces(); return (
{heading} {interfaces.length > 0 && ( <> Implements {interfaces.map((i) => ( ))} )} Fields {Object.keys(fields).map((fieldName) => { const field = fields[fieldName]; if (field == null) return null; const fieldItem: ExplorerItem = { kind: "field", type: field, from: item }; return (
); })}
); } console.log("Unknown GraphQL Type", item); return
Unknown GraphQL type
; } function GqlTypeRow({ item, setItem, name, description, className, hideDescription, }: { item: ExplorerItem; name?: { value: string; color: Color }; description?: string | null; setItem: (t: ExplorerItem) => void; className?: string; hideDescription?: boolean; }) { if (item == null) return null; let child: ReactNode = <>Unknown Type; if (item.kind === "type") { child = ( <>
{name && ( {name.value}:  )}
{!hideDescription && ( {(description === undefined ? getNamedType(item.type).description : description) ?? null} )} ); } else if (item.kind === "field") { const returnItem: ExplorerItem = { kind: "type", type: item.type.type, from: item.from, }; child = (
{name?.value} {item.type.args.length > 0 && ( <> ( {item.type.args.map((arg) => (
{item.type.args.length > 1 && <>  } {arg.name}: 
))} ) )} :{" "}
{item.type.description ?? null}
); } else if (item.kind === "input_field") { child = ( <>
{name && {name.value}:}{" "}
{item.type.description ?? null} ); } return
{child}
; } function GqlTypeLink({ item, setItem, color, children, leftSlot, rightSlot, onNavigate, className, noTruncate, }: { item: ExplorerItem; color?: Color; setItem: (item: ExplorerItem) => void; onNavigate?: () => void; children?: ReactNode; leftSlot?: ReactNode; rightSlot?: ReactNode; className?: string; noTruncate?: boolean; }) { if (item?.kind === "type" && isListType(item.type)) { return ( [ {children} ] ); } if (item?.kind === "type" && isNonNullType(item.type)) { return ( {children} ! ); } return ( ); } function GqlTypeLabel({ item, children, className, noTruncate, }: { item: ExplorerItem; children?: ReactNode; className?: string; noTruncate?: boolean; }) { let inner: ReactNode; if (children) { inner = children; } else if (item == null) { inner = "Root"; } else if (item.kind === "field") { inner = item.type.name + (item.type.args.length > 0 ? "(…)" : ""); } else if ("name" in item.type) { inner = item.type.name; } else { console.error("Unknown item type", item); inner = "UNKNOWN"; } return {inner}; } function Subheading({ children, count }: { children: ReactNode; count?: number }) { return (

{children}
{count && }

); } interface SearchResult { name: string; // oxlint-disable-next-line no-explicit-any type: GraphQLNamedType | GraphQLField | GraphQLInputField; score: number; from: GraphQLNamedType | null; depth: string[]; } function GqlSchemaSearch({ schema, currentItem, setItem, className, maxHeight, }: { currentItem: ExplorerItem | null; schema: GraphQLSchema; setItem: (t: ExplorerItem) => void; className?: string; maxHeight: number; }) { const [activeResult, setActiveResult] = useStateWithDeps(null, [ currentItem, ]); const [focused, setOpen] = useState(false); const [value, setValue] = useState(""); const debouncedValue = useDebouncedValue(value, 300); const menuRef = useRef(null); const canSearch = currentItem == null || (isNamedType(currentItem.type) && !isEnumType(currentItem.type) && !isScalarType(currentItem.type)); const results = useMemo(() => { const results: SearchResult[] = []; walkTypeGraph(schema, currentItem?.type ?? null, (type, from, depth) => { if (type === currentItem?.type) { return true; // Skip the current type and continue } const match = fuzzyMatch(type.name, debouncedValue); if (match == null) { // Do nothing } else { results.push({ name: type.name, type, score: match.score, from, depth }); } return true; }); results.sort((a, b) => { if (value === "") { if (a.name.startsWith("_") && !b.name.startsWith("_")) { // Always sort __ types to the end when there is no query return 1; } if (a.depth.length !== b.depth.length) { return a.depth.length - b.depth.length; } return a.name.localeCompare(b.name); } if (a.depth.length !== b.depth.length) { return a.depth.length - b.depth.length; } if (a.score === 0 && b.score === 0) { return a.name.localeCompare(b.name); } if (a.score === b.score && a.name.length === b.name.length) { return a.name.localeCompare(b.name); } if (a.score === b.score) { return a.name.length - b.type.name.length; } return b.score - a.score; }); return results.slice(0, 100); }, [currentItem, schema, debouncedValue, value]); const activeIndex = useMemo(() => { const index = (activeResult ? results.indexOf(activeResult) : 0) ?? 0; return index === -1 ? 0 : index; }, [activeResult, results]); const inputRef = useRef(null); useClickOutside(menuRef, () => setOpen(false)); const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === "ArrowDown" || (e.ctrlKey && e.key === "n")) { e.preventDefault(); const next = results[activeIndex + 1] ?? results[results.length - 1] ?? null; setActiveResult(next); } else if (e.key === "ArrowUp" || (e.ctrlKey && e.key === "k")) { e.preventDefault(); const prev = results[activeIndex - 1] ?? results[0] ?? null; setActiveResult(prev); } else if (e.key === "Escape") { inputRef.current?.blur(); } else if (e.key === "Enter") { const result = activeResult ?? results[0] ?? null; if (result) { setItem(toExplorerItem(result?.type, currentItem)); inputRef.current?.blur(); } } }, [results, activeIndex, setActiveResult, activeResult, setItem, currentItem], ); if (!canSearch) return ; return (
} onChange={setValue} onKeyDownCapture={handleKeyDown} onFocus={() => { setOpen(true); }} />
{results.length === 0 && ( No results found )} {results.map((r, i) => { const item = toExplorerItem(r.type, currentItem); if (item === currentItem) return null; return ( { setItem(item); setOpen(false); }} onMouseEnter={() => setActiveResult(r)} isActive={i === activeIndex} > {r.from !== currentItem?.type && r.from != null && ( <> . )} ); })}
); } function SearchResult({ isActive, className, ...extraProps }: { isActive: boolean; children: ReactNode; } & HTMLAttributes) { const initRef = useCallback( (el: HTMLButtonElement | null) => { if (el === null) return; if (isActive) { el.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" }); } }, [isActive], ); return (