Skip to content

Instantly share code, notes, and snippets.

@omavi
Forked from baohouse/GraphiQLExplorer.tsx
Created January 21, 2021 22:08
Show Gist options
  • Save omavi/9430671d1202b836d875a4ebe68db607 to your computer and use it in GitHub Desktop.
Save omavi/9430671d1202b836d875a4ebe68db607 to your computer and use it in GitHub Desktop.
Fork of graphiql-explorer 0.4.5 to support list types and object-based custom args
/**
* Copied from https://github.com/OneGraph/graphiql-explorer (0.4.5)
* We fork because we need to support customizable fields and could not wait for
* the PR process to finish. Also converted Flow type to TypeScript.
*/
import { Tooltip } from 'antd';
import prettier from 'prettier/standalone';
import parserGraphql from 'prettier/parser-graphql';
import React, { Fragment } from 'react';
import {
getNamedType,
isEnumType,
isInputObjectType,
isInterfaceType,
isLeafType,
isListType,
isNonNullType,
isObjectType,
isRequiredInputField,
isScalarType,
isUnionType,
isWrappingType,
parse,
print,
} from 'graphql';
import {
ArgumentNode,
DocumentNode,
FieldNode,
GraphQLArgument,
GraphQLEnumType,
GraphQLField,
GraphQLFieldMap,
GraphQLInputField,
GraphQLInputType,
GraphQLObjectType,
GraphQLOutputType,
GraphQLScalarType,
GraphQLSchema,
InlineFragmentNode,
FragmentDefinitionNode,
ListValueNode,
NameNode,
OperationDefinitionNode,
ObjectFieldNode,
ObjectValueNode,
SelectionNode,
SelectionSetNode,
ValueNode,
} from 'graphql';
type Field = GraphQLField<any, any>;
type GetDefaultScalarArgValue = (
parentField: Field,
arg: GraphQLArgument | GraphQLInputField,
underlyingArgType: GraphQLEnumType | GraphQLScalarType,
directParentArg?: GraphQLArgument | GraphQLInputField
) => ValueNode;
type GetScalarArgInput = (
parentField: Field,
arg: GraphQLArgument | GraphQLInputField,
underlyingArgType: GraphQLEnumType | GraphQLScalarType,
argValue: ValueNode,
onChange: (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void,
styleConfig: StyleConfig,
directParentArg?: GraphQLArgument | GraphQLInputField
) => JSX.Element | void;
type MakeDefaultArg = (
parentField: Field,
arg: GraphQLArgument | GraphQLInputField,
directParentArg?: GraphQLArgument | GraphQLInputField
) => boolean;
type Colors = {
keyword: string;
def: string;
property: string;
qualifier: string;
attribute: string;
number: string;
string: string;
builtin: string;
string2: string;
variable: string;
atom: string;
};
type Styles = {
explorerActionsStyle: React.CSSProperties;
buttonStyle: React.CSSProperties;
};
type StyleConfig = {
colors: Colors;
arrowOpen: React.ReactNode;
arrowClosed: React.ReactNode;
checkboxChecked: React.ReactNode;
checkboxUnchecked: React.ReactNode;
styles: Styles;
};
type Props = {
query: string;
width?: number;
title?: string;
schema?: GraphQLSchema;
onEdit: (x: string) => void;
getDefaultFieldNames?: (type: GraphQLObjectType) => Array<string>;
getDefaultScalarArgValue?: GetDefaultScalarArgValue;
getScalarArgInput: GetScalarArgInput;
makeDefaultArg?: MakeDefaultArg;
onToggleExplorer: () => void;
explorerIsOpen: boolean;
onRunOperation?: (name?: string) => void;
colors?: Colors;
arrowOpen?: React.ReactNode;
arrowClosed?: React.ReactNode;
checkboxChecked?: React.ReactNode;
checkboxUnchecked?: React.ReactNode;
styles?: {
explorerActionsStyle?: React.CSSProperties;
buttonStyle?: React.CSSProperties;
};
};
type State = {
operation: OperationDefinitionNode;
};
type Selections = readonly SelectionNode[];
function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// Names match class names in graphiql app.css
// https://github.com/graphql/graphiql/blob/master/packages/graphiql/css/app.css
const defaultColors: Colors = {
keyword: '#B11A04',
// OperationName, FragmentName
def: '#D2054E',
// FieldName
property: '#1F61A0',
// FieldAlias
qualifier: '#1C92A9',
// ArgumentName and ObjectFieldName
attribute: '#8B2BB9',
number: '#2882F9',
string: '#D64292',
// Boolean
builtin: '#D47509',
// Enum
string2: '#0B7FC7',
variable: '#397D13',
// Type
atom: '#CA9800',
};
const defaultArrowOpen = (
<svg width="12" height="9" style={{ marginLeft: -14, marginRight: 2 }}>
<path fill="#666" d="M 0 2 L 9 2 L 4.5 7.5 z" />
</svg>
);
const defaultArrowClosed = (
<svg width="12" height="9" style={{ marginLeft: -12, marginRight: 0 }}>
<path fill="#666" d="M 0 0 L 0 9 L 5.5 4.5 z" />
</svg>
);
const defaultCheckboxChecked = (
<svg
style={{ marginRight: 4, marginLeft: 0, verticalAlign: -2 }}
width="12"
height="12"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 0H2C0.9 0 0 0.9 0 2V16C0 17.1 0.9 18 2 18H16C17.1 18 18 17.1 18 16V2C18 0.9 17.1 0 16 0ZM16 16H2V2H16V16ZM14.99 6L13.58 4.58L6.99 11.17L4.41 8.6L2.99 10.01L6.99 14L14.99 6Z"
fill="#666"
/>
</svg>
);
const defaultCheckboxUnchecked = (
<svg
style={{ marginRight: 4, marginLeft: 0, verticalAlign: -2 }}
width="12"
height="12"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 2V16H2V2H16ZM16 0H2C0.9 0 0 0.9 0 2V16C0 17.1 0.9 18 2 18H16C17.1 18 18 17.1 18 16V2C18 0.9 17.1 0 16 0Z"
fill="#CCC"
/>
</svg>
);
function Checkbox(props: { checked: boolean; styleConfig: StyleConfig }) {
return (
<Fragment>
{props.checked ? props.styleConfig.checkboxChecked : props.styleConfig.checkboxUnchecked}
</Fragment>
);
}
function defaultGetDefaultFieldNames(type: GraphQLObjectType): Array<string> {
const fields = type.getFields();
// Is there an `id` field?
if (fields['id']) {
const res = ['id'];
if (fields['email']) {
res.push('email');
} else if (fields['name']) {
res.push('name');
}
return res;
}
// Is there an `edges` field?
if (fields['edges']) {
return ['edges'];
}
// Is there an `node` field?
if (fields['node']) {
return ['node'];
}
if (fields['nodes']) {
return ['nodes'];
}
// Include all leaf-type fields.
const leafFieldNames: any[] = [];
Object.keys(fields).forEach(fieldName => {
if (isLeafType(fields[fieldName].type)) {
leafFieldNames.push(fieldName);
}
});
if (!leafFieldNames.length) {
// No leaf fields, add typename so that the query stays valid
return ['__typename'];
}
return leafFieldNames.slice(0, 2); // Prevent too many fields from being added
}
function isListArgument(arg: GraphQLArgument): boolean {
let unwrappedType = arg.type;
while (isNonNullType(unwrappedType)) {
unwrappedType = unwrappedType.ofType;
}
return isListType(unwrappedType);
}
function isRequiredArgument(arg: GraphQLArgument): boolean {
return isNonNullType(arg.type) && arg.defaultValue === undefined;
}
function unwrapOutputType(outputType: GraphQLOutputType): any {
let unwrappedType = outputType;
while (isWrappingType(unwrappedType)) {
unwrappedType = unwrappedType.ofType;
}
return unwrappedType;
}
function unwrapInputType(inputType: GraphQLInputType): any {
let unwrappedType = inputType;
while (isWrappingType(unwrappedType)) {
unwrappedType = unwrappedType.ofType;
}
return unwrappedType;
}
function coerceArgValue(argType: GraphQLScalarType | GraphQLEnumType, value: string): ValueNode {
if (isScalarType(argType)) {
try {
switch (argType.name) {
case 'String':
return {
kind: 'StringValue',
value: String(argType.parseValue(value)),
};
case 'Float':
return {
kind: 'FloatValue',
value: String(argType.parseValue(parseFloat(value))),
};
case 'Int':
return {
kind: 'IntValue',
value: String(argType.parseValue(parseInt(value, 10))),
};
case 'Boolean':
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'boolean') {
return { kind: 'BooleanValue', value: parsed };
} else {
return { kind: 'BooleanValue', value: false };
}
} catch (e) {
return {
kind: 'BooleanValue',
value: false,
};
}
default:
return {
kind: 'StringValue',
value: String(argType.parseValue(value)),
};
}
} catch (e) {
console.error('error coercing arg value', e, value);
return { kind: 'StringValue', value: value };
}
} else {
try {
const parsedValue = argType.parseValue(value);
if (parsedValue) {
return { kind: 'EnumValue', value: String(parsedValue) };
} else {
return { kind: 'EnumValue', value: argType.getValues()[0].name };
}
} catch (e) {
return { kind: 'EnumValue', value: argType.getValues()[0].name };
}
}
}
const prettify = (query: string): string =>
prettier.format(query, {
parser: 'graphql',
plugins: [parserGraphql],
});
type InputArgViewProps = {
arg: GraphQLArgument;
argName?: String;
selection: ObjectValueNode;
parentField: Field;
directParentArg: GraphQLArgument;
modifyFields: (fields: readonly ObjectFieldNode[]) => void;
getDefaultScalarArgValue: GetDefaultScalarArgValue;
getScalarArgInput: GetScalarArgInput;
makeDefaultArg?: MakeDefaultArg;
onRunOperation: (e: React.KeyboardEvent<HTMLInputElement | HTMLSelectElement>) => void;
styleConfig: StyleConfig;
};
class InputArgView extends React.PureComponent<InputArgViewProps, {}> {
_previousArgSelection?: ObjectFieldNode;
_getArgSelection = () => {
return this.props.selection.fields.find(field => field.name.value === this.props.arg.name);
};
_removeArg = () => {
const { selection } = this.props;
const argSelection = this._getArgSelection();
this._previousArgSelection = argSelection;
this.props.modifyFields(selection.fields.filter(field => field !== argSelection));
};
_addArg = () => {
const {
arg,
directParentArg,
getDefaultScalarArgValue,
makeDefaultArg,
modifyFields,
parentField,
selection,
} = this.props;
const argType = unwrapInputType(arg.type);
if (isListArgument(arg)) {
const fields = isLeafType(argType) ? {} : argType.getFields();
const currentArgSelection = this._getArgSelection();
const newValue = isLeafType(argType)
? getDefaultScalarArgValue(parentField, arg, argType)
: {
kind: 'ObjectValue',
fields: defaultInputObjectFields(
getDefaultScalarArgValue,
makeDefaultArg || null,
parentField,
Object.keys(fields).map(k => fields[k]),
arg
),
};
const argSelection = {
kind: 'ObjectField',
name: { kind: 'Name', value: arg.name },
value: {
kind: 'ListValue',
values: [
...(currentArgSelection && currentArgSelection.value
? (currentArgSelection.value as ListValueNode).values
: []),
newValue,
],
},
} as ObjectFieldNode;
modifyFields([
...(selection.fields || []).filter(({ name }) => name.value !== arg.name),
argSelection,
]);
} else {
let argSelection = null;
if (this._previousArgSelection) {
argSelection = this._previousArgSelection;
} else if (isInputObjectType(argType)) {
const fields = argType.getFields();
argSelection = {
kind: 'ObjectField',
name: { kind: 'Name', value: arg.name },
value: {
kind: 'ObjectValue',
fields: defaultInputObjectFields(
getDefaultScalarArgValue,
makeDefaultArg || null,
parentField,
Object.keys(fields).map(k => fields[k]),
arg
),
},
} as ObjectFieldNode;
} else if (isLeafType(argType)) {
argSelection = {
kind: 'ObjectField',
name: { kind: 'Name', value: arg.name },
value: getDefaultScalarArgValue(parentField, arg, argType, directParentArg),
} as ObjectFieldNode;
}
if (!argSelection) {
console.error('Unable to add arg for argType', argType);
} else {
modifyFields([...(selection.fields || []), argSelection]);
}
}
};
_setArgValue = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { selection } = this.props;
const argSelection = this._getArgSelection();
if (!argSelection) {
console.error('missing arg selection when setting arg value');
return;
}
const argType = unwrapInputType(this.props.arg.type);
if (!isLeafType(argType)) {
console.warn('Unable to handle non leaf types in setArgValue');
return;
}
const targetValue = event.target.value;
this.props.modifyFields(
(selection.fields || []).map(field =>
field === argSelection
? {
...field,
value: coerceArgValue(argType, targetValue),
}
: field
)
);
};
_modifyChildFields = (fields: ObjectFieldNode[] | ListValueNode) => {
const { modifyFields, selection } = this.props;
if ((fields as ListValueNode).kind === 'ListValue') {
modifyFields(
selection.fields.map(field =>
field.name.value === this.props.arg.name
? {
...field,
value: fields as ListValueNode,
}
: field
)
);
} else {
modifyFields(
selection.fields.map(field =>
field.name.value === this.props.arg.name
? {
...field,
value: {
kind: 'ObjectValue',
fields: fields as ObjectFieldNode[],
},
}
: field
)
);
}
};
render() {
const { arg, getScalarArgInput, parentField, directParentArg } = this.props;
const argSelection = this._getArgSelection();
return (
<AbstractArgView
argValue={argSelection ? argSelection.value : undefined}
arg={arg}
parentField={parentField}
directParentArg={directParentArg}
addArg={this._addArg}
removeArg={this._removeArg}
setArgFields={this._modifyChildFields}
setArgValue={this._setArgValue}
getDefaultScalarArgValue={this.props.getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
/>
);
}
}
type InputArgListViewProps = {
arg: GraphQLArgument;
selection: ListValueNode;
parentField: Field;
modifyArgument: (list: ListValueNode) => void;
getDefaultScalarArgValue: GetDefaultScalarArgValue;
getScalarArgInput: GetScalarArgInput;
makeDefaultArg?: MakeDefaultArg;
onRunOperation: (e: React.KeyboardEvent<HTMLInputElement | HTMLSelectElement>) => void;
styleConfig: StyleConfig;
};
class InputArgListView extends React.PureComponent<InputArgListViewProps, {}> {
_addArg = () => {
const { arg, getDefaultScalarArgValue, makeDefaultArg, parentField } = this.props;
const argType = unwrapInputType(arg.type);
const fields = argType.getFields();
const modifiedListValue = {
kind: 'ListValue',
values: [
{
kind: 'ObjectValue',
fields: defaultInputObjectFields(
getDefaultScalarArgValue,
makeDefaultArg || null,
parentField,
Object.keys(fields).map(k => fields[k]),
arg
),
},
],
} as ListValueNode;
this.props.modifyArgument(modifiedListValue);
};
_removeArg = (index: number) => () => {
const { selection } = this.props;
const modifiedListValue = {
kind: 'ListValue',
values: selection.values.filter((_value, i) => i !== index),
} as ListValueNode;
this.props.modifyArgument(modifiedListValue);
};
_setArgFields = (index: number) => (fields: readonly ObjectFieldNode[]) => {
const { selection } = this.props;
const objectValue = selection.values[index] as ObjectValueNode;
const modifiedListValue = {
kind: 'ListValue',
values: Object.assign([], selection.values, {
[index.toString()]: {
...objectValue,
fields: [...fields].sort((a: ObjectFieldNode, b: ObjectFieldNode) =>
a.name.value.localeCompare(b.name.value)
),
},
}),
} as ListValueNode;
this.props.modifyArgument(modifiedListValue);
};
_setArgValue = (index: number) => (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { selection } = this.props;
const targetValue = event.target.value;
const objectValue = selection.values[index] as ObjectValueNode;
const modifiedListValue = {
kind: 'ListValue',
values: Object.assign([], selection.values, {
[index.toString()]: {
...objectValue,
value: targetValue,
},
}),
} as ListValueNode;
this.props.modifyArgument(modifiedListValue);
};
render() {
const {
arg,
getDefaultScalarArgValue,
getScalarArgInput,
makeDefaultArg,
onRunOperation,
parentField,
selection,
styleConfig,
} = this.props;
const argType = unwrapInputType(arg.type);
return (selection.values || []).map((argValue, index) => (
<AbstractArgView
key={`${arg.name}[${index}]`}
argValue={argValue}
arg={{
...arg,
name: index.toString(),
type: argType,
}}
parentField={parentField}
addArg={this._addArg}
removeArg={this._removeArg(index)}
setArgFields={this._setArgFields(index)}
setArgValue={this._setArgValue(index)}
getDefaultScalarArgValue={getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={makeDefaultArg}
onRunOperation={onRunOperation}
styleConfig={styleConfig}
/>
));
}
}
type ArgViewProps = {
parentField: Field;
arg: GraphQLArgument;
selection: FieldNode;
modifyArguments: (argumentNodes: readonly ArgumentNode[]) => void;
getDefaultScalarArgValue: GetDefaultScalarArgValue;
getScalarArgInput: GetScalarArgInput;
makeDefaultArg?: MakeDefaultArg;
onRunOperation: () => void;
styleConfig: StyleConfig;
};
type ArgViewState = {};
export function defaultValue(argType: GraphQLEnumType | GraphQLScalarType): ValueNode {
if (isEnumType(argType)) {
return { kind: 'EnumValue', value: argType.getValues()[0].name };
} else {
switch (argType.name) {
case 'String':
return { kind: 'StringValue', value: '' };
case 'Float':
return { kind: 'FloatValue', value: '1.5' };
case 'Int':
return { kind: 'IntValue', value: '10' };
case 'Boolean':
return { kind: 'BooleanValue', value: false };
default:
return { kind: 'StringValue', value: '' };
}
}
}
function defaultGetDefaultScalarArgValue(
parentField: Field,
arg: GraphQLArgument | GraphQLInputField,
argType: GraphQLEnumType | GraphQLScalarType,
directParentArg: GraphQLArgument | GraphQLInputField
): ValueNode {
return defaultValue(argType);
}
class ArgView extends React.PureComponent<ArgViewProps, ArgViewState> {
_previousArgSelection?: ArgumentNode;
_getArgSelection = () => {
const { selection } = this.props;
return (selection.arguments || []).find(arg => arg.name.value === this.props.arg.name);
};
_removeArg = () => {
const { selection } = this.props;
const argSelection = this._getArgSelection();
this._previousArgSelection = argSelection;
this.props.modifyArguments((selection.arguments || []).filter(arg => arg !== argSelection));
};
_addArg = () => {
const { selection, getDefaultScalarArgValue, makeDefaultArg, parentField, arg } = this.props;
const argType = unwrapInputType(arg.type);
if (isListArgument(arg)) {
const fields = isLeafType(argType) ? {} : argType.getFields();
const currentArgSelection = this._getArgSelection();
const newValue = isLeafType(argType)
? getDefaultScalarArgValue(parentField, arg, argType)
: {
kind: 'ObjectValue',
fields: defaultInputObjectFields(
getDefaultScalarArgValue,
makeDefaultArg || null,
parentField,
Object.keys(fields).map(k => fields[k]),
arg
),
};
const argSelection = {
kind: 'Argument',
name: { kind: 'Name', value: arg.name },
value: {
kind: 'ListValue',
values: [
...(currentArgSelection && currentArgSelection.value
? (currentArgSelection.value as ListValueNode).values
: []),
newValue,
],
},
} as ArgumentNode;
this.props.modifyArguments([
...(selection.arguments || []).filter(({ name }) => name.value !== arg.name),
argSelection,
]);
} else {
let argSelection = null;
if (this._previousArgSelection) {
argSelection = this._previousArgSelection;
} else if (isInputObjectType(argType)) {
const fields = argType.getFields();
argSelection = {
kind: 'Argument',
name: { kind: 'Name', value: arg.name },
value: {
kind: 'ObjectValue',
fields: defaultInputObjectFields(
getDefaultScalarArgValue,
makeDefaultArg || null,
parentField,
Object.keys(fields).map(k => fields[k]),
arg
),
},
} as ArgumentNode;
} else if (isLeafType(argType)) {
argSelection = {
kind: 'Argument',
name: { kind: 'Name', value: arg.name },
value: getDefaultScalarArgValue(parentField, arg, argType),
} as ArgumentNode;
}
if (!argSelection) {
console.error('Unable to add arg for argType', argType);
} else {
this.props.modifyArguments([...(selection.arguments || []), argSelection]);
}
}
};
_setArgValue = (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { selection } = this.props;
const argSelection = this._getArgSelection();
if (!argSelection) {
console.error('missing arg selection when setting arg value');
return;
}
const argType = unwrapInputType(this.props.arg.type);
if (!isLeafType(argType)) {
console.warn('Unable to handle non leaf types in setArgValue');
return;
}
const targetValue = event.target.value;
this.props.modifyArguments(
(selection.arguments || []).map(a =>
a === argSelection
? {
...a,
value: coerceArgValue(argType, targetValue),
}
: a
)
);
};
/**
* When fields is ObjectFieldNode[], we are modifying fields of an InputObject. But if it
* is a ListValueNode, then the argument is a list, in which case we return the entire list.
*/
_setArgFields = (fields: ObjectFieldNode[] | ListValueNode) => {
const { selection } = this.props;
const argSelection = this._getArgSelection();
if (!argSelection) {
console.error('missing arg selection when setting arg value');
return;
}
if ((fields as ListValueNode).kind === 'ListValue') {
this.props.modifyArguments(
(selection.arguments || []).map(a =>
a === argSelection
? {
...a,
value: fields as ListValueNode,
}
: a
)
);
} else {
this.props.modifyArguments(
(selection.arguments || []).map(a =>
a === argSelection
? {
...a,
value: {
kind: 'ObjectValue',
fields: fields as ObjectFieldNode[],
},
}
: a
)
);
}
};
render() {
const { arg, getScalarArgInput, parentField } = this.props;
const argSelection = this._getArgSelection();
return (
<AbstractArgView
argValue={argSelection ? argSelection.value : undefined}
arg={arg}
parentField={parentField}
addArg={this._addArg}
removeArg={this._removeArg}
setArgFields={this._setArgFields}
setArgValue={this._setArgValue}
getDefaultScalarArgValue={this.props.getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
/>
);
}
}
function isRunShortcut(event: React.KeyboardEvent<HTMLInputElement>) {
return event.metaKey && event.key === 'Enter';
}
type AbstractArgViewProps = {
argValue?: ValueNode;
arg: GraphQLArgument;
parentField: Field;
directParentArg?: GraphQLArgument;
setArgValue: (event: React.SyntheticEvent) => void;
setArgFields: (fields: readonly ObjectFieldNode[] | ListValueNode) => void;
addArg: () => void;
removeArg: () => void;
getDefaultScalarArgValue: GetDefaultScalarArgValue;
getScalarArgInput: GetScalarArgInput;
makeDefaultArg?: MakeDefaultArg;
onRunOperation: (e: React.KeyboardEvent<HTMLInputElement>) => void;
styleConfig: StyleConfig;
};
type ScalarInputProps = {
arg: GraphQLArgument;
argValue: ValueNode;
setArgValue: (event: React.SyntheticEvent) => void;
onRunOperation: (e: React.KeyboardEvent<HTMLInputElement>) => void;
styleConfig: StyleConfig;
};
class ScalarInput extends React.PureComponent<ScalarInputProps, {}> {
_ref?: any;
_handleChange = (event: React.SyntheticEvent) => {
this.props.setArgValue(event);
};
componentDidMount() {
const input = this._ref;
const activeElement = document.activeElement;
if (input && activeElement && !(activeElement instanceof HTMLTextAreaElement)) {
input.focus();
input.setSelectionRange(0, input.value.length);
}
}
render() {
const { arg, argValue, styleConfig } = this.props;
const argType = unwrapInputType(arg.type);
const value = 'value' in argValue && typeof argValue.value === 'string' ? argValue.value : '';
const color =
this.props.argValue.kind === 'StringValue'
? styleConfig.colors.string
: styleConfig.colors.number;
return (
<span style={{ color }}>
{argType.name === 'String' ? '"' : ''}
<input
style={{
border: 'none',
borderBottom: '1px solid #888',
outline: 'none',
width: `${Math.max(1, value.length)}ch`,
color,
}}
ref={ref => {
this._ref = ref;
}}
type="text"
onKeyDown={event => {
if (isRunShortcut(event)) {
this.props.onRunOperation(event);
}
}}
onChange={this._handleChange}
value={value}
/>
{argType.name === 'String' ? '"' : ''}
</span>
);
}
}
class AbstractArgView extends React.PureComponent<AbstractArgViewProps, {}> {
render() {
const {
argValue,
arg,
directParentArg,
getDefaultScalarArgValue,
getScalarArgInput,
makeDefaultArg,
onRunOperation,
parentField,
setArgFields,
setArgValue,
styleConfig,
} = this.props;
const argType = unwrapInputType(arg.type);
let input = null;
if (argValue) {
if (argValue.kind === 'Variable') {
input = <span style={{ color: styleConfig.colors.variable }}>${argValue.name.value}</span>;
} else if (isListArgument(arg)) {
input = (
<div style={{ marginLeft: 16 }}>
<InputArgListView
arg={arg}
parentField={parentField}
selection={argValue as ListValueNode}
modifyArgument={setArgFields}
getDefaultScalarArgValue={getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={makeDefaultArg}
onRunOperation={onRunOperation}
styleConfig={styleConfig}
/>
</div>
);
} else if (isScalarType(argType)) {
if (argType.name === 'Boolean') {
input = (
<select
style={{ color: styleConfig.colors.builtin }}
onChange={setArgValue}
value={argValue.kind === 'BooleanValue' ? argValue.value.toString() : undefined}
>
<option key="true" value="true">
true
</option>
<option key="false" value="false">
false
</option>
</select>
);
} else {
input = getScalarArgInput(
parentField,
arg,
argType,
argValue,
setArgValue,
styleConfig,
directParentArg
) || (
<ScalarInput
setArgValue={setArgValue}
arg={arg}
argValue={argValue}
onRunOperation={onRunOperation}
styleConfig={styleConfig}
/>
);
}
} else if (isEnumType(argType)) {
if (argValue.kind === 'EnumValue') {
input = (
<select
style={{
backgroundColor: 'white',
color: styleConfig.colors.string2,
}}
onChange={setArgValue}
value={argValue.value}
>
{argType.getValues().map(value => (
<option key={value.name} value={value.name}>
{value.name}
</option>
))}
</select>
);
} else {
console.error('arg mismatch between arg and selection', argType, argValue);
}
} else if (isInputObjectType(argType)) {
if (argValue.kind === 'ObjectValue') {
const fields = argType.getFields();
input = (
<div style={{ marginLeft: 16 }}>
{Object.keys(fields)
.sort()
.map(fieldName => (
<InputArgView
key={fieldName}
arg={fields[fieldName] as GraphQLArgument}
parentField={parentField}
directParentArg={arg}
selection={argValue}
modifyFields={this.props.setArgFields}
getDefaultScalarArgValue={this.props.getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={styleConfig}
/>
))}
</div>
);
} else {
console.error('arg mismatch between arg and selection', argType, argValue);
}
}
}
return (
<div
style={{
cursor: 'pointer',
minHeight: '16px',
WebkitUserSelect: 'none',
userSelect: 'none',
}}
data-arg-name={arg.name}
data-arg-type={argType.name}
>
<span>
<span
style={{ cursor: 'pointer' }}
onClick={argValue ? this.props.removeArg : this.props.addArg}
>
{isInputObjectType(argType) &&
(!!argValue ? this.props.styleConfig.arrowOpen : this.props.styleConfig.arrowClosed)}
<Checkbox checked={!!argValue} styleConfig={this.props.styleConfig} />
<Tooltip arrowPointAtCenter placement="rightTop" title={arg.description || ''}>
<span style={{ color: styleConfig.colors.attribute }}>
{arg.name}
{isRequiredArgument(arg) ? '*' : ''}:
{isListArgument(arg) &&
`[${
(argValue as ListValueNode) && (argValue as ListValueNode).values
? (argValue as ListValueNode).values.length
: '0'
}]`}
</span>
</Tooltip>
</span>
{isListArgument(arg) && (
<Fragment>
{' '}
<Tooltip title="Add array item">
<span onClick={this.props.addArg} style={{ cursor: 'pointer' }}>
</span>
</Tooltip>
</Fragment>
)}
</span>{' '}
{input || <span />}
</div>
);
}
}
type AbstractViewProps = {
implementingType: GraphQLObjectType;
selections: Selections;
modifySelections: (selections: Selections) => void;
schema: GraphQLSchema;
getDefaultFieldNames: (type: GraphQLObjectType) => Array<string>;
getDefaultScalarArgValue: GetDefaultScalarArgValue;
getScalarArgInput: GetScalarArgInput;
makeDefaultArg?: MakeDefaultArg;
onRunOperation: () => void;
styleConfig: StyleConfig;
};
class AbstractView extends React.PureComponent<AbstractViewProps, {}> {
_previousSelection?: InlineFragmentNode;
_addFragment = () => {
this.props.modifySelections([
...this.props.selections,
this._previousSelection || {
kind: 'InlineFragment',
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: this.props.implementingType.name },
},
selectionSet: {
kind: 'SelectionSet',
selections: this.props
.getDefaultFieldNames(this.props.implementingType)
.map(fieldName => ({
kind: 'Field',
name: { kind: 'Name', value: fieldName },
})),
},
},
]);
};
_removeFragment = () => {
const thisSelection = this._getSelection();
this._previousSelection = thisSelection;
this.props.modifySelections(this.props.selections.filter(s => s !== thisSelection));
};
_getSelection = (): InlineFragmentNode | undefined => {
const selection = this.props.selections.find(
selection =>
selection.kind === 'InlineFragment' &&
selection.typeCondition &&
this.props.implementingType.name === selection.typeCondition.name.value
);
if (!selection) {
return undefined;
}
if (selection.kind === 'InlineFragment') {
return selection;
}
return undefined;
};
_modifyChildSelections = (selections: Selections) => {
const thisSelection = this._getSelection();
this.props.modifySelections(
this.props.selections.map(selection => {
if (selection === thisSelection) {
return {
directives: selection.directives,
kind: 'InlineFragment',
typeCondition: {
kind: 'NamedType',
name: { kind: 'Name', value: this.props.implementingType.name },
},
selectionSet: {
kind: 'SelectionSet',
selections,
},
};
}
return selection;
})
);
};
render() {
const {
implementingType,
schema,
getDefaultFieldNames,
getScalarArgInput,
styleConfig,
} = this.props;
const selection = this._getSelection();
const fields = implementingType.getFields();
const childSelections = selection
? selection.selectionSet
? selection.selectionSet.selections
: []
: [];
return (
<div>
<span
style={{ cursor: 'pointer' }}
onClick={selection ? this._removeFragment : this._addFragment}
>
<Checkbox checked={!!selection} styleConfig={this.props.styleConfig} />
<span style={{ color: styleConfig.colors.atom }}>{this.props.implementingType.name}</span>
</span>
{selection ? (
<div style={{ marginLeft: 16 }}>
{Object.keys(fields)
.sort()
.map(fieldName => (
<FieldView
key={fieldName}
field={fields[fieldName]}
selections={childSelections}
modifySelections={this._modifyChildSelections}
schema={schema}
getDefaultFieldNames={getDefaultFieldNames}
getDefaultScalarArgValue={this.props.getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
/>
))}
</div>
) : null}
</div>
);
}
}
type FieldViewProps = {
field: Field;
selections: Selections;
modifySelections: (selections: Selections) => void;
schema: GraphQLSchema;
getDefaultFieldNames: (type: GraphQLObjectType) => Array<string>;
getDefaultScalarArgValue: GetDefaultScalarArgValue;
getScalarArgInput: GetScalarArgInput;
makeDefaultArg?: MakeDefaultArg;
onRunOperation: () => void;
styleConfig: StyleConfig;
};
function defaultInputObjectFields(
getDefaultScalarArgValue: GetDefaultScalarArgValue,
makeDefaultArg: MakeDefaultArg | null,
parentField: Field,
fields: GraphQLInputField[],
directParentArg: GraphQLArgument
): ObjectFieldNode[] {
const nodes = [];
for (const field of fields) {
if (
isRequiredInputField(field) ||
(makeDefaultArg && makeDefaultArg(parentField, field, directParentArg))
) {
const fieldType = unwrapInputType(field.type);
if (isInputObjectType(fieldType)) {
const fields = fieldType.getFields();
nodes.push({
kind: 'ObjectField',
name: { kind: 'Name', value: field.name },
value: {
kind: 'ObjectValue',
fields: defaultInputObjectFields(
getDefaultScalarArgValue,
makeDefaultArg,
parentField,
Object.keys(fields).map(k => fields[k]),
directParentArg
),
},
} as ObjectFieldNode);
} else if (isLeafType(fieldType)) {
nodes.push({
kind: 'ObjectField',
name: { kind: 'Name', value: field.name },
value: getDefaultScalarArgValue(parentField, field, fieldType, directParentArg),
} as ObjectFieldNode);
}
}
}
return nodes;
}
function defaultArgs(
getDefaultScalarArgValue: GetDefaultScalarArgValue,
makeDefaultArg: MakeDefaultArg | null,
field: Field
): ArgumentNode[] {
const args = [];
for (const arg of field.args) {
if (isRequiredArgument(arg) || (makeDefaultArg && makeDefaultArg(field, arg))) {
const argType = unwrapInputType(arg.type);
if (isInputObjectType(argType)) {
const fields = argType.getFields();
args.push({
kind: 'Argument',
name: { kind: 'Name', value: arg.name },
value: {
kind: 'ObjectValue',
fields: defaultInputObjectFields(
getDefaultScalarArgValue,
makeDefaultArg,
field,
Object.keys(fields).map(k => fields[k]),
arg
),
},
} as ArgumentNode);
} else if (isLeafType(argType)) {
args.push({
kind: 'Argument',
name: { kind: 'Name', value: arg.name },
value: getDefaultScalarArgValue(field, arg, argType),
} as ArgumentNode);
}
}
}
return args;
}
class FieldView extends React.PureComponent<FieldViewProps, {}> {
_previousSelection?: FieldNode;
_addAllFieldsToSelections = (rawSubfields?: GraphQLFieldMap<any, any>) => {
const subFields: FieldNode[] = !!rawSubfields
? Object.keys(rawSubfields).map(fieldName => {
return {
kind: 'Field',
name: { kind: 'Name', value: fieldName },
arguments: [],
};
})
: [];
const subSelectionSet: SelectionSetNode = {
kind: 'SelectionSet',
selections: subFields,
};
const nextSelections = [
...this.props.selections.filter(selection => {
if (selection.kind === 'InlineFragment') {
return true;
} else {
// Remove the current selection set for the target field
return selection.name.value !== this.props.field.name;
}
}),
{
kind: 'Field',
name: { kind: 'Name', value: this.props.field.name },
arguments: defaultArgs(
this.props.getDefaultScalarArgValue,
this.props.makeDefaultArg || null,
this.props.field
),
selectionSet: subSelectionSet,
} as FieldNode,
];
this.props.modifySelections(nextSelections);
};
_addFieldToSelections = (_rawSubfields?: GraphQLFieldMap<any, any>) => {
const nextSelections = [
...this.props.selections,
this._previousSelection || {
kind: 'Field',
name: { kind: 'Name', value: this.props.field.name },
arguments: defaultArgs(
this.props.getDefaultScalarArgValue,
this.props.makeDefaultArg || null,
this.props.field
),
},
];
this.props.modifySelections(nextSelections);
};
_handleUpdateSelections = (event: React.MouseEvent) => {
const selection = this._getSelection();
if (selection && !event.altKey) {
this._removeFieldFromSelections();
} else {
const fieldType = getNamedType(this.props.field.type);
const rawSubfields: GraphQLFieldMap<any, any> | undefined = isObjectType(fieldType)
? fieldType.getFields()
: undefined;
const shouldSelectAllSubfields = !!rawSubfields && event.altKey;
shouldSelectAllSubfields
? this._addAllFieldsToSelections(rawSubfields)
: this._addFieldToSelections(rawSubfields);
}
};
_removeFieldFromSelections = () => {
const previousSelection = this._getSelection();
this._previousSelection = previousSelection;
this.props.modifySelections(
this.props.selections.filter((selection: FieldNode) => selection !== previousSelection)
);
};
_getSelection = (): FieldNode | undefined => {
const selection = this.props.selections.find(
(selection: FieldNode) =>
selection.kind === 'Field' && this.props.field.name === selection.name.value
);
if (!selection) {
return undefined;
}
if (selection.kind === 'Field') {
return selection;
}
return undefined;
};
_setArguments = (argumentNodes: readonly ArgumentNode[]) => {
const selection = this._getSelection();
if (!selection) {
console.error('Missing selection when setting arguments', argumentNodes);
return;
}
this.props.modifySelections(
this.props.selections.map((s: FieldNode) =>
s === selection
? {
alias: selection.alias,
arguments: argumentNodes,
directives: selection.directives,
kind: 'Field',
name: selection.name,
selectionSet: selection.selectionSet,
}
: s
)
);
};
_modifyChildSelections = (selections: Selections) => {
this.props.modifySelections(
this.props.selections.map((selection: SelectionNode) => {
if (selection.kind === 'Field' && this.props.field.name === selection.name.value) {
if (selection.kind !== 'Field') {
throw new Error('invalid selection');
}
return {
alias: selection.alias,
arguments: selection.arguments,
directives: selection.directives,
kind: 'Field',
name: selection.name,
selectionSet: {
kind: 'SelectionSet',
selections,
},
};
}
return selection;
})
);
};
render() {
const { field, schema, getDefaultFieldNames, getScalarArgInput, styleConfig } = this.props;
const selection = this._getSelection();
const type = unwrapOutputType(field.type);
const args = field.args.sort((a, b) => a.name.localeCompare(b.name));
const node = (
<div className="graphiql-explorer-node">
<Tooltip arrowPointAtCenter placement="rightTop" title={field.description || ''}>
<span
style={{
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
minHeight: '16px',
WebkitUserSelect: 'none',
userSelect: 'none',
}}
data-field-name={field.name}
data-field-type={type.name}
onClick={this._handleUpdateSelections}
>
{isObjectType(type) ? (
<span>
{!!selection
? this.props.styleConfig.arrowOpen
: this.props.styleConfig.arrowClosed}
</span>
) : null}
<Checkbox checked={!!selection} styleConfig={this.props.styleConfig} />
<span style={{ color: styleConfig.colors.property }}>{field.name}</span>
</span>
</Tooltip>
{selection && args.length ? (
<div style={{ marginLeft: 16 }}>
{args.map(arg => (
<ArgView
key={arg.name}
parentField={field}
arg={arg}
selection={selection}
modifyArguments={this._setArguments}
getDefaultScalarArgValue={this.props.getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
/>
))}
</div>
) : null}
</div>
);
if (selection && (isObjectType(type) || isInterfaceType(type) || isUnionType(type))) {
const fields = isUnionType(type) ? {} : type.getFields();
const childSelections = selection
? selection.selectionSet
? selection.selectionSet.selections
: []
: [];
return (
<div>
{node}
<div style={{ marginLeft: 16 }}>
{Object.keys(fields)
.sort()
.map(fieldName => (
<FieldView
key={fieldName}
field={fields[fieldName]}
selections={childSelections}
modifySelections={this._modifyChildSelections}
schema={schema}
getDefaultFieldNames={getDefaultFieldNames}
getDefaultScalarArgValue={this.props.getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
/>
))}
{isInterfaceType(type) || isUnionType(type)
? schema
.getPossibleTypes(type)
.map(type => (
<AbstractView
key={type.name}
implementingType={type}
selections={childSelections}
modifySelections={this._modifyChildSelections}
schema={schema}
getDefaultFieldNames={getDefaultFieldNames}
getDefaultScalarArgValue={this.props.getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
/>
))
: null}
</div>
</div>
);
}
return node;
}
}
function parseQuery(text: string): DocumentNode | Error | null {
try {
if (!text.trim()) {
return null;
}
return parse(
text,
// Tell graphql to not bother track locations when parsing, we don't need
// it and it's a tiny bit more expensive.
{ noLocation: true }
);
} catch (e) {
return new Error(e);
}
}
const DEFAULT_OPERATION = {
kind: 'OperationDefinition',
operation: 'query',
variableDefinitions: [],
name: { kind: 'Name', value: 'MyQuery' },
directives: [],
selectionSet: {
kind: 'SelectionSet',
selections: [],
},
} as OperationDefinitionNode;
const DEFAULT_DOCUMENT: DocumentNode = {
kind: 'Document',
definitions: [DEFAULT_OPERATION],
};
let parseQueryMemoize: [string, DocumentNode] | null = null;
function memoizeParseQuery(query: string): DocumentNode {
if (parseQueryMemoize && parseQueryMemoize[0] === query) {
return parseQueryMemoize[1];
} else {
const result = parseQuery(query);
if (!result) {
return DEFAULT_DOCUMENT;
} else if (result instanceof Error) {
if (parseQueryMemoize) {
// Most likely a temporarily invalid query while they type
return parseQueryMemoize[1];
} else {
return DEFAULT_DOCUMENT;
}
} else {
parseQueryMemoize = [query, result];
return result;
}
}
}
const defaultStyles: Styles = {
buttonStyle: {
fontSize: '1.2em',
padding: '0px',
backgroundColor: 'white',
border: 'none',
margin: '5px 0px',
height: '40px',
width: '100%',
display: 'block',
maxWidth: 'none',
},
explorerActionsStyle: {
margin: '4px -8px -8px',
paddingLeft: '8px',
bottom: '0px',
width: '100%',
textAlign: 'center',
background: 'none',
borderTop: 'none',
borderBottom: 'none',
},
};
type RootViewProps = {
schema: GraphQLSchema;
fields?: GraphQLFieldMap<any, any>;
operation: 'query' | 'mutation' | 'subscription' | 'fragment';
name?: string;
onTypeName?: string;
definition: FragmentDefinitionNode | OperationDefinitionNode;
onEdit: (operationDef?: OperationDefinitionNode | FragmentDefinitionNode) => void;
onOperationRename: (query: string) => void;
onRunOperation: (name?: string) => void;
getDefaultFieldNames: (type: GraphQLObjectType) => Array<string>;
getDefaultScalarArgValue: GetDefaultScalarArgValue;
getScalarArgInput: GetScalarArgInput;
makeDefaultArg?: MakeDefaultArg;
styleConfig: StyleConfig;
};
class RootView extends React.PureComponent<RootViewProps, {}> {
_previousOperationDef?: OperationDefinitionNode | FragmentDefinitionNode;
_modifySelections = (selections: Selections) => {
let operationDef: FragmentDefinitionNode | OperationDefinitionNode = this.props.definition;
if (operationDef.selectionSet.selections.length === 0 && this._previousOperationDef) {
operationDef = this._previousOperationDef;
}
let newOperationDef: OperationDefinitionNode | FragmentDefinitionNode | undefined;
if (selections.length === 0) {
this._previousOperationDef = operationDef;
} else if (operationDef.kind === 'FragmentDefinition') {
newOperationDef = {
...operationDef,
selectionSet: {
...operationDef.selectionSet,
selections,
},
};
} else if (operationDef.kind === 'OperationDefinition') {
newOperationDef = {
...operationDef,
selectionSet: {
...operationDef.selectionSet,
selections,
},
};
}
this.props.onEdit(newOperationDef);
};
_onOperationRename = (event: React.ChangeEvent<HTMLInputElement>) =>
this.props.onOperationRename(event.target.value);
_handlePotentialRun = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (isRunShortcut(event)) {
this.props.onRunOperation(this.props.name);
}
};
render() {
const {
operation,
name,
definition,
schema,
getDefaultFieldNames,
getScalarArgInput,
styleConfig,
} = this.props;
const fields = this.props.fields || {};
const operationDef = definition;
const selections = operationDef.selectionSet.selections;
const operationDisplayName = this.props.name || `${capitalize(operation)} Name`;
return (
<div
id={`${operation}-${name || 'unknown'}`}
style={{
borderBottom: '1px solid #d6d6d6',
marginLeft: 16,
marginBottom: '0em',
paddingBottom: '1em',
}}
>
<div style={{ color: styleConfig.colors.keyword, marginLeft: -16, paddingBottom: 4 }}>
{operation}{' '}
<span style={{ color: styleConfig.colors.def }}>
<input
style={{
color: styleConfig.colors.def,
border: 'none',
borderBottom: '1px solid #888',
outline: 'none',
width: `${Math.max(4, operationDisplayName.length)}ch`,
}}
autoComplete="false"
placeholder={`${capitalize(operation)} Name`}
value={this.props.name || undefined}
onKeyDown={this._handlePotentialRun}
onChange={this._onOperationRename}
/>
</span>
{!!this.props.onTypeName ? (
<span>
<br />
{`on ${this.props.onTypeName}`}
</span>
) : (
''
)}
</div>
{Object.keys(fields)
.sort()
.map((fieldName: string) => (
<FieldView
key={fieldName}
field={fields[fieldName]}
selections={selections}
modifySelections={this._modifySelections}
schema={schema}
getDefaultFieldNames={getDefaultFieldNames}
getDefaultScalarArgValue={this.props.getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
/>
))}
</div>
);
}
}
class Explorer extends React.PureComponent<Props, State> {
static defaultProps = {
getDefaultFieldNames: defaultGetDefaultFieldNames,
getDefaultScalarArgValue: defaultGetDefaultScalarArgValue,
getScalarArgInput: () => {},
};
_ref?: any;
_resetScroll = () => {
const container = this._ref;
if (container) {
container.scrollLeft = 0;
}
};
componentDidMount() {
this._resetScroll();
}
_onEdit = (query: string): void => this.props.onEdit(query);
render() {
const { schema, query, makeDefaultArg, getScalarArgInput } = this.props;
if (!schema) {
return (
<div style={{ fontFamily: 'sans-serif' }} className="error-container">
No Schema Available
</div>
);
}
const styleConfig: StyleConfig = {
colors: this.props.colors || defaultColors,
checkboxChecked: this.props.checkboxChecked || defaultCheckboxChecked,
checkboxUnchecked: this.props.checkboxUnchecked || defaultCheckboxUnchecked,
arrowClosed: this.props.arrowClosed || defaultArrowClosed,
arrowOpen: this.props.arrowOpen || defaultArrowOpen,
styles: this.props.styles
? {
...defaultStyles,
...this.props.styles,
}
: defaultStyles,
};
const queryType = schema.getQueryType();
const mutationType = schema.getMutationType();
const subscriptionType = schema.getSubscriptionType();
if (!queryType && !mutationType && !subscriptionType) {
return <div>Missing query type</div>;
}
const queryFields = queryType ? queryType.getFields() : undefined;
const mutationFields = mutationType ? mutationType.getFields() : undefined;
const subscriptionFields = subscriptionType ? subscriptionType.getFields() : undefined;
const parsedQuery: DocumentNode = memoizeParseQuery(query);
const getDefaultFieldNames = this.props.getDefaultFieldNames || defaultGetDefaultFieldNames;
const getDefaultScalarArgValue =
this.props.getDefaultScalarArgValue || defaultGetDefaultScalarArgValue;
const definitions = parsedQuery.definitions;
const _relevantOperations = definitions.filter(
({ kind }) => kind === 'FragmentDefinition' || kind === 'OperationDefinition'
);
const relevantOperations =
// If we don't have any relevant definitions from the parsed document,
// then at least show an expanded Query selection
_relevantOperations.length === 0 ? DEFAULT_DOCUMENT.definitions : _relevantOperations;
const renameOperation = (
targetOperation: OperationDefinitionNode | FragmentDefinitionNode,
name?: string
) => {
const newName: NameNode = { kind: 'Name', value: name || '', loc: undefined };
const newOperation = { ...targetOperation, name: newName };
const existingDefs = parsedQuery.definitions;
const newDefinitions = existingDefs.map(existingOperation => {
if (targetOperation === existingOperation) {
return newOperation;
} else {
return existingOperation;
}
});
return {
...parsedQuery,
definitions: newDefinitions,
};
};
const addOperation = (kind: 'query' | 'mutation' | 'subscription') => {
const existingDefs = parsedQuery.definitions;
const viewingDefaultOperation =
parsedQuery.definitions.length === 1 &&
parsedQuery.definitions[0] === DEFAULT_DOCUMENT.definitions[0];
const MySiblingDefs = viewingDefaultOperation
? []
: existingDefs.filter(def => {
if (def.kind === 'OperationDefinition') {
return def.operation === kind;
} else {
// Don't support adding fragments from explorer
return false;
}
});
const newOperationName = `My${capitalize(kind)}${
MySiblingDefs.length === 0 ? '' : MySiblingDefs.length + 1
}`;
// Add this as the default field as it guarantees a valid selectionSet
const firstFieldName = '__typename # Placeholder value';
const selectionSet: SelectionSetNode = {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
name: {
kind: 'Name',
value: firstFieldName,
loc: undefined,
},
arguments: [],
directives: [],
selectionSet: undefined,
loc: undefined,
},
],
loc: undefined,
};
const newDefinition: OperationDefinitionNode = {
kind: 'OperationDefinition',
operation: kind,
name: { kind: 'Name', value: newOperationName },
variableDefinitions: [],
directives: [],
selectionSet: selectionSet,
loc: undefined,
};
const newDefinitions =
// If we only have our default operation in the document right now, then
// just replace it with our new definition
viewingDefaultOperation ? [newDefinition] : [...parsedQuery.definitions, newDefinition];
const newOperationDef = {
...parsedQuery,
definitions: newDefinitions,
};
this.props.onEdit(prettify(print(newOperationDef)));
};
return (
<div
ref={ref => {
this._ref = ref;
}}
style={{
fontSize: 12,
overflow: 'scroll',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
margin: 0,
padding: 8,
fontFamily: 'Consolas, Inconsolata, "Droid Sans Mono", Monaco, monospace',
}}
className="graphiql-explorer-root"
>
{relevantOperations.map(
(operation: OperationDefinitionNode | FragmentDefinitionNode, index: number) => {
const operationName = operation && operation.name && operation.name.value;
const operationKind =
operation.kind === 'FragmentDefinition'
? 'fragment'
: (operation && operation.operation) || 'query';
const onOperationRename = (newName: string) => {
const newOperationDef = renameOperation(operation, newName);
this.props.onEdit(prettify(print(newOperationDef)));
};
const fragmentType =
operation.kind === 'FragmentDefinition' &&
operation.typeCondition.kind === 'NamedType' &&
schema.getType(operation.typeCondition.name.value);
const fragmentFields =
fragmentType instanceof GraphQLObjectType ? fragmentType.getFields() : undefined;
const fields =
operationKind === 'query'
? queryFields
: operationKind === 'mutation'
? mutationFields
: operationKind === 'subscription'
? subscriptionFields
: operation.kind === 'FragmentDefinition'
? fragmentFields
: undefined;
const fragmentTypeName =
operation.kind === 'FragmentDefinition'
? operation.typeCondition.name.value
: undefined;
return (
<RootView
key={index}
fields={fields}
operation={operationKind}
name={operationName}
definition={operation}
onOperationRename={onOperationRename}
onTypeName={fragmentTypeName}
onEdit={newDefinition => {
if (newDefinition) {
const newQuery = {
...parsedQuery,
definitions: parsedQuery.definitions.map(existingDefinition =>
existingDefinition === operation ? newDefinition : existingDefinition
),
};
const textualNewQuery = prettify(print(newQuery));
this.props.onEdit(textualNewQuery);
}
}}
schema={schema}
getDefaultFieldNames={getDefaultFieldNames}
getDefaultScalarArgValue={getDefaultScalarArgValue}
getScalarArgInput={getScalarArgInput}
makeDefaultArg={makeDefaultArg}
onRunOperation={() => {
if (!!this.props.onRunOperation) {
this.props.onRunOperation(operationName);
}
}}
styleConfig={styleConfig}
/>
);
}
)}
<div className="variable-editor-title" style={styleConfig.styles.explorerActionsStyle}>
{!!queryFields ? (
<button
className={'toolbar-button'}
style={styleConfig.styles.buttonStyle}
onClick={() => addOperation('query')}
>
+ ADD NEW QUERY
</button>
) : null}
{!!mutationFields ? (
<button
className={'toolbar-button'}
style={styleConfig.styles.buttonStyle}
onClick={() => addOperation('mutation')}
>
+ ADD NEW MUTATION
</button>
) : null}
{!!subscriptionFields ? (
<button
className={'toolbar-button'}
style={styleConfig.styles.buttonStyle}
onClick={() => addOperation('subscription')}
>
+ ADD NEW SUBSCRIPTION
</button>
) : null}
</div>
</div>
);
}
}
type ErrorBoundaryState = {
hasError: boolean;
error?: Error;
errorInfo?: React.ErrorInfo;
};
class ErrorBoundary extends React.Component<any, ErrorBoundaryState> {
state = { hasError: false, error: undefined, errorInfo: undefined };
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
this.setState({ hasError: true, error, errorInfo });
console.error('Error in component', error, errorInfo);
}
render() {
const { error, errorInfo, hasError } = this.state;
if (hasError) {
return (
<div style={{ padding: 18, fontFamily: 'sans-serif' }}>
<div>Something went wrong</div>
<details style={{ whiteSpace: 'pre-wrap' }}>
{(error || '').toString()}
<br />
{(errorInfo || ({} as React.ErrorInfo)).componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
class ExplorerWrapper extends React.PureComponent<Props, {}> {
static defaultValue = defaultValue;
static defaultProps = {
width: 380,
title: 'Explorer',
};
render() {
return (
<div
className="historyPaneWrap"
style={{
height: '100%',
width: this.props.width,
zIndex: 7,
display: this.props.explorerIsOpen ? 'block' : 'none',
}}
>
<div className="history-title-bar">
<div className="history-title">{this.props.title}</div>
<div className="doc-explorer-rhs">
<div className="docExplorerHide" onClick={this.props.onToggleExplorer}>
{'\u2715'}
</div>
</div>
</div>
<div className="history-contents">
<ErrorBoundary>
<Explorer {...this.props} />
</ErrorBoundary>
</div>
</div>
);
}
}
export default ExplorerWrapper;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment