Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/plugin custom graphql scalars #47

Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0e2b43e
add a 'deprecated' class to fields which have been marked as such
TimLehner Jan 10, 2020
9fe6dd3
Merge branch 'master' of https://github.com/OneGraph/graphiql-explorer
TimLehner Jan 29, 2020
025c824
Refactor existing behaviour into seperate method
TimLehner Feb 1, 2020
745e400
Introduce custom scalar input plugin manager
TimLehner Feb 1, 2020
9c3647a
Create example DateInput plugin based on PR #46
TimLehner Feb 1, 2020
014ec5b
Only enable bundled plugins if requested.
TimLehner Feb 1, 2020
77348c4
update readme for custom-graphql-scalars plugin
TimLehner Feb 1, 2020
ce1453f
[bugfix] only disable the bundled plugins
TimLehner Feb 1, 2020
47f6bae
typo
TimLehner Feb 2, 2020
7673dc1
Date -> DateTime
TimLehner Feb 2, 2020
c48ce5f
[bugfix] if manager is null explorer could break
TimLehner Feb 2, 2020
ae727bd
show checkbox if rendered by plugin
TimLehner Feb 2, 2020
0180b44
Add support for complex types
TimLehner Feb 2, 2020
433d702
update example inputs when query text changes
TimLehner Feb 2, 2020
48ae2f0
disable example input plugin if not selected
TimLehner Feb 2, 2020
7d597f7
typo
TimLehner Feb 2, 2020
ead2b1d
update plugin readme
TimLehner Feb 2, 2020
064c579
Merge changes from master
TimLehner Apr 22, 2020
8062b6b
Move everything into Explorer.js following review
TimLehner Apr 22, 2020
7674eba
Merge branch 'master' of https://github.com/OneGraph/graphiql-explore…
TimLehner Apr 22, 2020
f684f5c
fix broken merge
TimLehner May 16, 2020
3e99795
Merge remote-tracking branch 'upstream/master'
dboakes-ag Mar 2, 2022
93cd44b
Merge branch 'master' into feature/plugin-custom-graphql-scalars
dboakes-ag Mar 2, 2022
437b01d
[sc-28504] Bump version and ensure package.json has agrimetrics in na…
dboakes-ag Mar 2, 2022
65c0290
[sc-28504] Fix ScalarInputPluginManager handler and bump version
dboakes-ag Mar 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
47 changes: 36 additions & 11 deletions src/Explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import type {
ValueNode,
} from 'graphql';

import ScalarInputPluginManager from './plugins/graphql-scalar-inputs'

type Field = GraphQLField<any, any>;

type GetDefaultScalarArgValue = (
Expand Down Expand Up @@ -635,6 +637,9 @@ class ArgView extends React.PureComponent<ArgViewProps, ArgViewState> {
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
scalarInputsPluginManager={this.props.scalarInputsPluginManager}
modifyArguments={this.props.modifyArguments}
selection={this.props.selection}
/>
);
}
Expand Down Expand Up @@ -723,11 +728,7 @@ class ScalarInput extends React.PureComponent<ScalarInputProps, {}> {
}

class AbstractArgView extends React.PureComponent<AbstractArgViewProps, {}> {
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') {
Expand Down Expand Up @@ -823,6 +824,19 @@ class AbstractArgView extends React.PureComponent<AbstractArgViewProps, {}> {
}
}
}
return input;
}

