diff --git a/README.md b/README.md index 704b5eb..c5a75f3 100644 --- a/README.md +++ b/README.md @@ -140,3 +140,161 @@ Example styles map: }} /> ``` + + +# Handling Custom GraphQL Scalars + +Custom GraphQL Scalars can be supported by using the [GraphQL Scalar Input Plugins](./src/plugins/graphql-scalar-inputs/README.md). + +## Plugin Definition + +A GraphQL Scalar Input plugin must implement the following: +```js +function canProcess(arg): Boolean! +function render(prop): React.Element! + +const name: String! +``` + +> Please note this interface is not currently finalised. Perhaps it is preferred to use typescript to define an abstract base class. External plugins would then extend this, making the interface concrete. + +## Usage + +### Enabling bundled plugins + +By default, these plugins are disabled to avoid breaking changes to existing users. To enable the bundled plugins, instantiate the explorer with the prop `enableBundledPlugins` set to `true`. + +## Example Plugins + +### Date Input + +See the bundled `DateInput` plugin, which demonstrates a simple implementation for a single GraphQL Scalar. + +When instantiating the GraphiQL Explorer, pass the list of desired plugins as the prop `graphqlCustomScalarPlugins`. + +### Complex Numbers + +This examples shows a plugin that can be used for more complicated input types which should form objects. + +For this example, consider the following schema: +``` +type ComplexNumber { + real: Float! + imaginary: Float! +} + +input ComplexNumberInput { + real: Float! + imaginary: Float! +} + +type Query { + complexCalculations(z: ComplexNumberInput): ComplexResponse! +} + +type ComplexResponse { + real: Float! + imaginary: Float! + length: Float! + complexConjugate: ComplexNumber! +} +``` + +The custom object type can be handled with a custom plugin. The file `ComplexNumberHandler.js` shows an example implementation for the `ComplexNumberInput`. + +```js +import * as React from "react"; + +class ComplexNumberHandler extends React.Component { + static canProcess = (arg) => arg && arg.type && arg.type.name === 'ComplexNumberInput'; + + updateComponent(arg, value, targetName) { + const updatedFields = arg.value.fields.map(childArg => { + if (childArg.name.value !== targetName) { + return childArg; + } + const updatedChild = { ...childArg }; + updatedChild.value.value = value; + return updatedChild; + }); + + const mappedArg = { ...arg }; + mappedArg.value.fields = updatedFields; + return mappedArg; + } + + handleChangeEvent(event, complexComponent) { + const { arg, selection, modifyArguments, argValue } = this.props; + return modifyArguments(selection.arguments.map(originalArg => { + if (originalArg.name.value !== arg.name) { + return originalArg; + } + + return this.updateComponent(originalArg, event.target.value, complexComponent); + })); + } + + getValue(complexArg, complexComponent) { + const childNode = complexArg && complexArg.value.fields.find(childArg => childArg.name.value === complexComponent) + + if (complexArg && childNode) { + return childNode.value.value; + } + + return ''; + } + + render() { + const { selection, arg } = this.props; + const selectedComplexArg = (selection.arguments || []).find(a => a.name.value === arg.name); + const rePart = this.getValue(selectedComplexArg, 'real'); + const imPart = this.getValue(selectedComplexArg, 'imaginary'); + return ( + + this.handleChangeEvent(e, 'real')} + style={{ maxWidth: '50px', margin: '5px' }} + step='any' + disabled={!selectedComplexArg} + /> + + + this.handleChangeEvent(e, 'imaginary')} + style={{ maxWidth: '50px', margin: '5px' }} + step='any' + disabled={!selectedComplexArg} + /> i + ); + } +} + +export default { + canProcess: ComplexNumberHandler.canProcess, + name: 'Complex Number', + render: props => ( + ), +} +``` +To add the custom plugin, pass it to the GraphiQLExplorer on instantiation. +```js +import ComplexNumberHandler from './ComplexNumberHandler'; +const configuredPlugins = [ComplexNumberHandler]; + +// Then later, in your render method where you create the explorer... + +``` +> To see examples of instantiating the explorer, see the [example repo](https://github.com/OneGraph/graphiql-explorer-example). + +Any number of plugins can be added, and can override existing bundled plugins. + +Plugins are checked in the order they are given in the `graphqlCustomScalarPlugins` list. The first plugin with a `canProcess` value that returns `true` will be used. Bundled plugins are always checked last, after all customised plugins. diff --git a/package.json b/package.json index ec336cb..4a5e702 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "graphiql-explorer", - "version": "0.8.0", + "name": "@agrimetrics/graphiql-explorer", + "version": "0.8.2", "homepage": "https://github.com/onegraph/graphiql-explorer", "bugs": { "url": "https://github.com/onegraph/graphiql-explorer/issues" }, "repository": { "type": "git", - "url": "http://github.com/onegraph/graphiql-explorer.git" + "url": "http://github.com/agrimetrics/graphiql-explorer.git" }, "license": "MIT", "main": "dist/index.js", diff --git a/src/Explorer.js b/src/Explorer.js index 01d8ff6..ab41b1d 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -310,9 +310,9 @@ function coerceArgValue( 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', @@ -327,16 +327,16 @@ function coerceArgValue( } } catch (e) { console.error('error coercing arg value', e, value); - return {kind: 'StringValue', value: value}; + return {kind: 'StringValue', 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}; } @@ -467,7 +467,7 @@ class InputArgView extends React.PureComponent { value = null; } else if ( !event.target && - !!event.kind && + Boolean(event.kind) && event.kind === 'VariableDefinition' ) { targetValue = event; @@ -485,7 +485,7 @@ class InputArgView extends React.PureComponent { const newField = isTarget ? { ...field, - value: value, + value, } : field; @@ -505,7 +505,7 @@ class InputArgView extends React.PureComponent { ...field, value: { kind: 'ObjectValue', - fields: fields, + fields, }, } : field, @@ -528,6 +528,7 @@ class InputArgView extends React.PureComponent { setArgFields={this._modifyChildFields} setArgValue={this._setArgValue} getDefaultScalarArgValue={this.props.getDefaultScalarArgValue} + scalarInputsPluginManager={this.props.scalarInputsPluginManager} makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} @@ -561,7 +562,7 @@ export function defaultValue( ): ValueNode { if (isEnumType(argType)) { return {kind: 'EnumValue', value: argType.getValues()[0].name}; - } else { + } switch (argType.name) { case 'String': return {kind: 'StringValue', value: ''}; @@ -574,7 +575,7 @@ export function defaultValue( default: return {kind: 'StringValue', value: ''}; } - } + } function defaultGetDefaultScalarArgValue( @@ -642,12 +643,12 @@ class ArgView extends React.PureComponent { if (!argSelection) { console.error('Unable to add arg for argType', argType); return null; - } else { + } return this.props.modifyArguments( [...(selection.arguments || []), argSelection], commit, ); - } + }; _setArgValue = ( event: SyntheticInputEvent<*> | VariableDefinitionNode, @@ -704,7 +705,7 @@ class ArgView extends React.PureComponent { a === argSelection ? { ...a, - value: value, + value, } : a, ), @@ -753,6 +754,9 @@ class ArgView extends React.PureComponent { makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + scalarInputsPluginManager={this.props.scalarInputsPluginManager} + modifyArguments={this.props.modifyArguments} + selection={this.props.selection} onCommit={this.props.onCommit} definition={this.props.definition} /> @@ -858,11 +862,8 @@ class AbstractArgView extends React.PureComponent< {|displayArgActions: boolean|}, > { state = {displayArgActions: false}; - render() { - const {argValue, arg, styleConfig} = this.props; - /* TODO: handle List types*/ - const argType = unwrapInputType(arg.type); + defaultArgViewHandler(arg, argType, argValue, styleConfig) { let input = null; if (argValue) { if (argValue.kind === 'Variable') { @@ -942,6 +943,7 @@ class AbstractArgView extends React.PureComponent< getDefaultScalarArgValue={ this.props.getDefaultScalarArgValue } + scalarInputsPluginManager={this.props.scalarInputsPluginManager} makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} @@ -960,6 +962,19 @@ class AbstractArgView extends React.PureComponent< } } } + return input; + } + + render() { + const {argValue, arg, styleConfig, scalarInputsPluginManager} = this.props; + /* TODO: handle List types*/ + const argType = unwrapInputType(arg.type); + + let input = scalarInputsPluginManager && scalarInputsPluginManager.process(this.props) + const usedDefaultRender = !input; + if (usedDefaultRender) { + input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig); + } const variablize = () => { /** @@ -1008,7 +1023,7 @@ class AbstractArgView extends React.PureComponent< let variable: ?VariableDefinitionNode; - let subVariableUsageCountByName: { + const subVariableUsageCountByName: { [key: string]: number, } = {}; @@ -1056,17 +1071,17 @@ class AbstractArgView extends React.PureComponent< if (newDoc) { const targetOperation = newDoc.definitions.find(definition => { if ( - !!definition.operation && - !!definition.name && - !!definition.name.value && + Boolean(definition.operation) && + Boolean(definition.name) && + Boolean(definition.name.value) && // - !!this.props.definition.name && - !!this.props.definition.name.value + Boolean(this.props.definition.name) && + Boolean(this.props.definition.name.value) ) { return definition.name.value === this.props.definition.name.value; - } else { + } return false; - } + }); const newVariableDefinitions: Array = [ @@ -1087,9 +1102,9 @@ class AbstractArgView extends React.PureComponent< const newDefinitions = existingDefs.map(existingOperation => { if (targetOperation === existingOperation) { return newOperation; - } else { + } return existingOperation; - } + }); const finalDoc = { @@ -1145,7 +1160,7 @@ class AbstractArgView extends React.PureComponent< visit(targetOperation, { Variable(node) { if (node.name.value === variableName) { - variableUseCount = variableUseCount + 1; + variableUseCount += 1; } }, }); @@ -1169,9 +1184,9 @@ class AbstractArgView extends React.PureComponent< const newDefinitions = existingDefs.map(existingOperation => { if (targetOperation === existingOperation) { return newOperation; - } else { + } return existingOperation; - } + }); const finalDoc = { @@ -1231,15 +1246,15 @@ class AbstractArgView extends React.PureComponent< } this.setState({displayArgActions: shouldAdd}); }}> - {isInputObjectType(argType) ? ( + {usedDefaultRender && isInputObjectType(argType) ? ( - {!!argValue + {argValue ? this.props.styleConfig.arrowOpen : this.props.styleConfig.arrowClosed} ) : ( )} @@ -1373,7 +1388,7 @@ class AbstractView extends React.PureComponent { style={{cursor: 'pointer'}} onClick={selection ? this._removeFragment : this._addFragment}> @@ -1393,6 +1408,7 @@ class AbstractView extends React.PureComponent { schema={schema} getDefaultFieldNames={getDefaultFieldNames} getDefaultScalarArgValue={this.props.getDefaultScalarArgValue} + scalarInputsPluginManager={this.props.scalarInputsPluginManager} makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} onCommit={this.props.onCommit} @@ -1464,7 +1480,7 @@ class FragmentView extends React.PureComponent { style={{cursor: 'pointer'}} onClick={selection ? this._removeFragment : this._addFragment}> { - const subFields: Array = !!rawSubfields + const subFields: Array = rawSubfields ? Object.keys(rawSubfields).map(fieldName => { return { kind: 'Field', @@ -1602,10 +1618,10 @@ class FieldView extends React.PureComponent< ...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', @@ -1647,7 +1663,7 @@ class FieldView extends React.PureComponent< const fieldType = getNamedType(this.props.field.type); const rawSubfields = isObjectType(fieldType) && fieldType.getFields(); - const shouldSelectAllSubfields = !!rawSubfields && event.altKey; + const shouldSelectAllSubfields = Boolean(rawSubfields) && event.altKey; shouldSelectAllSubfields ? this._addAllFieldsToSelections(rawSubfields) @@ -1740,6 +1756,7 @@ class FieldView extends React.PureComponent< const selection = this._getSelection(); const type = unwrapOutputType(field.type); const args = field.args.sort((a, b) => a.name.localeCompare(b.name)); + let className = `graphiql-explorer-node graphiql-explorer-${field.name}`; if (field.isDeprecated) { @@ -1783,14 +1800,14 @@ class FieldView extends React.PureComponent< onMouseLeave={() => this.setState({displayFieldActions: false})}> {isObjectType(type) ? ( - {!!selection + {selection ? this.props.styleConfig.arrowOpen : this.props.styleConfig.arrowClosed} ) : null} {isObjectType(type) ? null : ( )} @@ -1899,6 +1916,7 @@ class FieldView extends React.PureComponent< makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + scalarInputsPluginManager={this.props.scalarInputsPluginManager} onCommit={this.props.onCommit} definition={this.props.definition} /> @@ -1922,7 +1940,7 @@ class FieldView extends React.PureComponent<
{node}
- {!!applicableFragments + {applicableFragments ? applicableFragments.map(fragment => { const type = schema.getType( fragment.typeCondition.name.value, @@ -1952,6 +1970,7 @@ class FieldView extends React.PureComponent< schema={schema} getDefaultFieldNames={getDefaultFieldNames} getDefaultScalarArgValue={this.props.getDefaultScalarArgValue} + scalarInputsPluginManager={this.props.scalarInputsPluginManager} makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} @@ -1974,6 +1993,7 @@ class FieldView extends React.PureComponent< getDefaultScalarArgValue={ this.props.getDefaultScalarArgValue } + scalarInputsPluginManager={this.props.scalarInputsPluginManager} makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} @@ -2025,7 +2045,7 @@ let parseQueryMemoize: ?[string, DocumentNode] = null; function memoizeParseQuery(query: string): DocumentNode { if (parseQueryMemoize && parseQueryMemoize[0] === query) { return parseQueryMemoize[1]; - } else { + } const result = parseQuery(query); if (!result) { return DEFAULT_DOCUMENT; @@ -2033,14 +2053,14 @@ function memoizeParseQuery(query: string): DocumentNode { 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 = { @@ -2238,7 +2258,7 @@ class RootView extends React.PureComponent< onChange={this._onOperationRename} /> - {!!this.props.onTypeName ? ( + {this.props.onTypeName ? (
{`on ${this.props.onTypeName}`} @@ -2246,7 +2266,7 @@ class RootView extends React.PureComponent< ) : ( '' )} - {!!this.state.displayTitleActions ? ( + {this.state.displayTitleActions ? (