From 0e2b43eabfb5b59e15d94f1b6b497535f379a9f5 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Fri, 10 Jan 2020 17:07:20 +0000 Subject: [PATCH 01/21] add a 'deprecated' class to fields which have been marked as such --- src/Explorer.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Explorer.js b/src/Explorer.js index ebdb2a4..f692c8c 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -1233,8 +1233,14 @@ 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'; + + if (field.isDeprecated) { + className += ' deprecated'; + } + const node = ( -
+
Date: Sat, 1 Feb 2020 20:44:30 +0000 Subject: [PATCH 02/21] Refactor existing behaviour into seperate method --- src/Explorer.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Explorer.js b/src/Explorer.js index ea2ed42..9bfa105 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -723,11 +723,7 @@ class ScalarInput extends React.PureComponent { } class AbstractArgView extends React.PureComponent { - 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') { @@ -823,6 +819,15 @@ class AbstractArgView extends React.PureComponent { } } } + return input; + } + + render() { + const {argValue, arg, styleConfig, scalarInputsPluginManager} = this.props; + /* TODO: handle List types*/ + const argType = unwrapInputType(arg.type); + const input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig); + return (
Date: Sat, 1 Feb 2020 20:50:45 +0000 Subject: [PATCH 03/21] Introduce custom scalar input plugin manager --- src/Explorer.js | 27 ++++++++++++++++----- src/plugins/graphql-scalar-inputs/README.md | 18 ++++++++++++++ src/plugins/graphql-scalar-inputs/index.js | 15 ++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 src/plugins/graphql-scalar-inputs/README.md create mode 100644 src/plugins/graphql-scalar-inputs/index.js diff --git a/src/Explorer.js b/src/Explorer.js index 9bfa105..99a4c8e 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -51,6 +51,8 @@ import type { ValueNode, } from 'graphql'; +import ScalarInputPluginManager from './plugins/graphql-scalar-inputs' + type Field = GraphQLField; type GetDefaultScalarArgValue = ( @@ -635,6 +637,7 @@ class ArgView extends React.PureComponent { makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + scalarInputsPluginManager={this.props.scalarInputsPluginManager} /> ); } @@ -826,8 +829,11 @@ class AbstractArgView extends React.PureComponent { const {argValue, arg, styleConfig, scalarInputsPluginManager} = this.props; /* TODO: handle List types*/ const argType = unwrapInputType(arg.type); - const input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig); + let input = scalarInputsPluginManager.process(arg, styleConfig, this.props.setArgValue) + if (!input) { + input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig); + } return (
{ makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + scalarInputsPluginManager={this.props.scalarInputsPluginManager} /> ))}
@@ -1603,6 +1610,7 @@ class RootView extends React.PureComponent { makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + scalarInputsPluginManager={this.props.scalarInputsPluginManager} /> ))}
@@ -1646,11 +1654,16 @@ class Explorer extends React.PureComponent { getDefaultScalarArgValue: defaultGetDefaultScalarArgValue, }; - state = { - newOperationType: 'query', - operation: null, - operationToScrollTo: null, - }; + constructor(props) { + super(props); + this.scalarInputsPluginManager = new ScalarInputPluginManager(this.props.graphqlCustomScalarPlugins); + // should set initial state in object constructor + this.state = { + newOperationType: 'query', + operation: null, + operationToScrollTo: null, + }; + } _ref: ?any; _resetScroll = () => { @@ -2025,6 +2038,7 @@ class Explorer extends React.PureComponent { } }} styleConfig={styleConfig} + scalarInputsPluginManager={this.scalarInputsPluginManager} /> ); }, @@ -2071,6 +2085,7 @@ class ExplorerWrapper extends React.PureComponent { static defaultProps = { width: 320, title: 'Explorer', + graphqlCustomScalarPlugins: [], }; render() { diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md new file mode 100644 index 0000000..e85531f --- /dev/null +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -0,0 +1,18 @@ +# GraphQL Scalar Input Plugins + +These allow custom handling of custom GraphQL scalars + +# Definition + +A GraphQL Scalar Input pluging implements the following function signatures: +```js +function canProcess(arg): boolean + +function render(arg, styleConfig, onChangeHandler): void + +export default { + canProcess, + render, + name: "InputHandlerName" +} +``` diff --git a/src/plugins/graphql-scalar-inputs/index.js b/src/plugins/graphql-scalar-inputs/index.js new file mode 100644 index 0000000..b83426b --- /dev/null +++ b/src/plugins/graphql-scalar-inputs/index.js @@ -0,0 +1,15 @@ +class ScalarInputPluginManager { + constructor(plugins = []) { + this.plugins = plugins; + } + + process(arg, styleConfig, onChangeHandler) { + const handler = this.plugins.find(plugin => plugin.canProcess(arg)); + if (handler) { + return handler.render(arg, styleConfig, onChangeHandler); + } + return null; + } +} + +export default ScalarInputPluginManager; \ No newline at end of file From 9c3647a4b44a1d286e3726e2209a2e97e47b04de Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sat, 1 Feb 2020 20:53:09 +0000 Subject: [PATCH 04/21] Create example DateInput plugin based on PR #46 --- src/plugins/graphql-scalar-inputs/README.md | 6 +++++ .../bundled/DateInput.js | 24 +++++++++++++++++++ src/plugins/graphql-scalar-inputs/index.js | 8 ++++++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/plugins/graphql-scalar-inputs/bundled/DateInput.js diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md index e85531f..8162af0 100644 --- a/src/plugins/graphql-scalar-inputs/README.md +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -16,3 +16,9 @@ export default { name: "InputHandlerName" } ``` + +# Examples + +See the bundled `DateInput` plugin. + +When instantiating the GraphiQL Explorer, pass the list of desired plugins as the prop `graphqlCustomScalarPlugins`. \ No newline at end of file diff --git a/src/plugins/graphql-scalar-inputs/bundled/DateInput.js b/src/plugins/graphql-scalar-inputs/bundled/DateInput.js new file mode 100644 index 0000000..eb1a538 --- /dev/null +++ b/src/plugins/graphql-scalar-inputs/bundled/DateInput.js @@ -0,0 +1,24 @@ +import * as React from 'react'; + +function canProcess(arg) { + return arg && arg.type && arg.type.name === 'Date'; +} + +function render(arg, styleConfig, onChangeHandler) { + return ( + + ); +} + + +const DatePlugin = { + canProcess, + render, + name: 'DateInput' +}; + +export default DatePlugin; diff --git a/src/plugins/graphql-scalar-inputs/index.js b/src/plugins/graphql-scalar-inputs/index.js index b83426b..be24715 100644 --- a/src/plugins/graphql-scalar-inputs/index.js +++ b/src/plugins/graphql-scalar-inputs/index.js @@ -1,9 +1,15 @@ +import DatePlugin from './bundled/DateInput' + +const bundledPlugins = [DatePlugin]; + class ScalarInputPluginManager { constructor(plugins = []) { - this.plugins = plugins; + // ensure bundled plugins are the last plugins checked. + this.plugins = [...plugins, ...bundledPlugins]; } process(arg, styleConfig, onChangeHandler) { + // plugins are provided in order, the first matching plugin will be used. const handler = this.plugins.find(plugin => plugin.canProcess(arg)); if (handler) { return handler.render(arg, styleConfig, onChangeHandler); From 014ec5b2e159b4545739cb49f838106f17a10928 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sat, 1 Feb 2020 20:59:30 +0000 Subject: [PATCH 05/21] Only enable bundled plugins if requested. - ensures this is a non-breaking change --- src/Explorer.js | 4 +++- src/plugins/graphql-scalar-inputs/index.js | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Explorer.js b/src/Explorer.js index 99a4c8e..955b55a 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -1656,7 +1656,8 @@ class Explorer extends React.PureComponent { constructor(props) { super(props); - this.scalarInputsPluginManager = new ScalarInputPluginManager(this.props.graphqlCustomScalarPlugins); + const { graphqlCustomScalarPlugins, enableBundledPlugins } = this.props; + this.scalarInputsPluginManager = new ScalarInputPluginManager(graphqlCustomScalarPlugins, enableBundledPlugins); // should set initial state in object constructor this.state = { newOperationType: 'query', @@ -2086,6 +2087,7 @@ class ExplorerWrapper extends React.PureComponent { width: 320, title: 'Explorer', graphqlCustomScalarPlugins: [], + enableBundledPlugins: false, }; render() { diff --git a/src/plugins/graphql-scalar-inputs/index.js b/src/plugins/graphql-scalar-inputs/index.js index be24715..952c1b6 100644 --- a/src/plugins/graphql-scalar-inputs/index.js +++ b/src/plugins/graphql-scalar-inputs/index.js @@ -3,12 +3,21 @@ import DatePlugin from './bundled/DateInput' const bundledPlugins = [DatePlugin]; class ScalarInputPluginManager { - constructor(plugins = []) { - // ensure bundled plugins are the last plugins checked. - this.plugins = [...plugins, ...bundledPlugins]; + constructor(plugins = [], enableBundledPlugins = false) { + this.isEnabled = enableBundledPlugins; + let enabledPlugins = plugins; + if (enableBundledPlugins) { + // ensure bundled plugins are the last plugins checked. + enabledPlugins.push(...bundledPlugins); + } + this.plugins = enabledPlugins; } process(arg, styleConfig, onChangeHandler) { + if (!this.isEnabled) { + return null; + } + // plugins are provided in order, the first matching plugin will be used. const handler = this.plugins.find(plugin => plugin.canProcess(arg)); if (handler) { From 77348c4137a31494e1061ae3eff3f8e7cbdbc611 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sat, 1 Feb 2020 22:03:35 +0000 Subject: [PATCH 06/21] update readme for custom-graphql-scalars plugin --- README.md | 5 ++ src/plugins/graphql-scalar-inputs/README.md | 61 ++++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 704b5eb..1ac46d5 100644 --- a/README.md +++ b/README.md @@ -140,3 +140,8 @@ Example styles map: }} /> ``` + + +## Handling Custom GraphQL Scalars + +Custom GraphQL Scalars can be provided support for using the [GraphQL Scalar Input Plugins](./src/plugins/graphql-scalar-inputs/README.md). \ No newline at end of file diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md index 8162af0..cbbd9a3 100644 --- a/src/plugins/graphql-scalar-inputs/README.md +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -1,6 +1,7 @@ + # GraphQL Scalar Input Plugins -These allow custom handling of custom GraphQL scalars +These allow custom handling of custom GraphQL scalars. # Definition @@ -17,8 +18,62 @@ export default { } ``` -# Examples +## Examples See the bundled `DateInput` plugin. -When instantiating the GraphiQL Explorer, pass the list of desired plugins as the prop `graphqlCustomScalarPlugins`. \ No newline at end of file +When instantiating the GraphiQL Explorer, pass the list of desired plugins as the prop `graphqlCustomScalarPlugins`. + +# Usage + +## Enabling bundled plugins + +By default, these plugins are disabled. To enable the bundled plugins, instantiate the explorer with the prop `enableBundledPlugins` set to `true`. + +## Example adding custom plugins + +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. + +```js +// Note this is just an example using a third-party plugin. This is for illustrative purposes only +const anotherThirdPartyHandler = require('') + +const customISODateTypeHandler = { + render: (arg, styleConfig, onChangeHandler) => ( + { + // Set the date placed into the query to be an ISO dateTime string + const isoDate = new Date(event.target.value).toISOString(); + event.target = { value: isoDate }; + return onChangeHandler(event); + }} + /> + ), + canProcess: (arg) => arg && arg.type && arg.type.name === 'Date', + name: 'customDate', +} + +const configuredPlugins = [anotherThirdPartyHandler, customISODateTypeHandler] +``` +Then later, in your render method where you create the explorer... +``` + + this._graphiql.handleRunQuery(operationName) + } + explorerIsOpen={this.state.explorerIsOpen} + onToggleExplorer={this._handleToggleExplorer} + getDefaultScalarArgValue={getDefaultScalarArgValue} + makeDefaultArg={makeDefaultArg} + enableBundledPlugins={true} + graphqlCustomScalarPlugins={configuredPlugins} +/> +``` +> To see examples of instantiating the explorer, see the [example repo](https://github.com/OneGraph/graphiql-explorer-example). From ce1453fc72d990b1ebab939f126d473577bdfc23 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sat, 1 Feb 2020 22:17:07 +0000 Subject: [PATCH 07/21] [bugfix] only disable the bundled plugins - Initially had the whole plugin toggled. --- src/plugins/graphql-scalar-inputs/index.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/plugins/graphql-scalar-inputs/index.js b/src/plugins/graphql-scalar-inputs/index.js index 952c1b6..5b5e0b6 100644 --- a/src/plugins/graphql-scalar-inputs/index.js +++ b/src/plugins/graphql-scalar-inputs/index.js @@ -4,7 +4,6 @@ const bundledPlugins = [DatePlugin]; class ScalarInputPluginManager { constructor(plugins = [], enableBundledPlugins = false) { - this.isEnabled = enableBundledPlugins; let enabledPlugins = plugins; if (enableBundledPlugins) { // ensure bundled plugins are the last plugins checked. @@ -13,11 +12,7 @@ class ScalarInputPluginManager { this.plugins = enabledPlugins; } - process(arg, styleConfig, onChangeHandler) { - if (!this.isEnabled) { - return null; - } - + process(arg, styleConfig, onChangeHandler) { // plugins are provided in order, the first matching plugin will be used. const handler = this.plugins.find(plugin => plugin.canProcess(arg)); if (handler) { From 47f6baef6a87b13d8365a0b448a429d6b01b1d79 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sun, 2 Feb 2020 11:06:59 +0000 Subject: [PATCH 08/21] typo --- src/plugins/graphql-scalar-inputs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md index cbbd9a3..8df3e34 100644 --- a/src/plugins/graphql-scalar-inputs/README.md +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -5,7 +5,7 @@ These allow custom handling of custom GraphQL scalars. # Definition -A GraphQL Scalar Input pluging implements the following function signatures: +A GraphQL Scalar Input plugin implements the following function signatures: ```js function canProcess(arg): boolean From 7673dc169b8a2f3b307b3f15e4ba5d0017cb3ed9 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sun, 2 Feb 2020 11:17:20 +0000 Subject: [PATCH 09/21] Date -> DateTime --- src/plugins/graphql-scalar-inputs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md index 8df3e34..fca4163 100644 --- a/src/plugins/graphql-scalar-inputs/README.md +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -43,7 +43,7 @@ const anotherThirdPartyHandler = require(' ( { // Set the date placed into the query to be an ISO dateTime string @@ -53,7 +53,7 @@ const customISODateTypeHandler = { }} /> ), - canProcess: (arg) => arg && arg.type && arg.type.name === 'Date', + canProcess: (arg) => arg && arg.type && arg.type.name === 'DateTime', name: 'customDate', } From c48ce5f28efc052a16601f32b216828a8486f740 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sun, 2 Feb 2020 11:27:54 +0000 Subject: [PATCH 10/21] [bugfix] if manager is null explorer could break --- src/Explorer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer.js b/src/Explorer.js index 955b55a..4c9e8d2 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -830,7 +830,7 @@ class AbstractArgView extends React.PureComponent { /* TODO: handle List types*/ const argType = unwrapInputType(arg.type); - let input = scalarInputsPluginManager.process(arg, styleConfig, this.props.setArgValue) + let input = scalarInputsPluginManager && scalarInputsPluginManager.process(arg, styleConfig, this.props.setArgValue) if (!input) { input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig); } From ae727bd2a1c5358f0885cdb42bacdf3ee9d4f890 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sun, 2 Feb 2020 13:19:21 +0000 Subject: [PATCH 11/21] show checkbox if rendered by plugin --- src/Explorer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Explorer.js b/src/Explorer.js index 4c9e8d2..64aa386 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -831,7 +831,8 @@ class AbstractArgView extends React.PureComponent { const argType = unwrapInputType(arg.type); let input = scalarInputsPluginManager && scalarInputsPluginManager.process(arg, styleConfig, this.props.setArgValue) - if (!input) { + let usedDefaultRender = !input; + if (usedDefaultRender) { input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig); } @@ -848,7 +849,7 @@ class AbstractArgView extends React.PureComponent { - {isInputObjectType(argType) ? ( + {usedDefaultRender && isInputObjectType(argType) ? ( {!!argValue ? this.props.styleConfig.arrowOpen From 0180b44cda8cd5eaca4e8c4753bea5623c9d0339 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sun, 2 Feb 2020 16:17:22 +0000 Subject: [PATCH 12/21] Add support for complex types --- src/Explorer.js | 4 +- src/plugins/graphql-scalar-inputs/README.md | 148 ++++++++++++------ .../bundled/DateInput.js | 6 +- src/plugins/graphql-scalar-inputs/index.js | 6 +- 4 files changed, 107 insertions(+), 57 deletions(-) diff --git a/src/Explorer.js b/src/Explorer.js index 64aa386..fc8cb93 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -638,6 +638,8 @@ class ArgView extends React.PureComponent { onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} scalarInputsPluginManager={this.props.scalarInputsPluginManager} + modifyArguments={this.props.modifyArguments} + selection={this.props.selection} /> ); } @@ -830,7 +832,7 @@ class AbstractArgView extends React.PureComponent { /* TODO: handle List types*/ const argType = unwrapInputType(arg.type); - let input = scalarInputsPluginManager && scalarInputsPluginManager.process(arg, styleConfig, this.props.setArgValue) + let input = scalarInputsPluginManager && scalarInputsPluginManager.process(this.props) let usedDefaultRender = !input; if (usedDefaultRender) { input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig); diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md index fca4163..f75b88f 100644 --- a/src/plugins/graphql-scalar-inputs/README.md +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -5,26 +5,15 @@ These allow custom handling of custom GraphQL scalars. # Definition -A GraphQL Scalar Input plugin implements the following function signatures: +A GraphQL Scalar Input plugin must implement the following: ```js -function canProcess(arg): boolean +function canProcess(arg): Boolean! +function render(prop): React.Element! -function render(arg, styleConfig, onChangeHandler): void - -export default { - canProcess, - render, - name: "InputHandlerName" -} +const name: String! ``` -## Examples - -See the bundled `DateInput` plugin. - -When instantiating the GraphiQL Explorer, pass the list of desired plugins as the prop `graphqlCustomScalarPlugins`. - -# Usage +# Usage ## Enabling bundled plugins @@ -37,43 +26,102 @@ 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. ```js -// Note this is just an example using a third-party plugin. This is for illustrative purposes only -const anotherThirdPartyHandler = require('') - -const customISODateTypeHandler = { - render: (arg, styleConfig, onChangeHandler) => ( - { - // Set the date placed into the query to be an ISO dateTime string - const isoDate = new Date(event.target.value).toISOString(); - event.target = { value: isoDate }; - return onChangeHandler(event); - }} - /> - ), - canProcess: (arg) => arg && arg.type && arg.type.name === 'DateTime', - name: 'customDate', -} +import ComplexNumberHandler from 'path/to/module'; +const configuredPlugins = [ComplexNumberHandler]; -const configuredPlugins = [anotherThirdPartyHandler, customISODateTypeHandler] -``` -Then later, in your render method where you create the explorer... -``` +// Then later, in your render method where you create the explorer... - this._graphiql.handleRunQuery(operationName) - } - explorerIsOpen={this.state.explorerIsOpen} - onToggleExplorer={this._handleToggleExplorer} - getDefaultScalarArgValue={getDefaultScalarArgValue} - makeDefaultArg={makeDefaultArg} - enableBundledPlugins={true} + ... graphqlCustomScalarPlugins={configuredPlugins} /> ``` > To see examples of instantiating the explorer, see the [example repo](https://github.com/OneGraph/graphiql-explorer-example). + +For an example implementation of a third-party plugin, see the [Complex Numbers Example]('#complex-numbers). + +## Examples + +### 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. + +The `ComplexNumber` is defined as: +```js +{ + real: Float! + imaginary: Float! +} +``` + +```js +class ComplexNumberHandler extends React.Component { + static canProcess = (arg) => arg && arg.type && arg.type.name === 'ComplexNumber'; + + updateComponent(arg, value, targetName) { + // Actually need to update the appropriate child node + const updatedFields = arg.value.fields.map(childArg => { + // targetName is either 'real' or 'imaginary', skip the other children we don't want to change + 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 } = this.props; + return modifyArguments( + (selection.arguments || []).map(originalArg => { + if (originalArg.name.value !== arg.name) { + return originalArg; + } + + return this.updateComponent(originalArg, event.target.value, complexComponent); + }) + ); + } + + render() { + return ( + + this.handleChangeEvent(e, 'real')} + style={{ maxWidth: '35px', margin: '5px' }} + step='0.1' + /> + ± + this.handleChangeEvent(e, 'imaginary')} + style={{ maxWidth: '35px', margin: '5px' }} + step='0.1' + /> i + ); + } +} + + +export default { + canProcess: ComplexNumberHandler.canProcess, + name: 'Complex Number', + render: props => ( + ), +} +``` diff --git a/src/plugins/graphql-scalar-inputs/bundled/DateInput.js b/src/plugins/graphql-scalar-inputs/bundled/DateInput.js index eb1a538..75e2967 100644 --- a/src/plugins/graphql-scalar-inputs/bundled/DateInput.js +++ b/src/plugins/graphql-scalar-inputs/bundled/DateInput.js @@ -4,12 +4,12 @@ function canProcess(arg) { return arg && arg.type && arg.type.name === 'Date'; } -function render(arg, styleConfig, onChangeHandler) { +function render(props) { return ( ); } diff --git a/src/plugins/graphql-scalar-inputs/index.js b/src/plugins/graphql-scalar-inputs/index.js index 5b5e0b6..290a9d8 100644 --- a/src/plugins/graphql-scalar-inputs/index.js +++ b/src/plugins/graphql-scalar-inputs/index.js @@ -12,11 +12,11 @@ class ScalarInputPluginManager { this.plugins = enabledPlugins; } - process(arg, styleConfig, onChangeHandler) { + process(props) { // plugins are provided in order, the first matching plugin will be used. - const handler = this.plugins.find(plugin => plugin.canProcess(arg)); + const handler = this.plugins.find(plugin => plugin.canProcess(props.arg)); if (handler) { - return handler.render(arg, styleConfig, onChangeHandler); + return handler.render(props); } return null; } From 433d7024aed1c707a23bf4425ec6d42644bd9ff1 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sun, 2 Feb 2020 18:04:10 +0000 Subject: [PATCH 13/21] update example inputs when query text changes - changes in editor now reflected in inputs --- src/plugins/graphql-scalar-inputs/README.md | 51 ++++++++++++--------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md index f75b88f..09ad375 100644 --- a/src/plugins/graphql-scalar-inputs/README.md +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -61,12 +61,10 @@ The `ComplexNumber` is defined as: ```js class ComplexNumberHandler extends React.Component { - static canProcess = (arg) => arg && arg.type && arg.type.name === 'ComplexNumber'; + static canProcess = (arg) => arg && arg.type && arg.type.name === 'ComplexNumberInput'; updateComponent(arg, value, targetName) { - // Actually need to update the appropriate child node const updatedFields = arg.value.fields.map(childArg => { - // targetName is either 'real' or 'imaginary', skip the other children we don't want to change if (childArg.name.value !== targetName) { return childArg; } @@ -81,47 +79,58 @@ class ComplexNumberHandler extends React.Component { } handleChangeEvent(event, complexComponent) { - const { arg, selection, modifyArguments } = this.props; - return modifyArguments( - (selection.arguments || []).map(originalArg => { - if (originalArg.name.value !== arg.name) { - return originalArg; - } - - return this.updateComponent(originalArg, event.target.value, 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: '35px', margin: '5px' }} - step='0.1' + style={{ maxWidth: '50px', margin: '5px' }} + step='any' /> ± this.handleChangeEvent(e, 'imaginary')} - style={{ maxWidth: '35px', margin: '5px' }} - step='0.1' + style={{ maxWidth: '50px', margin: '5px' }} + step='any' /> i ); } } - export default { canProcess: ComplexNumberHandler.canProcess, name: 'Complex Number', render: props => ( ), } ``` From 48ae2f01c1df34f78113942e21851d43258c7a1d Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sun, 2 Feb 2020 18:04:44 +0000 Subject: [PATCH 14/21] disable example input plugin if not selected --- src/plugins/graphql-scalar-inputs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md index 09ad375..62dbcea 100644 --- a/src/plugins/graphql-scalar-inputs/README.md +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -112,6 +112,7 @@ class ComplexNumberHandler extends React.Component { onChange={e => 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 ); } From 7d597f727390e734f9ef8d3eca6da177c24244a4 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sun, 2 Feb 2020 18:14:12 +0000 Subject: [PATCH 15/21] typo --- src/plugins/graphql-scalar-inputs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md index 62dbcea..6f8c77f 100644 --- a/src/plugins/graphql-scalar-inputs/README.md +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -114,8 +114,8 @@ class ComplexNumberHandler extends React.Component { step='any' disabled={!selectedComplexArg} /> - ± - this.handleChangeEvent(e, 'imaginary')} From ead2b1db10169658d9148bf994aedacec133016d Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sun, 2 Feb 2020 18:18:02 +0000 Subject: [PATCH 16/21] update plugin readme --- src/plugins/graphql-scalar-inputs/README.md | 62 +++++++++++++-------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md index 6f8c77f..0e9d3f7 100644 --- a/src/plugins/graphql-scalar-inputs/README.md +++ b/src/plugins/graphql-scalar-inputs/README.md @@ -19,26 +19,6 @@ const name: String! By default, these plugins are disabled. To enable the bundled plugins, instantiate the explorer with the prop `enableBundledPlugins` set to `true`. -## Example adding custom plugins - -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. - -```js -import ComplexNumberHandler from 'path/to/module'; -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). - -For an example implementation of a third-party plugin, see the [Complex Numbers Example]('#complex-numbers). - ## Examples ### Date Input @@ -51,15 +31,35 @@ When instantiating the GraphiQL Explorer, pass the list of desired plugins as th This examples shows a plugin that can be used for more complicated input types which should form objects. -The `ComplexNumber` is defined as: -```js -{ +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'; @@ -136,3 +136,19 @@ export default { />), } ``` +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. From 064c5791357b6674bea9a88f843559e8909319f1 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Wed, 22 Apr 2020 08:49:20 +0100 Subject: [PATCH 17/21] Merge changes from master --- package-lock.json | 2 +- package.json | 4 +- src/Explorer.js | 1019 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 911 insertions(+), 114 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5fc188..b2c701f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "graphiql-explorer", - "version": "0.5.1", + "version": "0.6.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 19036ed..44ae41f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphiql-explorer", - "version": "0.5.1", + "version": "0.6.1", "homepage": "https://github.com/onegraph/graphiql-explorer", "bugs": { "url": "https://github.com/onegraph/graphiql-explorer/issues" @@ -76,8 +76,6 @@ "scripts": { "build": "bash ./resources/build.sh", "check": "flow check", - "prepublish": ". ./resources/prepublish.sh", - "preversion": ". ./resources/checkgit.sh && npm test", "test": "npm run check && npm run build" } } diff --git a/src/Explorer.js b/src/Explorer.js index fc8cb93..638b134 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -26,12 +26,16 @@ import { isWrappingType, parse, print, + parseType, + visit, } from 'graphql'; import type { ArgumentNode, + DefinitionNode, DocumentNode, FieldNode, + FragmentSpread, GraphQLArgument, GraphQLEnumType, GraphQLField, @@ -48,6 +52,7 @@ import type { ObjectValueNode, SelectionNode, SelectionSetNode, + VariableDefinitionNode, ValueNode, } from 'graphql'; @@ -87,6 +92,7 @@ type StyleMap = { type Styles = { explorerActionsStyle: StyleMap, buttonStyle: StyleMap, + actionButtonStyle: StyleMap, }; type StyleConfig = { @@ -118,6 +124,7 @@ type Props = { styles?: ?{ explorerActionsStyle?: StyleMap, buttonStyle?: StyleMap, + actionButtonStyle?: StyleMap, }, showAttribution: boolean, }; @@ -133,6 +140,8 @@ type State = {| type Selections = $ReadOnlyArray; +type AvailableFragments = {[key: string]: FragmentDefinitionNode}; + function capitalize(string) { return string.charAt(0).toUpperCase() + string.slice(1); } @@ -273,9 +282,12 @@ function unwrapInputType(inputType: GraphQLInputType): * { function coerceArgValue( argType: GraphQLScalarType | GraphQLEnumType, - value: string, + value: string | VariableDefinitionNode, ): ValueNode { - if (isScalarType(argType)) { + // Handle the case where we're setting a variable as the value + if (typeof value !== 'string' && value.kind === 'VariableDefinition') { + return value.variable; + } else if (isScalarType(argType)) { try { switch (argType.name) { case 'String': @@ -335,11 +347,16 @@ type InputArgViewProps = {| arg: GraphQLArgument, selection: ObjectValueNode, parentField: Field, - modifyFields: (fields: $ReadOnlyArray) => void, + modifyFields: ( + fields: $ReadOnlyArray, + commit: boolean, + ) => DocumentNode | null, getDefaultScalarArgValue: GetDefaultScalarArgValue, makeDefaultArg: ?MakeDefaultArg, onRunOperation: void => void, styleConfig: StyleConfig, + onCommit: (newDoc: DocumentNode) => void, + definition: FragmentDefinitionNode | OperationDefinitionNode, |}; class InputArgView extends React.PureComponent { @@ -356,6 +373,7 @@ class InputArgView extends React.PureComponent { this._previousArgSelection = argSelection; this.props.modifyFields( selection.fields.filter(field => field !== argSelection), + true, ); }; @@ -398,38 +416,89 @@ class InputArgView extends React.PureComponent { if (!argSelection) { console.error('Unable to add arg for argType', argType); } else { - this.props.modifyFields([...(selection.fields || []), argSelection]); + return this.props.modifyFields( + [...(selection.fields || []), argSelection], + true, + ); } }; - _setArgValue = event => { + _setArgValue = (event, options: ?{commit: boolean}) => { + let settingToNull = false; + let settingToVariable = false; + let settingToLiteralValue = false; + try { + if (event.kind === 'VariableDefinition') { + settingToVariable = true; + } else if (event === null || typeof event === 'undefined') { + settingToNull = true; + } else if (typeof event.kind === 'string') { + settingToLiteralValue = true; + } + } catch (e) {} + 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'); + + const handleable = + isLeafType(argType) || + settingToVariable || + settingToNull || + settingToLiteralValue; + + if (!handleable) { + console.warn( + 'Unable to handle non leaf types in InputArgView.setArgValue', + event, + ); return; } - const targetValue = event.target.value; + let targetValue: string | VariableDefinitionNode; + let value: ?ValueNode; + + if (event === null || typeof event === 'undefined') { + value = null; + } else if ( + !event.target && + !!event.kind && + event.kind === 'VariableDefinition' + ) { + targetValue = event; + value = targetValue.variable; + } else if (typeof event.kind === 'string') { + value = event; + } else if (event.target && typeof event.target.value === 'string') { + targetValue = event.target.value; + value = coerceArgValue(argType, targetValue); + } - this.props.modifyFields( - (selection.fields || []).map(field => - field === argSelection + const newDoc = this.props.modifyFields( + (selection.fields || []).map(field => { + const isTarget = field === argSelection; + const newField = isTarget ? { ...field, - value: coerceArgValue(argType, targetValue), + value: value, } - : field, - ), + : field; + + return newField; + }), + options, ); + + return newDoc; }; _modifyChildFields = fields => { - this.props.modifyFields( + return this.props.modifyFields( this.props.selection.fields.map(field => field.name.value === this.props.arg.name ? { @@ -441,6 +510,7 @@ class InputArgView extends React.PureComponent { } : field, ), + true, ); }; @@ -461,6 +531,8 @@ class InputArgView extends React.PureComponent { makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + onCommit={this.props.onCommit} + definition={this.props.definition} /> ); } @@ -470,11 +542,16 @@ type ArgViewProps = {| parentField: Field, arg: GraphQLArgument, selection: FieldNode, - modifyArguments: (argumentNodes: $ReadOnlyArray) => void, + modifyArguments: ( + argumentNodes: $ReadOnlyArray, + commit: boolean, + ) => DocumentNode | null, getDefaultScalarArgValue: GetDefaultScalarArgValue, makeDefaultArg: ?MakeDefaultArg, onRunOperation: void => void, styleConfig: StyleConfig, + onCommit: (newDoc: DocumentNode) => void, + definition: FragmentDefinitionNode | OperationDefinitionNode, |}; type ArgViewState = {||}; @@ -517,15 +594,16 @@ class ArgView extends React.PureComponent { arg => arg.name.value === this.props.arg.name, ); }; - _removeArg = () => { + _removeArg = (commit: boolean): DocumentNode | null => { const {selection} = this.props; const argSelection = this._getArgSelection(); this._previousArgSelection = argSelection; - this.props.modifyArguments( + return this.props.modifyArguments( (selection.arguments || []).filter(arg => arg !== argSelection), + commit, ); }; - _addArg = () => { + _addArg = (commit: boolean): DocumentNode | null => { const { selection, getDefaultScalarArgValue, @@ -563,41 +641,78 @@ class ArgView extends React.PureComponent { if (!argSelection) { console.error('Unable to add arg for argType', argType); + return null; } else { - this.props.modifyArguments([ - ...(selection.arguments || []), - argSelection, - ]); + return this.props.modifyArguments( + [...(selection.arguments || []), argSelection], + commit, + ); } }; - _setArgValue = event => { + _setArgValue = ( + event: SyntheticInputEvent<*> | VariableDefinitionNode, + options: ?{commit: boolean}, + ) => { + let settingToNull = false; + let settingToVariable = false; + let settingToLiteralValue = false; + try { + if (event.kind === 'VariableDefinition') { + settingToVariable = true; + } else if (event === null || typeof event === 'undefined') { + settingToNull = true; + } else if (typeof event.kind === 'string') { + settingToLiteralValue = true; + } + } catch (e) {} const {selection} = this.props; const argSelection = this._getArgSelection(); - if (!argSelection) { + if (!argSelection && !settingToVariable) { 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'); + + const handleable = + isLeafType(argType) || + settingToVariable || + settingToNull || + settingToLiteralValue; + + if (!handleable) { + console.warn('Unable to handle non leaf types in ArgView._setArgValue'); return; } - const targetValue = event.target.value; + let targetValue: string | VariableDefinitionNode; + let value: ValueNode; + + if (event === null || typeof event === 'undefined') { + value = null; + } else if (event.target && typeof event.target.value === 'string') { + targetValue = event.target.value; + value = coerceArgValue(argType, targetValue); + } else if (!event.target && event.kind === 'VariableDefinition') { + targetValue = event; + value = targetValue.variable; + } else if (typeof event.kind === 'string') { + value = event; + } - this.props.modifyArguments( + return this.props.modifyArguments( (selection.arguments || []).map(a => a === argSelection ? { ...a, - value: coerceArgValue(argType, targetValue), + value: value, } : a, ), + options, ); }; - _setArgFields = fields => { + _setArgFields = (fields, commit: boolean): DocumentNode | null => { const {selection} = this.props; const argSelection = this._getArgSelection(); if (!argSelection) { @@ -605,7 +720,7 @@ class ArgView extends React.PureComponent { return; } - this.props.modifyArguments( + return this.props.modifyArguments( (selection.arguments || []).map(a => a === argSelection ? { @@ -617,6 +732,7 @@ class ArgView extends React.PureComponent { } : a, ), + commit, ); }; @@ -640,33 +756,29 @@ class ArgView extends React.PureComponent { scalarInputsPluginManager={this.props.scalarInputsPluginManager} modifyArguments={this.props.modifyArguments} selection={this.props.selection} + onCommit={this.props.onCommit} + definition={this.props.definition} /> ); } } function isRunShortcut(event) { - return event.metaKey && event.key === 'Enter'; + return event.ctrlKey && event.key === 'Enter'; } -type AbstractArgViewProps = {| - argValue: ?ValueNode, - arg: GraphQLArgument, - parentField: Field, - setArgValue: (event: SyntheticInputEvent<*>) => void, - setArgFields: (fields: $ReadOnlyArray) => void, - addArg: () => void, - removeArg: () => void, - getDefaultScalarArgValue: GetDefaultScalarArgValue, - makeDefaultArg: ?MakeDefaultArg, - onRunOperation: void => void, - styleConfig: StyleConfig, -|}; +function canRunOperation(operationName) { + // it does not make sense to try to execute a fragment + return operationName !== 'FragmentDefinition'; +} type ScalarInputProps = {| arg: GraphQLArgument, argValue: ValueNode, - setArgValue: (event: SyntheticInputEvent<*>) => void, + setArgValue: ( + SyntheticInputEvent<*> | VariableDefinitionNode, + commit: boolean, + ) => DocumentNode | null, onRunOperation: void => void, styleConfig: StyleConfig, |}; @@ -674,7 +786,7 @@ type ScalarInputProps = {| class ScalarInput extends React.PureComponent { _ref: ?any; _handleChange = event => { - this.props.setArgValue(event); + this.props.setArgValue(event, true); }; componentDidMount() { @@ -706,18 +818,13 @@ class ScalarInput extends React.PureComponent { border: 'none', borderBottom: '1px solid #888', outline: 'none', - width: `${Math.max(1, value.length)}ch`, + width: `${Math.max(1, Math.min(15, 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} /> @@ -727,7 +834,34 @@ class ScalarInput extends React.PureComponent { } } -class AbstractArgView extends React.PureComponent { +type AbstractArgViewProps = {| + argValue: ?ValueNode, + arg: GraphQLArgument, + parentField: Field, + setArgValue: ( + SyntheticInputEvent<*> | VariableDefinitionNode, + commit: boolean, + ) => DocumentNode | null, + setArgFields: ( + fields: $ReadOnlyArray, + commit: boolean, + ) => DocumentNode | null, + addArg: (commit: boolean) => DocumentNode | null, + removeArg: (commit: boolean) => DocumentNode | null, + onCommit: (newDoc: DocumentNode) => void, + getDefaultScalarArgValue: GetDefaultScalarArgValue, + makeDefaultArg: ?MakeDefaultArg, + onRunOperation: void => void, + styleConfig: StyleConfig, + definition: FragmentDefinitionNode | OperationDefinitionNode, +|}; + +class AbstractArgView extends React.PureComponent< + AbstractArgViewProps, + {|displayArgActions: boolean|}, +> { + state = {displayArgActions: false}; + defaultArgViewHandler(arg, argType, argValue, styleConfig) { let input = null; if (argValue) { @@ -811,6 +945,8 @@ class AbstractArgView extends React.PureComponent { makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + onCommit={this.props.onCommit} + definition={this.props.definition} /> ))}
@@ -838,6 +974,254 @@ class AbstractArgView extends React.PureComponent { input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig); } + const variablize = () => { + /** + 1. Find current operation variables + 2. Find current arg value + 3. Create a new variable + 4. Replace current arg value with variable + 5. Add variable to operation + */ + + const baseVariableName = arg.name; + const conflictingNameCount = ( + this.props.definition.variableDefinitions || [] + ).filter(varDef => + varDef.variable.name.value.startsWith(baseVariableName), + ).length; + + let variableName; + if (conflictingNameCount > 0) { + variableName = `${baseVariableName}${conflictingNameCount}`; + } else { + variableName = baseVariableName; + } + // To get an AST definition of our variable from the instantiated arg, + // we print it to a string, then parseType to get our AST. + const argPrintedType = arg.type.toString(); + const argType = parseType(argPrintedType); + + const base: VariableDefinitionNode = { + kind: 'VariableDefinition', + variable: { + kind: 'Variable', + name: { + kind: 'Name', + value: variableName, + }, + }, + type: argType, + directives: [], + }; + + const variableDefinitionByName = name => + (this.props.definition.variableDefinitions || []).find( + varDef => varDef.variable.name.value === name, + ); + + let variable: ?VariableDefinitionNode; + + let subVariableUsageCountByName: { + [key: string]: number, + } = {}; + + if (typeof argValue !== 'undefined' && argValue !== null) { + /** In the process of devariabilizing descendent selections, + * we may have caused their variable definitions to become unused. + * Keep track and remove any variable definitions with 1 or fewer usages. + * */ + const cleanedDefaultValue = visit(argValue, { + Variable(node) { + const varName = node.name.value; + const varDef = variableDefinitionByName(varName); + + subVariableUsageCountByName[varName] = + subVariableUsageCountByName[varName] + 1 || 1; + + if (!varDef) { + return; + } + + return varDef.defaultValue; + }, + }); + + const isNonNullable = base.type.kind === 'NonNullType'; + + // We're going to give the variable definition a default value, so we must make its type nullable + const unwrappedBase = isNonNullable + ? {...base, type: base.type.type} + : base; + + variable = {...unwrappedBase, defaultValue: cleanedDefaultValue}; + } else { + variable = base; + } + + const newlyUnusedVariables = Object.entries(subVariableUsageCountByName) + // $FlowFixMe: Can't get Object.entries to realize usageCount *must* be a number + .filter(([_, usageCount]: [string, number]) => usageCount < 2) + .map(([varName: string, _]) => varName); + + if (variable) { + const newDoc: ?DocumentNode = this.props.setArgValue(variable, false); + + if (newDoc) { + const targetOperation = newDoc.definitions.find(definition => { + if ( + !!definition.operation && + !!definition.name && + !!definition.name.value && + // + !!this.props.definition.name && + !!this.props.definition.name.value + ) { + return definition.name.value === this.props.definition.name.value; + } else { + return false; + } + }); + + const newVariableDefinitions: Array = [ + ...(targetOperation.variableDefinitions || []), + variable, + ].filter( + varDef => + newlyUnusedVariables.indexOf(varDef.variable.name.value) === -1, + ); + + const newOperation = { + ...targetOperation, + variableDefinitions: newVariableDefinitions, + }; + + const existingDefs = newDoc.definitions; + + const newDefinitions = existingDefs.map(existingOperation => { + if (targetOperation === existingOperation) { + return newOperation; + } else { + return existingOperation; + } + }); + + const finalDoc = { + ...newDoc, + definitions: newDefinitions, + }; + + this.props.onCommit(finalDoc); + } + } + }; + + const devariablize = () => { + /** + * 1. Find the current variable definition in the operation def + * 2. Extract its value + * 3. Replace the current arg value + * 4. Visit the resulting operation to see if there are any other usages of the variable + * 5. If not, remove the variableDefinition + */ + if (!argValue || !argValue.name || !argValue.name.value) { + return; + } + + const variableName = argValue.name.value; + const variableDefinition = ( + this.props.definition.variableDefinitions || [] + ).find(varDef => varDef.variable.name.value === variableName); + + if (!variableDefinition) { + return; + } + + const defaultValue = variableDefinition.defaultValue; + + const newDoc: ?DocumentNode = this.props.setArgValue(defaultValue, { + commit: false, + }); + + if (newDoc) { + const targetOperation: ?OperationDefinitionNode = newDoc.definitions.find( + definition => + definition.name.value === this.props.definition.name.value, + ); + + if (!targetOperation) { + return; + } + + // After de-variabilizing, see if the variable is still in use. If not, remove it. + let variableUseCount = 0; + + visit(targetOperation, { + Variable(node) { + if (node.name.value === variableName) { + variableUseCount = variableUseCount + 1; + } + }, + }); + + let newVariableDefinitions = targetOperation.variableDefinitions || []; + + // A variable is in use if it shows up at least twice (once in the definition, once in the selection) + if (variableUseCount < 2) { + newVariableDefinitions = newVariableDefinitions.filter( + varDef => varDef.variable.name.value !== variableName, + ); + } + + const newOperation = { + ...targetOperation, + variableDefinitions: newVariableDefinitions, + }; + + const existingDefs = newDoc.definitions; + + const newDefinitions = existingDefs.map(existingOperation => { + if (targetOperation === existingOperation) { + return newOperation; + } else { + return existingOperation; + } + }); + + const finalDoc = { + ...newDoc, + definitions: newDefinitions, + }; + + this.props.onCommit(finalDoc); + } + }; + + const isArgValueVariable = argValue && argValue.kind === 'Variable'; + + const variablizeActionButton = !this.state.displayArgActions ? null : ( + + ); + return (
{ userSelect: 'none', }} data-arg-name={arg.name} - data-arg-type={argType.name}> + data-arg-type={argType.name} + className={`graphiql-explorer-${arg.name}`}> + onClick={event => { + const shouldAdd = !argValue; + if (shouldAdd) { + this.props.addArg(true); + } else { + this.props.removeArg(true); + } + this.setState({displayArgActions: shouldAdd}); + }}> {usedDefaultRender && isInputObjectType(argType) ? ( {!!argValue @@ -865,12 +1258,19 @@ class AbstractArgView extends React.PureComponent { )} + title={arg.description} + onMouseEnter={() => { + // Make implementation a bit easier and only show 'variablize' action if arg is already added + if (argValue !== null && typeof argValue !== 'undefined') { + this.setState({displayArgActions: true}); + } + }} + onMouseLeave={() => this.setState({displayArgActions: false})}> {arg.name} - {isRequiredArgument(arg) ? '*' : ''}: - - {' '} - {input || } + {isRequiredArgument(arg) ? '*' : ''}: {variablizeActionButton}{' '} + {' '} + + {input || }{' '}
); } @@ -879,13 +1279,18 @@ class AbstractArgView extends React.PureComponent { type AbstractViewProps = {| implementingType: GraphQLObjectType, selections: Selections, - modifySelections: (selections: Selections) => void, + modifySelections: ( + selections: Selections, + ?{commit: boolean}, + ) => DocumentNode | null, schema: GraphQLSchema, getDefaultFieldNames: (type: GraphQLObjectType) => Array, getDefaultScalarArgValue: GetDefaultScalarArgValue, makeDefaultArg: ?MakeDefaultArg, onRunOperation: void => void, + onCommit: (newDoc: DocumentNode) => void, styleConfig: StyleConfig, + definition: FragmentDefinitionNode | OperationDefinitionNode, |}; class AbstractView extends React.PureComponent { @@ -933,9 +1338,12 @@ class AbstractView extends React.PureComponent { } }; - _modifyChildSelections = (selections: Selections) => { + _modifyChildSelections = ( + selections: Selections, + options: ?{commit: boolean}, + ): DocumentNode | null => { const thisSelection = this._getSelection(); - this.props.modifySelections( + return this.props.modifySelections( this.props.selections.map(selection => { if (selection === thisSelection) { return { @@ -953,6 +1361,7 @@ class AbstractView extends React.PureComponent { } return selection; }), + options, ); }; @@ -970,8 +1379,9 @@ class AbstractView extends React.PureComponent { ? selection.selectionSet.selections : [] : []; + return ( -
+
@@ -998,7 +1408,10 @@ class AbstractView extends React.PureComponent { getDefaultScalarArgValue={this.props.getDefaultScalarArgValue} makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} + onCommit={this.props.onCommit} styleConfig={this.props.styleConfig} + definition={this.props.definition} + availableFragments={this.props.availableFragments} /> ))}
@@ -1008,16 +1421,92 @@ class AbstractView extends React.PureComponent { } } +type FragmentViewProps = {| + fragment: FragmentDefinitionNode, + selections: Selections, + modifySelections: ( + selections: Selections, + ?{commit: boolean}, + ) => DocumentNode | null, + onCommit: (newDoc: DocumentNode) => void, + schema: GraphQLSchema, + styleConfig: StyleConfig, +|}; + +class FragmentView extends React.PureComponent { + _previousSelection: ?InlineFragmentNode; + _addFragment = () => { + this.props.modifySelections([ + ...this.props.selections, + this._previousSelection || { + kind: 'FragmentSpread', + name: this.props.fragment.name, + }, + ]); + }; + _removeFragment = () => { + const thisSelection = this._getSelection(); + this._previousSelection = thisSelection; + this.props.modifySelections( + this.props.selections.filter(s => { + const isTargetSelection = + s.kind === 'FragmentSpread' && + s.name.value === this.props.fragment.name.value; + + return !isTargetSelection; + }), + ); + }; + _getSelection = (): ?FragmentSpread => { + const selection = this.props.selections.find(selection => { + return ( + selection.kind === 'FragmentSpread' && + selection.name.value === this.props.fragment.name.value + ); + }); + + return selection; + }; + + render() { + const {styleConfig} = this.props; + const selection = this._getSelection(); + return ( +
+ + + + {this.props.fragment.name.value} + + +
+ ); + } +} + type FieldViewProps = {| field: Field, selections: Selections, - modifySelections: (selections: Selections) => void, + modifySelections: ( + selections: Selections, + ?{commit: boolean}, + ) => DocumentNode | null, schema: GraphQLSchema, getDefaultFieldNames: (type: GraphQLObjectType) => Array, getDefaultScalarArgValue: GetDefaultScalarArgValue, makeDefaultArg: ?MakeDefaultArg, onRunOperation: void => void, styleConfig: StyleConfig, + onCommit: (newDoc: DocumentNode) => void, + definition: FragmentDefinitionNode | OperationDefinitionNode, + availableFragments: AvailableFragments, |}; function defaultInputObjectFields( @@ -1099,7 +1588,12 @@ function defaultArgs( return args; } -class FieldView extends React.PureComponent { +class FieldView extends React.PureComponent< + FieldViewProps, + {|displayFieldActions: boolean|}, +> { + state = {displayFieldActions: false}; + _previousSelection: ?SelectionNode; _addAllFieldsToSelections = rawSubfields => { const subFields: Array = !!rawSubfields @@ -1197,13 +1691,16 @@ class FieldView extends React.PureComponent { } }; - _setArguments = (argumentNodes: $ReadOnlyArray) => { + _setArguments = ( + argumentNodes: $ReadOnlyArray, + options: ?{commit: boolean}, + ): DocumentNode | null => { const selection = this._getSelection(); if (!selection) { console.error('Missing selection when setting arguments', argumentNodes); return; } - this.props.modifySelections( + return this.props.modifySelections( this.props.selections.map(s => s === selection ? { @@ -1216,11 +1713,15 @@ class FieldView extends React.PureComponent { } : s, ), + options, ); }; - _modifyChildSelections = (selections: Selections) => { - this.props.modifySelections( + _modifyChildSelections = ( + selections: Selections, + options: ?{commit: boolean}, + ): DocumentNode | null => { + return this.props.modifySelections( this.props.selections.map(selection => { if ( selection.kind === 'Field' && @@ -1243,6 +1744,7 @@ class FieldView extends React.PureComponent { } return selection; }), + options, ); }; @@ -1251,12 +1753,18 @@ 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'; + let className = `graphiql-explorer-node graphiql-explorer-${field.name}`; if (field.isDeprecated) { className += ' graphiql-explorer-deprecated'; } + const applicableFragments = + isObjectType(type) || isInterfaceType(type) || isUnionType(type) + ? this.props.availableFragments && + this.props.availableFragments[type.name] + : null; + const node = (
{ }} data-field-name={field.name} data-field-type={type.name} - onClick={this._handleUpdateSelections}> + onClick={this._handleUpdateSelections} + onMouseEnter={() => { + const containsMeaningfulSubselection = + isObjectType(type) && + selection && + selection.selectionSet && + selection.selectionSet.selections.filter( + selection => selection.kind !== 'FragmentSpread', + ).length > 0; + + if (containsMeaningfulSubselection) { + this.setState({displayFieldActions: true}); + } + }} + onMouseLeave={() => this.setState({displayFieldActions: false})}> {isObjectType(type) ? ( {!!selection @@ -1290,9 +1812,95 @@ class FieldView extends React.PureComponent { className="graphiql-explorer-field-view"> {field.name} + {!this.state.displayFieldActions ? null : ( + + )} {selection && args.length ? ( -
+
{args.map(arg => ( { onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} scalarInputsPluginManager={this.props.scalarInputsPluginManager} + onCommit={this.props.onCommit} + definition={this.props.definition} /> ))}
@@ -1323,9 +1933,28 @@ class FieldView extends React.PureComponent { : [] : []; return ( -
+
{node}
+ {!!applicableFragments + ? applicableFragments.map(fragment => { + const type = schema.getType( + fragment.typeCondition.name.value, + ); + const fragmentName = fragment.name.value; + return !type ? null : ( + + ); + }) + : null} {Object.keys(fields) .sort() .map(fieldName => ( @@ -1340,6 +1969,9 @@ class FieldView extends React.PureComponent { makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + onCommit={this.props.onCommit} + definition={this.props.definition} + availableFragments={this.props.availableFragments} /> ))} {isInterfaceType(type) || isUnionType(type) @@ -1359,6 +1991,8 @@ class FieldView extends React.PureComponent { makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} + onCommit={this.props.onCommit} + definition={this.props.definition} /> )) : null} @@ -1436,6 +2070,18 @@ const defaultStyles = { maxWidth: 'none', }, + actionButtonStyle: { + padding: '0px', + backgroundColor: 'white', + border: 'none', + margin: '0px', + maxWidth: 'none', + height: '15px', + width: '15px', + display: 'inline-block', + fontSize: 'smaller', + }, + explorerActionsStyle: { margin: '4px -8px -8px', paddingLeft: '8px', @@ -1452,27 +2098,38 @@ type RootViewProps = {| schema: GraphQLSchema, isLast: boolean, fields: ?GraphQLFieldMap, - operation: OperationType, + operationType: OperationType, name: ?string, onTypeName: ?string, definition: FragmentDefinitionNode | OperationDefinitionNode, onEdit: ( operationDef: ?OperationDefinitionNode | ?FragmentDefinitionNode, - ) => void, + commit: boolean, + ) => DocumentNode, + onCommit: (document: DocumentNode) => void, onOperationRename: (query: string) => void, + onOperationDestroy: () => void, + onOperationClone: () => void, onRunOperation: (name: ?string) => void, onMount: (rootViewElId: string) => void, getDefaultFieldNames: (type: GraphQLObjectType) => Array, getDefaultScalarArgValue: GetDefaultScalarArgValue, makeDefaultArg: ?MakeDefaultArg, styleConfig: StyleConfig, + availableFragments: AvailableFragments, |}; -class RootView extends React.PureComponent { - state = {newOperationType: 'query'}; +class RootView extends React.PureComponent< + RootViewProps, + {|newOperationType: NewOperationType, displayTitleActions: boolean|}, +> { + state = {newOperationType: 'query', displayTitleActions: false}; _previousOperationDef: ?OperationDefinitionNode | ?FragmentDefinitionNode; - _modifySelections = (selections: Selections) => { + _modifySelections = ( + selections: Selections, + options: ?{commit: boolean}, + ): DocumentNode => { let operationDef: FragmentDefinitionNode | OperationDefinitionNode = this .props.definition; @@ -1521,21 +2178,21 @@ class RootView extends React.PureComponent { }; } - this.props.onEdit(newOperationDef); + return this.props.onEdit(newOperationDef, options); }; _onOperationRename = event => this.props.onOperationRename(event.target.value); _handlePotentialRun = event => { - if (isRunShortcut(event)) { + if (isRunShortcut(event) && canRunOperation(this.props.definition.kind)) { this.props.onRunOperation(this.props.name); } }; _rootViewElId = () => { - const {operation, name} = this.props; - const rootViewElId = `${operation}-${name || 'unknown'}`; + const {operationType, name} = this.props; + const rootViewElId = `${operationType}-${name || 'unknown'}`; return rootViewElId; }; @@ -1547,7 +2204,7 @@ class RootView extends React.PureComponent { render() { const { - operation, + operationType, definition, schema, getDefaultFieldNames, @@ -1560,19 +2217,25 @@ class RootView extends React.PureComponent { const selections = operationDef.selectionSet.selections; const operationDisplayName = - this.props.name || `${capitalize(operation)} Name`; + this.props.name || `${capitalize(operationType)} Name`; return (
-
- {operation}{' '} +
this.setState({displayTitleActions: true})} + onMouseLeave={() => this.setState({displayTitleActions: false})}> + {operationType}{' '} { width: `${Math.max(4, operationDisplayName.length)}ch`, }} autoComplete="false" - placeholder={`${capitalize(operation)} Name`} + placeholder={`${capitalize(operationType)} Name`} value={this.props.name} onKeyDown={this._handlePotentialRun} onChange={this._onOperationRename} @@ -1597,6 +2260,30 @@ class RootView extends React.PureComponent { ) : ( '' )} + {!!this.state.displayTitleActions ? ( + + + + + ) : ( + '' + )}
{Object.keys(fields) @@ -1614,6 +2301,9 @@ class RootView extends React.PureComponent { onRunOperation={this.props.onRunOperation} styleConfig={this.props.styleConfig} scalarInputsPluginManager={this.props.scalarInputsPluginManager} + onCommit={this.props.onCommit} + definition={this.props.definition} + availableFragments={this.props.availableFragments} /> ))}
@@ -1782,6 +2472,56 @@ class Explorer extends React.PureComponent { }; }; + const cloneOperation = ( + targetOperation: OperationDefinitionNode | FragmentDefinitionNode, + ) => { + let kind; + if (targetOperation.kind === 'FragmentDefinition') { + kind = 'fragment'; + } else { + kind = targetOperation.operation; + } + + const newOperationName = + ((targetOperation.name && targetOperation.name.value) || '') + 'Copy'; + + const newName = { + kind: 'Name', + value: newOperationName, + loc: undefined, + }; + + const newOperation = {...targetOperation, name: newName}; + + const existingDefs = parsedQuery.definitions; + + const newDefinitions = [...existingDefs, newOperation]; + + this.setState({operationToScrollTo: `${kind}-${newOperationName}`}); + + return { + ...parsedQuery, + definitions: newDefinitions, + }; + }; + + const destroyOperation = targetOperation => { + const existingDefs = parsedQuery.definitions; + + const newDefinitions = existingDefs.filter(existingOperation => { + if (targetOperation === existingOperation) { + return false; + } else { + return true; + } + }); + + return { + ...parsedQuery, + definitions: newDefinitions, + }; + }; + const addOperation = (kind: NewOperationType) => { const existingDefs = parsedQuery.definitions; @@ -1937,6 +2677,26 @@ class Explorer extends React.PureComponent {
); + const availableFragments: AvailableFragments = relevantOperations.reduce( + (acc, operation) => { + if (operation.kind === 'FragmentDefinition') { + const fragmentTypeName = operation.typeCondition.name.value; + const existingFragmentsForType = acc[fragmentTypeName] || []; + const newFragmentsForType = [ + ...existingFragmentsForType, + operation, + ].sort((a, b) => a.name.value.localeCompare(b.name.value)); + return { + ...acc, + [fragmentTypeName]: newFragmentsForType, + }; + } + + return acc; + }, + {}, + ); + const attribution = this.props.showAttribution ? : null; return ( @@ -1970,7 +2730,7 @@ class Explorer extends React.PureComponent { const operationName = operation && operation.name && operation.name.value; - const operationKind = + const operationType = operation.kind === 'FragmentDefinition' ? 'fragment' : (operation && operation.operation) || 'query'; @@ -1980,6 +2740,16 @@ class Explorer extends React.PureComponent { this.props.onEdit(print(newOperationDef)); }; + const onOperationClone = () => { + const newOperationDef = cloneOperation(operation); + this.props.onEdit(print(newOperationDef)); + }; + + const onOperationDestroy = () => { + const newOperationDef = destroyOperation(operation); + this.props.onEdit(print(newOperationDef)); + }; + const fragmentType = operation.kind === 'FragmentDefinition' && operation.typeCondition.kind === 'NamedType' && @@ -1991,11 +2761,11 @@ class Explorer extends React.PureComponent { : null; const fields = - operationKind === 'query' + operationType === 'query' ? queryFields - : operationKind === 'mutation' + : operationType === 'mutation' ? mutationFields - : operationKind === 'subscription' + : operationType === 'subscription' ? subscriptionFields : operation.kind === 'FragmentDefinition' ? fragmentFields @@ -2006,31 +2776,59 @@ class Explorer extends React.PureComponent { ? operation.typeCondition.name.value : null; + const onCommit = (parsedDocument: DocumentNode) => { + const textualNewDocument = print(parsedDocument); + + this.props.onEdit(textualNewDocument); + }; + return ( { - const newQuery = { - ...parsedQuery, - definitions: parsedQuery.definitions.map( - existingDefinition => - existingDefinition === operation - ? newDefinition - : existingDefinition, - ), - }; - - const textualNewQuery = print(newQuery); - - this.props.onEdit(textualNewQuery); + onCommit={onCommit} + onEdit={( + newDefinition: ?DefinitionNode, + options: ?{commit: boolean}, + ): DocumentNode => { + let commit; + if ( + typeof options === 'object' && + typeof options.commit !== 'undefined' + ) { + commit = options.commit; + } else { + commit = true; + } + + if (!!newDefinition) { + const newQuery: DocumentNode = { + ...parsedQuery, + definitions: parsedQuery.definitions.map( + existingDefinition => + existingDefinition === operation + ? newDefinition + : existingDefinition, + ), + }; + + if (commit) { + onCommit(newQuery); + return newQuery; + } else { + return newQuery; + } + } else { + return parsedQuery; + } }} schema={schema} getDefaultFieldNames={getDefaultFieldNames} @@ -2043,6 +2841,7 @@ class Explorer extends React.PureComponent { }} styleConfig={styleConfig} scalarInputsPluginManager={this.scalarInputsPluginManager} + availableFragments={availableFragments} /> ); }, From 8062b6b1596c8e033b923f764d9dae3e83e85691 Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Wed, 22 Apr 2020 09:57:33 +0100 Subject: [PATCH 18/21] Move everything into Explorer.js following review --- README.md | 157 +++++++++++++++++- src/Explorer.js | 42 ++++- src/plugins/graphql-scalar-inputs/README.md | 154 ----------------- .../bundled/DateInput.js | 24 --- src/plugins/graphql-scalar-inputs/index.js | 25 --- 5 files changed, 195 insertions(+), 207 deletions(-) delete mode 100644 src/plugins/graphql-scalar-inputs/README.md delete mode 100644 src/plugins/graphql-scalar-inputs/bundled/DateInput.js delete mode 100644 src/plugins/graphql-scalar-inputs/index.js diff --git a/README.md b/README.md index 1ac46d5..c5a75f3 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,159 @@ Example styles map: ``` -## Handling Custom GraphQL Scalars +# Handling Custom GraphQL Scalars -Custom GraphQL Scalars can be provided support for using the [GraphQL Scalar Input Plugins](./src/plugins/graphql-scalar-inputs/README.md). \ No newline at end of file +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/src/Explorer.js b/src/Explorer.js index 638b134..c726697 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -56,8 +56,6 @@ import type { ValueNode, } from 'graphql'; -import ScalarInputPluginManager from './plugins/graphql-scalar-inputs' - type Field = GraphQLField; type GetDefaultScalarArgValue = ( @@ -2932,4 +2930,44 @@ class ExplorerWrapper extends React.PureComponent { } } +class DatePlugin { + name = 'DateInput'; + + static canProcess(arg) { + return arg && arg.type && arg.type.name === 'Date'; + } + + static render(props) { + return ( + + ); + } +} + +const bundledPlugins = [DatePlugin]; + +class ScalarInputPluginManager { + constructor(plugins = [], enableBundledPlugins = false) { + const enabledPlugins = plugins; + if (enableBundledPlugins) { + // ensure bundled plugins are the last plugins checked. + enabledPlugins.push(...bundledPlugins); + } + this.plugins = enabledPlugins; + } + + process(props) { + // plugins are provided in order, the first matching plugin will be used. + const handler = this.plugins.find(plugin => plugin.canProcess(props.arg)); + if (handler) { + return handler.render(props); + } + return null; + } +} + export default ExplorerWrapper; diff --git a/src/plugins/graphql-scalar-inputs/README.md b/src/plugins/graphql-scalar-inputs/README.md deleted file mode 100644 index 0e9d3f7..0000000 --- a/src/plugins/graphql-scalar-inputs/README.md +++ /dev/null @@ -1,154 +0,0 @@ - -# GraphQL Scalar Input Plugins - -These allow custom handling of custom GraphQL scalars. - -# Definition - -A GraphQL Scalar Input plugin must implement the following: -```js -function canProcess(arg): Boolean! -function render(prop): React.Element! - -const name: String! -``` - -# Usage - -## Enabling bundled plugins - -By default, these plugins are disabled. To enable the bundled plugins, instantiate the explorer with the prop `enableBundledPlugins` set to `true`. - -## Examples - -### 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/src/plugins/graphql-scalar-inputs/bundled/DateInput.js b/src/plugins/graphql-scalar-inputs/bundled/DateInput.js deleted file mode 100644 index 75e2967..0000000 --- a/src/plugins/graphql-scalar-inputs/bundled/DateInput.js +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; - -function canProcess(arg) { - return arg && arg.type && arg.type.name === 'Date'; -} - -function render(props) { - return ( - - ); -} - - -const DatePlugin = { - canProcess, - render, - name: 'DateInput' -}; - -export default DatePlugin; diff --git a/src/plugins/graphql-scalar-inputs/index.js b/src/plugins/graphql-scalar-inputs/index.js deleted file mode 100644 index 290a9d8..0000000 --- a/src/plugins/graphql-scalar-inputs/index.js +++ /dev/null @@ -1,25 +0,0 @@ -import DatePlugin from './bundled/DateInput' - -const bundledPlugins = [DatePlugin]; - -class ScalarInputPluginManager { - constructor(plugins = [], enableBundledPlugins = false) { - let enabledPlugins = plugins; - if (enableBundledPlugins) { - // ensure bundled plugins are the last plugins checked. - enabledPlugins.push(...bundledPlugins); - } - this.plugins = enabledPlugins; - } - - process(props) { - // plugins are provided in order, the first matching plugin will be used. - const handler = this.plugins.find(plugin => plugin.canProcess(props.arg)); - if (handler) { - return handler.render(props); - } - return null; - } -} - -export default ScalarInputPluginManager; \ No newline at end of file From f684f5c0a75d1a7b518ec6214252fe286997803b Mon Sep 17 00:00:00 2001 From: Tim Lehner Date: Sat, 16 May 2020 11:59:28 +0100 Subject: [PATCH 19/21] fix broken merge --- src/Explorer.js | 253 +----------------------------------------------- 1 file changed, 5 insertions(+), 248 deletions(-) diff --git a/src/Explorer.js b/src/Explorer.js index ebb6295..a86c897 100644 --- a/src/Explorer.js +++ b/src/Explorer.js @@ -526,6 +526,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} @@ -940,6 +941,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} @@ -1220,254 +1222,6 @@ class AbstractArgView extends React.PureComponent< ); - const variablize = () => { - /** - 1. Find current operation variables - 2. Find current arg value - 3. Create a new variable - 4. Replace current arg value with variable - 5. Add variable to operation - */ - - const baseVariableName = arg.name; - const conflictingNameCount = ( - this.props.definition.variableDefinitions || [] - ).filter(varDef => - varDef.variable.name.value.startsWith(baseVariableName), - ).length; - - let variableName; - if (conflictingNameCount > 0) { - variableName = `${baseVariableName}${conflictingNameCount}`; - } else { - variableName = baseVariableName; - } - // To get an AST definition of our variable from the instantiated arg, - // we print it to a string, then parseType to get our AST. - const argPrintedType = arg.type.toString(); - const argType = parseType(argPrintedType); - - const base: VariableDefinitionNode = { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { - kind: 'Name', - value: variableName, - }, - }, - type: argType, - directives: [], - }; - - const variableDefinitionByName = name => - (this.props.definition.variableDefinitions || []).find( - varDef => varDef.variable.name.value === name, - ); - - let variable: ?VariableDefinitionNode; - - let subVariableUsageCountByName: { - [key: string]: number, - } = {}; - - if (typeof argValue !== 'undefined' && argValue !== null) { - /** In the process of devariabilizing descendent selections, - * we may have caused their variable definitions to become unused. - * Keep track and remove any variable definitions with 1 or fewer usages. - * */ - const cleanedDefaultValue = visit(argValue, { - Variable(node) { - const varName = node.name.value; - const varDef = variableDefinitionByName(varName); - - subVariableUsageCountByName[varName] = - subVariableUsageCountByName[varName] + 1 || 1; - - if (!varDef) { - return; - } - - return varDef.defaultValue; - }, - }); - - const isNonNullable = base.type.kind === 'NonNullType'; - - // We're going to give the variable definition a default value, so we must make its type nullable - const unwrappedBase = isNonNullable - ? {...base, type: base.type.type} - : base; - - variable = {...unwrappedBase, defaultValue: cleanedDefaultValue}; - } else { - variable = base; - } - - const newlyUnusedVariables = Object.entries(subVariableUsageCountByName) - // $FlowFixMe: Can't get Object.entries to realize usageCount *must* be a number - .filter(([_, usageCount]: [string, number]) => usageCount < 2) - .map(([varName: string, _]) => varName); - - if (variable) { - const newDoc: ?DocumentNode = this.props.setArgValue(variable, false); - - if (newDoc) { - const targetOperation = newDoc.definitions.find(definition => { - if ( - !!definition.operation && - !!definition.name && - !!definition.name.value && - // - !!this.props.definition.name && - !!this.props.definition.name.value - ) { - return definition.name.value === this.props.definition.name.value; - } else { - return false; - } - }); - - const newVariableDefinitions: Array = [ - ...(targetOperation.variableDefinitions || []), - variable, - ].filter( - varDef => - newlyUnusedVariables.indexOf(varDef.variable.name.value) === -1, - ); - - const newOperation = { - ...targetOperation, - variableDefinitions: newVariableDefinitions, - }; - - const existingDefs = newDoc.definitions; - - const newDefinitions = existingDefs.map(existingOperation => { - if (targetOperation === existingOperation) { - return newOperation; - } else { - return existingOperation; - } - }); - - const finalDoc = { - ...newDoc, - definitions: newDefinitions, - }; - - this.props.onCommit(finalDoc); - } - } - }; - - const devariablize = () => { - /** - * 1. Find the current variable definition in the operation def - * 2. Extract its value - * 3. Replace the current arg value - * 4. Visit the resulting operation to see if there are any other usages of the variable - * 5. If not, remove the variableDefinition - */ - if (!argValue || !argValue.name || !argValue.name.value) { - return; - } - - const variableName = argValue.name.value; - const variableDefinition = ( - this.props.definition.variableDefinitions || [] - ).find(varDef => varDef.variable.name.value === variableName); - - if (!variableDefinition) { - return; - } - - const defaultValue = variableDefinition.defaultValue; - - const newDoc: ?DocumentNode = this.props.setArgValue(defaultValue, { - commit: false, - }); - - if (newDoc) { - const targetOperation: ?OperationDefinitionNode = newDoc.definitions.find( - definition => - definition.name.value === this.props.definition.name.value, - ); - - if (!targetOperation) { - return; - } - - // After de-variabilizing, see if the variable is still in use. If not, remove it. - let variableUseCount = 0; - - visit(targetOperation, { - Variable(node) { - if (node.name.value === variableName) { - variableUseCount = variableUseCount + 1; - } - }, - }); - - let newVariableDefinitions = targetOperation.variableDefinitions || []; - - // A variable is in use if it shows up at least twice (once in the definition, once in the selection) - if (variableUseCount < 2) { - newVariableDefinitions = newVariableDefinitions.filter( - varDef => varDef.variable.name.value !== variableName, - ); - } - - const newOperation = { - ...targetOperation, - variableDefinitions: newVariableDefinitions, - }; - - const existingDefs = newDoc.definitions; - - const newDefinitions = existingDefs.map(existingOperation => { - if (targetOperation === existingOperation) { - return newOperation; - } else { - return existingOperation; - } - }); - - const finalDoc = { - ...newDoc, - definitions: newDefinitions, - }; - - this.props.onCommit(finalDoc); - } - }; - - const isArgValueVariable = argValue && argValue.kind === 'Variable'; - - const variablizeActionButton = !this.state.displayArgActions ? null : ( - - ); - return (
{ schema={schema} getDefaultFieldNames={getDefaultFieldNames} getDefaultScalarArgValue={this.props.getDefaultScalarArgValue} + scalarInputsPluginManager={this.props.scalarInputsPluginManager} makeDefaultArg={this.props.makeDefaultArg} onRunOperation={this.props.onRunOperation} onCommit={this.props.onCommit} @@ -2212,6 +1967,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} @@ -2234,6 +1990,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} From 437b01d459ec83d9a2ae8c02f43f2be1fd08a268 Mon Sep 17 00:00:00 2001 From: Daryl Boakes Date: Wed, 2 Mar 2022 14:28:04 +0000 Subject: [PATCH 20/21] [sc-28504] Bump version and ensure package.json has agrimetrics in namespace --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ec336cb..3f09b37 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.1", "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", From 65c0290fb7ea14a54029849c3ce2a59fc55ff623 Mon Sep 17 00:00:00 2001 From: Daryl Boakes Date: Wed, 2 Mar 2022 14:52:38 +0000 Subject: [PATCH 21/21] [sc-28504] Fix ScalarInputPluginManager handler and bump version --- package.json | 2 +- src/Explorer.js | 138 ++++++++++++++++++++++++------------------------ 2 files changed, 70 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index 3f09b37..4a5e702 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agrimetrics/graphiql-explorer", - "version": "0.8.1", + "version": "0.8.2", "homepage": "https://github.com/onegraph/graphiql-explorer", "bugs": { "url": "https://github.com/onegraph/graphiql-explorer/issues" diff --git a/src/Explorer.js b/src/Explorer.js index 60e9ddd..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, @@ -562,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: ''}; @@ -575,7 +575,7 @@ export function defaultValue( default: return {kind: 'StringValue', value: ''}; } - } + } function defaultGetDefaultScalarArgValue( @@ -643,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, @@ -705,7 +705,7 @@ class ArgView extends React.PureComponent { a === argSelection ? { ...a, - value: value, + value, } : a, ), @@ -971,7 +971,7 @@ class AbstractArgView extends React.PureComponent< const argType = unwrapInputType(arg.type); let input = scalarInputsPluginManager && scalarInputsPluginManager.process(this.props) - let usedDefaultRender = !input; + const usedDefaultRender = !input; if (usedDefaultRender) { input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig); } @@ -1023,7 +1023,7 @@ class AbstractArgView extends React.PureComponent< let variable: ?VariableDefinitionNode; - let subVariableUsageCountByName: { + const subVariableUsageCountByName: { [key: string]: number, } = {}; @@ -1071,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 = [ @@ -1102,9 +1102,9 @@ class AbstractArgView extends React.PureComponent< const newDefinitions = existingDefs.map(existingOperation => { if (targetOperation === existingOperation) { return newOperation; - } else { + } return existingOperation; - } + }); const finalDoc = { @@ -1160,7 +1160,7 @@ class AbstractArgView extends React.PureComponent< visit(targetOperation, { Variable(node) { if (node.name.value === variableName) { - variableUseCount = variableUseCount + 1; + variableUseCount += 1; } }, }); @@ -1184,9 +1184,9 @@ class AbstractArgView extends React.PureComponent< const newDefinitions = existingDefs.map(existingOperation => { if (targetOperation === existingOperation) { return newOperation; - } else { + } return existingOperation; - } + }); const finalDoc = { @@ -1248,13 +1248,13 @@ class AbstractArgView extends React.PureComponent< }}> {usedDefaultRender && isInputObjectType(argType) ? ( - {!!argValue + {argValue ? this.props.styleConfig.arrowOpen : this.props.styleConfig.arrowClosed} ) : ( )} @@ -1388,7 +1388,7 @@ class AbstractView extends React.PureComponent { style={{cursor: 'pointer'}} onClick={selection ? this._removeFragment : this._addFragment}> @@ -1480,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', @@ -1618,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', @@ -1663,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) @@ -1800,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 : ( )} @@ -1940,7 +1940,7 @@ class FieldView extends React.PureComponent<
{node}
- {!!applicableFragments + {applicableFragments ? applicableFragments.map(fragment => { const type = schema.getType( fragment.typeCondition.name.value, @@ -2045,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; @@ -2053,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 = { @@ -2258,7 +2258,7 @@ class RootView extends React.PureComponent< onChange={this._onOperationRename} /> - {!!this.props.onTypeName ? ( + {this.props.onTypeName ? (
{`on ${this.props.onTypeName}`} @@ -2266,7 +2266,7 @@ class RootView extends React.PureComponent< ) : ( '' )} - {!!this.state.displayTitleActions ? ( + {this.state.displayTitleActions ? (