render() {
const {argValue, arg, styleConfig, scalarInputsPluginManager} = this.props;
/* TODO: handle List types*/
const argType = unwrapInputType(arg.type);

let input = scalarInputsPluginManager && scalarInputsPluginManager.process(this.props)
let usedDefaultRender = !input;
if (usedDefaultRender) {
input = this.defaultArgViewHandler(arg, argType, argValue, styleConfig);
}

return (
<div
Expand All @@ -837,7 +851,7 @@ class AbstractArgView extends React.PureComponent<AbstractArgViewProps, {}> {
<span
style={{cursor: 'pointer'}}
onClick={argValue ? this.props.removeArg : this.props.addArg}>
{isInputObjectType(argType) ? (
{usedDefaultRender && isInputObjectType(argType) ? (
<span>
{!!argValue
? this.props.styleConfig.arrowOpen
Expand Down Expand Up @@ -1290,6 +1304,7 @@ class FieldView extends React.PureComponent<FieldViewProps, {}> {
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
scalarInputsPluginManager={this.props.scalarInputsPluginManager}
/>
))}
</div>
Expand Down Expand Up @@ -1598,6 +1613,7 @@ class RootView extends React.PureComponent<RootViewProps, {}> {
makeDefaultArg={this.props.makeDefaultArg}
onRunOperation={this.props.onRunOperation}
styleConfig={this.props.styleConfig}
scalarInputsPluginManager={this.props.scalarInputsPluginManager}
/>
))}
</div>
Expand Down Expand Up @@ -1641,11 +1657,17 @@ class Explorer extends React.PureComponent<Props, State> {
getDefaultScalarArgValue: defaultGetDefaultScalarArgValue,
};

state = {
newOperationType: 'query',
operation: null,
operationToScrollTo: null,
};
constructor(props) {
super(props);
const { graphqlCustomScalarPlugins, enableBundledPlugins } = this.props;
this.scalarInputsPluginManager = new ScalarInputPluginManager(graphqlCustomScalarPlugins, enableBundledPlugins);
// should set initial state in object constructor
this.state = {
newOperationType: 'query',
operation: null,
operationToScrollTo: null,
};
}

_ref: ?any;
_resetScroll = () => {
Expand Down Expand Up @@ -2020,6 +2042,7 @@ class Explorer extends React.PureComponent<Props, State> {
}
}}
styleConfig={styleConfig}
scalarInputsPluginManager={this.scalarInputsPluginManager}
/>
);
},
Expand Down Expand Up @@ -2066,6 +2089,8 @@ class ExplorerWrapper extends React.PureComponent<Props, {}> {
static defaultProps = {
width: 320,
title: 'Explorer',
graphqlCustomScalarPlugins: [],
enableBundledPlugins: false,
};

render() {
Expand Down
154 changes: 154 additions & 0 deletions src/plugins/graphql-scalar-inputs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@

# 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 (
<span>
<input
type="number"
value={rePart}
onChange={e => this.handleChangeEvent(e, 'real')}
style={{ maxWidth: '50px', margin: '5px' }}
step='any'
disabled={!selectedComplexArg}
/>
+
<input
type="number"
defaultValue={imPart}
onChange={e => this.handleChangeEvent(e, 'imaginary')}
style={{ maxWidth: '50px', margin: '5px' }}
step='any'
disabled={!selectedComplexArg}
/> i
</span>);
}
}

export default {
canProcess: ComplexNumberHandler.canProcess,
name: 'Complex Number',
render: props => (
<ComplexNumberHandler
{...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...
<GraphiQLExplorer
...
graphqlCustomScalarPlugins={configuredPlugins}
/>
```
> 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.
24 changes: 24 additions & 0 deletions src/plugins/graphql-scalar-inputs/bundled/DateInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';

function canProcess(arg) {
return arg && arg.type && arg.type.name === 'Date';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it should be "DateTime" instead of "Date", because you use .toISOString().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DateInput is distinct from the example in the README which demonstrates adding additional handlers not bundled with the explorer.

This bundled plugin is just a direct implementation of the Date input in the POC PR.

}

function render(props) {
return (
<input
type="date"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it should be "datetime-local" instead of "date", because you use .toISOString().

value={props.arg.defaultValue}
onChange={props.setArgValue}
/>
);
}


const DatePlugin = {
canProcess,
render,
name: 'DateInput'
};

export default DatePlugin;
25 changes: 25 additions & 0 deletions src/plugins/graphql-scalar-inputs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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;