// @flow import React, {Component} from 'react'; import copy from 'copy-to-clipboard'; import {parse, print} from 'graphql'; // $FlowFixMe: can't find module import CodeMirror from 'codemirror'; import toposort from './toposort.js'; import type { GraphQLSchema, FragmentDefinitionNode, OperationDefinitionNode, VariableDefinitionNode, OperationTypeNode, SelectionSetNode, } from 'graphql'; function formatVariableName(name: string) { var uppercasePattern = /[A-Z]/g; return ( name.charAt(0).toUpperCase() + name .slice(1) .replace(uppercasePattern, '_$&') .toUpperCase() ); } const copyIcon = ( ); const codesandboxIcon = ( ); export type Variables = {[key: string]: ?mixed}; // TODO: Need clearer separation between option defs and option values export type Options = Array<{id: string, label: string, initial: boolean}>; export type OptionValues = {[id: string]: boolean}; export type OperationData = { query: string, name: string, displayName: string, type: OperationTypeNode | 'fragment', variableName: string, variables: Variables, operationDefinition: OperationDefinitionNode | FragmentDefinitionNode, fragmentDependencies: Array, }; export type GenerateOptions = { serverUrl: string, headers: {[name: string]: string}, context: Object, operationDataList: Array, options: OptionValues, }; export type CodesandboxFile = { content: string | mixed, }; export type CodesandboxFiles = { [filename: string]: CodesandboxFile, }; export type Snippet = { options: Options, language: string, codeMirrorMode: string, name: string, generate: (options: GenerateOptions) => string, generateCodesandboxFiles?: ?(options: GenerateOptions) => CodesandboxFiles, }; async function createCodesandbox( files: CodesandboxFiles, ): Promise<{sandboxId: string}> { const res = await fetch( 'https://codesandbox.io/api/v1/sandboxes/define?json=1', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({files}), }, ); const json = await res.json(); if (!json.sandbox_id) { throw new Error('Invalid response from Codesandbox API'); } else { return {sandboxId: json.sandbox_id}; } } let findFragmentDependencies = ( operationDefinitions: Array, def: OperationDefinitionNode | FragmentDefinitionNode, ): Array => { const fragmentByName = (name: string) => { return operationDefinitions.find(def => def.name.value === name); }; const findReferencedFragments = ( selectionSet: SelectionSetNode, ): Array => { const selections = selectionSet.selections; const namedFragments = selections .map(selection => { if (selection.kind === 'FragmentSpread') { return fragmentByName(selection.name.value); } else { return null; } }) .filter(Boolean); const nestedNamedFragments: Array = selections.reduce( (acc, selection) => { if ( (selection.kind === 'Field' || selection.kind === 'SelectionNode' || selection.kind === 'InlineFragment') && selection.selectionSet !== undefined ) { return acc.concat(findReferencedFragments(selection.selectionSet)); } else { return acc; } }, [], ); return namedFragments.concat(nestedNamedFragments); }; const selectionSet = def.selectionSet; return findReferencedFragments(selectionSet); }; let operationNodesMemo: [ ?string, ?Array, ] = [null, null]; function getOperationNodes( query: string, ): Array { if (operationNodesMemo[0] === query && operationNodesMemo[1]) { return operationNodesMemo[1]; } const operationDefinitions = []; try { for (const def of parse(query).definitions) { if ( def.kind === 'FragmentDefinition' || def.kind === 'OperationDefinition' ) { operationDefinitions.push(def); } } } catch (e) {} operationNodesMemo = [query, operationDefinitions]; return operationDefinitions; } const getUsedVariables = ( variables: Variables, operationDefinition: OperationDefinitionNode | FragmentDefinitionNode, ): Variables => { return (operationDefinition.variableDefinitions || []).reduce( (usedVariables, variable: VariableDefinitionNode) => { const variableName = variable.variable.name.value; if (variables[variableName]) { usedVariables[variableName] = variables[variableName]; } return usedVariables; }, {}, ); }; const getOperationName = ( operationDefinition: OperationDefinitionNode | FragmentDefinitionNode, ) => operationDefinition.name ? operationDefinition.name.value : operationDefinition.operation; const getOperationDisplayName = (operationDefinition): string => operationDefinition.name ? operationDefinition.name.value : ''; /** * ToolbarMenu * * A menu style button to use within the Toolbar. * Copied from GraphiQL: https://github.com/graphql/graphiql/blob/272e2371fc7715217739efd7817ce6343cb4fbec/src/components/ToolbarMenu.js#L16-L80 */ export class ToolbarMenu extends Component< {title: string, label: string, children: React$Node}, {visible: boolean}, > { state = {visible: false}; _node: ?HTMLAnchorElement; _listener: ?(e: Event) => void; componentWillUnmount() { this._release(); } render() { const visible = this.state.visible; return ( // eslint-disable-next-line e.preventDefault()} ref={node => { this._node = node; }} title={this.props.title}> {this.props.label}
    {this.props.children}
); } _subscribe() { if (!this._listener) { this._listener = this.handleClick.bind(this); document.addEventListener('click', this._listener); } } _release() { if (this._listener) { document.removeEventListener('click', this._listener); this._listener = null; } } handleClick(e: Event) { if (this._node !== e.target) { e.preventDefault(); this.setState({visible: false}); this._release(); } } handleOpen = (e: Event) => { e.preventDefault(); this.setState({visible: true}); this._subscribe(); }; } type CodeDisplayProps = {code: string, mode: string, theme: ?string}; class CodeDisplay extends React.PureComponent { _node: ?HTMLDivElement; editor: CodeMirror; componentDidMount() { this.editor = CodeMirror(this._node, { value: this.props.code.trim(), lineNumbers: false, mode: this.props.mode, readOnly: true, theme: this.props.theme, }); } componentDidUpdate(prevProps: CodeDisplayProps) { if (this.props.code !== prevProps.code) { this.editor.setValue(this.props.code); } if (this.props.mode !== prevProps.mode) { this.editor.setOption('mode', this.props.mode); } if (this.props.theme !== prevProps.theme) { this.editor.setOption('theme', this.props.theme); } } render() { return
(this._node = ref)} />; } } type Props = {| snippet: ?Snippet, snippets: Array, query: string, serverUrl: string, context: Object, variables: Variables, headers: {[name: string]: string}, setOptionValue?: (id: string, value: boolean) => void, optionValues: OptionValues, codeMirrorTheme: ?string, onSelectSnippet: ?(snippet: Snippet) => void, onSetOptionValue: ?(snippet: Snippet, option: string, value: boolean) => void, onGenerateCodesandbox?: ?({sandboxId: string}) => void, schema: ?GraphQLSchema, |}; type State = {| showCopiedTooltip: boolean, optionValuesBySnippet: Map, snippet: ?Snippet, codesandboxResult: | null | {type: 'loading'} | {type: 'success', sandboxId: string} | {type: 'error', error: string}, |}; class CodeExporter extends Component { style: ?HTMLLinkElement; state = { showCopiedTooltip: false, optionValuesBySnippet: new Map(), snippet: null, codesandboxResult: null, }; _activeSnippet = (): Snippet => this.props.snippet || this.state.snippet || this.props.snippets[0]; setSnippet = (snippet: Snippet) => { this.props.onSelectSnippet && this.props.onSelectSnippet(snippet); this.setState({snippet, codesandboxResult: null}); }; setLanguage = (language: string) => { const snippet = this.props.snippets.find( snippet => snippet.language === language, ); if (snippet) { this.setSnippet(snippet); } }; handleSetOptionValue = (snippet: Snippet, id: string, value: boolean) => { this.props.onSetOptionValue && this.props.onSetOptionValue(snippet, id, value); const {optionValuesBySnippet} = this.state; const snippetOptions = optionValuesBySnippet.get(snippet) || {}; optionValuesBySnippet.set(snippet, {...snippetOptions, [id]: value}); return this.setState({optionValuesBySnippet}); }; getOptionValues = (snippet: Snippet) => { const snippetDefaults = snippet.options.reduce( (acc, option) => ({...acc, [option.id]: option.initial}), {}, ); return { ...snippetDefaults, ...(this.state.optionValuesBySnippet.get(snippet) || {}), ...this.props.optionValues, }; }; _generateCodesandbox = async (operationDataList: Array) => { this.setState({codesandboxResult: {type: 'loading'}}); const snippet = this._activeSnippet(); if (!snippet) { // Shouldn't be able to get in this state, but just in case... this.setState({ codesandboxResult: {type: 'error', error: 'No active snippet'}, }); return; } const generateFiles = snippet.generateCodesandboxFiles; if (!generateFiles) { // Shouldn't be able to get in this state, but just in case... this.setState({ codesandboxResult: { type: 'error', error: 'Snippet does not support CodeSandbox', }, }); return; } try { const sandboxResult = await createCodesandbox( generateFiles( this._collectOptions(snippet, operationDataList, this.props.schema), ), ); this.setState({ codesandboxResult: {type: 'success', ...sandboxResult}, }); this.props.onGenerateCodesandbox && this.props.onGenerateCodesandbox(sandboxResult); } catch (e) { console.error('Error generating codesandbox', e); this.setState({ codesandboxResult: { type: 'error', error: 'Failed to generate CodeSandbox', }, }); } }; _collectOptions = ( snippet: Snippet, operationDataList: Array, schema: ?GraphQLSchema, ): GenerateOptions => { const {serverUrl, context = {}, headers = {}} = this.props; const optionValues = this.getOptionValues(snippet); return { serverUrl, headers, context, operationDataList, options: optionValues, schema, }; }; render() { const {query, snippets, variables = {}} = this.props; const {showCopiedTooltip, codesandboxResult} = this.state; const snippet = this._activeSnippet(); const operationDefinitions = getOperationNodes(query); const {name, language, generate} = snippet; const fragmentDefinitions: Array = []; for (const operationDefinition of operationDefinitions) { if (operationDefinition.kind === 'FragmentDefinition') { fragmentDefinitions.push(operationDefinition); } } const rawOperationDataList: Array = operationDefinitions.map( ( operationDefinition: OperationDefinitionNode | FragmentDefinitionNode, ) => ({ query: print(operationDefinition), name: getOperationName(operationDefinition), displayName: getOperationDisplayName(operationDefinition), // $FlowFixMe: Come back for this type: operationDefinition.operation || 'fragment', variableName: formatVariableName(getOperationName(operationDefinition)), variables: getUsedVariables(variables, operationDefinition), operationDefinition, fragmentDependencies: findFragmentDependencies( fragmentDefinitions, operationDefinition, ), }), ); const operationDataList = toposort(rawOperationDataList); const optionValues: Array = this.getOptionValues(snippet); const codeSnippet = operationDefinitions.length ? generate( this._collectOptions(snippet, operationDataList, this.props.schema), ) : null; const supportsCodesandbox = snippet.generateCodesandboxFiles; const languages = [ ...new Set(snippets.map(snippet => snippet.language)), ].sort((a, b) => a.localeCompare(b)); return (
{languages.map((lang: string) => (
  • this.setLanguage(lang)}> {lang}
  • ))}
    {snippets .filter(snippet => snippet.language === language) .map(snippet => (
  • this.setSnippet(snippet)}> {snippet.name}
  • ))}
    {snippet.options.length > 0 ? (
    Options
    {snippet.options.map(option => (
    this.handleSetOptionValue( snippet, option.id, // $FlowFixMe: Come back for this !optionValues[option.id], ) } />
    ))}
    ) : (
    )} {supportsCodesandbox ? (
    {codesandboxResult ? (
    {codesandboxResult.type === 'loading' ? ( 'Loading...' ) : codesandboxResult.type === 'error' ? ( `Error: ${codesandboxResult.error}` ) : ( Visit CodeSandbox )}
    ) : null}
    ) : null}
    {codeSnippet ? ( ) : (
    The query is invalid.
    The generated code will appear here once the errors in the query editor are resolved.
    )}
    ); } } class ErrorBoundary extends React.Component<*, {hasError: boolean}> { state = {hasError: false}; componentDidCatch(error, info) { this.setState({hasError: true}); console.error('Error in component', error, info); } render() { if (this.state.hasError) { return (
    Error generating code. Please{' '} report your query on Spectrum .
    ); } return this.props.children; } } type WrapperProps = { query: string, serverUrl: string, variables: string, context: Object, headers?: {[name: string]: string}, hideCodeExporter: () => void, snippets: Array, snippet?: Snippet, codeMirrorTheme?: string, onSelectSnippet?: (snippet: Snippet) => void, onSetOptionValue?: (snippet: Snippet, option: string, value: boolean) => void, optionValues?: OptionValues, onGenerateCodesandbox?: ?({sandboxId: string}) => void, schema: ?GraphQLSchema, }; // we borrow class names from graphiql's CSS as the visual appearance is the same // yet we might want to change that at some point in order to have a self-contained standalone export default function CodeExporterWrapper({ query, serverUrl, variables, context = {}, headers = {}, hideCodeExporter = () => {}, snippets, snippet, codeMirrorTheme, onSelectSnippet, onSetOptionValue, optionValues, onGenerateCodesandbox, schema, }: WrapperProps) { let jsonVariables: Variables = {}; try { const parsedVariables = JSON.parse(variables); if (typeof parsedVariables === 'object') { jsonVariables = parsedVariables; } } catch (e) {} return (
    Code Exporter
    {'\u2715'}
    {snippets.length ? ( ) : (
    Please provide a list of snippets
    )}
    ); }