import React, { useCallback, useState, useMemo, useRef } from 'react'; import { IconChevronRight, IconChevronDown, IconTrash, IconInfoCircle } from '@tabler/icons'; import { nanoid } from 'nanoid'; import { getInputObjectFields } from 'utils/graphql/queryBuilder'; const ListArgValueInput = ({ values, onChange, field, indent }) => { const [items, setItems] = useState(() => { const vals = Array.isArray(values) ? values : (values ? [values] : []); const mapped = vals.map((v) => ({ id: nanoid(), value: v })); return [...mapped, { id: nanoid(), value: '' }]; }); const lastExternalRef = useRef(values); // Sync internal items when values prop changes externally (e.g. editor edits) if (values !== lastExternalRef.current) { lastExternalRef.current = values; const vals = Array.isArray(values) ? values : (values ? [values] : []); const filledValues = items.filter((i) => i.value !== '').map((i) => i.value); if (JSON.stringify(vals) !== JSON.stringify(filledValues)) { const mapped = vals.map((v) => ({ id: nanoid(), value: v })); setItems([...mapped, { id: nanoid(), value: '' }]); } } const handleItemChange = (id, newValue) => { let nextItems = items.map((item) => (item.id === id ? { ...item, value: newValue } : item)); const lastItem = nextItems[nextItems.length - 1]; if (lastItem && lastItem.value !== '') { nextItems = [...nextItems, { id: nanoid(), value: '' }]; } setItems(nextItems); onChange(nextItems.filter((item) => item.value !== '').map((item) => item.value)); }; const handleRemove = (id) => { const nextItems = items.filter((item) => item.id !== id); setItems(nextItems); onChange(nextItems.filter((item) => item.value !== '').map((item) => item.value)); }; return (
{items.map((item, index) => { const isEmptyRow = index === items.length - 1 && item.value === ''; return (
e.stopPropagation()}> handleItemChange(item.id, v)} field={field} /> {isEmptyRow ? ( ) : ( )}
); })}
); }; const ArgValueInput = ({ value, onChange, field }) => { if (field.isEnum && field.enumValues) { return ( ); } if (field.isBoolean) { return ( ); } return ( onChange(e.target.value)} onClick={(e) => e.stopPropagation()} placeholder="Enter value" className="mousetrap" /> ); }; const InputObjectFields = ({ namedType, parentKey, fieldPath, indent, argValues, enabledArgs, onToggleInputField, onSetInputFieldValue }) => { const [expandedFields, setExpandedFields] = useState(new Set()); const fields = useMemo(() => getInputObjectFields(namedType), [namedType]); if (!fields || fields.length === 0) return null; return fields.map((field) => { const fieldKey = `${parentKey}.${field.name}`; const isEnabled = enabledArgs ? enabledArgs.has(fieldKey) : false; const isExpanded = expandedFields.has(field.name); const value = argValues.get(fieldKey) ?? ''; const toggleExpand = (e) => { e.stopPropagation(); setExpandedFields((prev) => { const next = new Set(prev); if (next.has(field.name)) next.delete(field.name); else next.add(field.name); return next; }); }; const isListOfInputObject = field.isList && field.isInputObject; const isExpandable = field.isInputObject && !isListOfInputObject; return (
e.stopPropagation()}> {isExpandable ? ( ) : ( )} { e.stopPropagation(); const willEnable = !isEnabled; onToggleInputField(fieldKey, fieldPath); if (isExpandable && willEnable) { setExpandedFields((prev) => { const next = new Set(prev); next.add(field.name); return next; }); } }} onClick={(e) => e.stopPropagation()} /> {field.name} {field.isRequired && !} {(!isEnabled || field.isInputObject) && {field.typeLabel}} {isListOfInputObject && ( )} {!field.isInputObject && isEnabled && ( onSetInputFieldValue(fieldKey, v)} field={field} /> )}
{isExpandable && isExpanded && ( )}
); }); }; const FieldNode = ({ field, depth, isChecked, isExpanded, onToggleCheck, onToggleExpand, argValues, enabledArgs, onToggleArg, onArgChange, onToggleInputField, onSetInputFieldValue, hasChildren }) => { const indent = depth * 20; const handleCheck = useCallback( (e) => { e.stopPropagation(); onToggleCheck(field.path, field); }, [field, onToggleCheck] ); const hasArgs = field.args && field.args.length > 0; const canExpand = !field.isLeaf || hasArgs; const handleExpand = useCallback( (e) => { e.stopPropagation(); if (canExpand) { onToggleExpand(field.path); } }, [field.path, canExpand, onToggleExpand] ); // Union member type row (e.g. "... on Human") if (field.isUnionMember) { return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleExpand(e); } }} tabIndex={0} > {isExpanded ? ( ) : ( )} e.stopPropagation()} /> ... on {field.name}
); } const showSections = isExpanded && (hasArgs || hasChildren); const sectionIndent = (depth + 1) * 20; return ( <>
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleExpand(e); } }} tabIndex={0} > {canExpand ? ( isExpanded ? ( ) : ( ) ) : null} e.stopPropagation()} /> {field.name} : {field.typeLabel}
{showSections && hasArgs && ( <>
ARGUMENTS
{field.args.map((arg) => { const argKey = `${field.path}.${arg.name}`; const isArgEnabled = enabledArgs ? enabledArgs.has(argKey) : false; const argValue = argValues.get(argKey) ?? ''; // List of input objects: show unsupported message if (arg.isList && arg.isInputObject) { return (
e.stopPropagation()}> onToggleArg && onToggleArg(field.path, arg.name)} onClick={(e) => e.stopPropagation()} /> {arg.name} {arg.isRequired && !} {arg.typeLabel}
); } // Input object arg: render as expandable with children if (arg.isInputObject) { return ( ); } if (arg.isList && !arg.isInputObject) { return ( ); } return (
e.stopPropagation()}> onToggleArg && onToggleArg(field.path, arg.name)} onClick={(e) => e.stopPropagation()} /> {arg.name} {arg.isRequired && !} {!isArgEnabled && {arg.typeLabel}} {isArgEnabled && ( onArgChange(field.path, arg.name, v)} field={arg} /> )}
); })} )} {showSections && hasChildren && hasArgs && (
FIELDS
)} ); }; const InputObjectArgRow = ({ arg, argKey, fieldPath, isArgEnabled, sectionIndent, argValues, enabledArgs, onToggleArg, onToggleInputField, onSetInputFieldValue }) => { const [isExpanded, setIsExpanded] = useState(false); const toggleExpand = (e) => { e.stopPropagation(); setIsExpanded((prev) => !prev); }; const handleCheck = (e) => { e.stopPropagation(); const willEnable = !isArgEnabled; onToggleArg && onToggleArg(fieldPath, arg.name); // Auto-expand when checking only if (willEnable) { setIsExpanded(true); } }; return ( <>
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleExpand(e); } }} tabIndex={0} role="button" aria-expanded={isExpanded} > {isExpanded ? ( ) : ( )} e.stopPropagation()} /> {arg.name} {arg.isRequired && !} {arg.typeLabel}
{isExpanded && arg.namedType && ( )} ); }; const ListArgRow = ({ arg, fieldPath, isArgEnabled, argValue, sectionIndent, onToggleArg, onArgChange }) => { const [isExpanded, setIsExpanded] = useState(false); const toggleExpand = (e) => { e.stopPropagation(); setIsExpanded((prev) => !prev); }; const handleCheck = (e) => { e.stopPropagation(); const willEnable = !isArgEnabled; onToggleArg && onToggleArg(fieldPath, arg.name); if (willEnable) { setIsExpanded(true); } }; return ( <>
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleExpand(e); } }} tabIndex={0} role="button" aria-expanded={isExpanded} > {isExpanded ? ( ) : ( )} e.stopPropagation()} /> {arg.name} {arg.isRequired && !} {arg.typeLabel}
{isExpanded && ( onArgChange(fieldPath, arg.name, v)} field={arg} indent={sectionIndent + 28} /> )} ); }; export default React.memo(FieldNode);