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 } // biome-ignore lint/suspicious/noExplicitAny: none | { 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; // biome-ignore lint/suspicious/noExplicitAny: none 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 (