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);