├── .babelrc ├── .circleci └── config.yml ├── .eslintrc ├── .flowconfig ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── resources ├── build.sh ├── checkgit.sh └── prepublish.sh └── src ├── Explorer.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "syntax-async-functions", 8 | "transform-class-properties", 9 | "transform-object-rest-spread", 10 | "transform-regenerator" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: node:9.5.0-stretch 6 | environment: 7 | - TERM: dumb 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | key: dependency-cache-{{ checksum "package.json" }} 12 | - run: 13 | name: 'Install Dependencies' 14 | command: yarn install --frozen-lockfile 15 | - save_cache: 16 | key: dependency-cache-{{ checksum "package.json" }} 17 | paths: 18 | - node_modules 19 | - run: 20 | name: 'bsb (reason)' 21 | command: yarn bsb 22 | - run: 23 | name: 'flow' 24 | command: yarn flow 25 | - run: 26 | name: 'Build' 27 | command: yarn build 28 | - run: 29 | name: 'Hack for firebase routing' 30 | command: 'cp build/index.html build/404.html' 31 | - persist_to_workspace: 32 | root: ./ 33 | paths: 34 | - build 35 | - .firebaserc 36 | - firebase.json 37 | 38 | deploy: 39 | docker: 40 | - image: node:9.5.0-stretch 41 | environment: 42 | - TERM: dumb 43 | steps: 44 | - attach_workspace: 45 | at: ./ 46 | - restore_cache: 47 | key: deploy-cache-v2 48 | - run: 49 | name: 'Install Dependencies' 50 | command: yarn add firebase-tools 51 | - save_cache: 52 | key: deploy-cache-v2 53 | paths: 54 | - node_modules 55 | - run: 56 | name: 'Deploy to Firebase' 57 | command: node_modules/.bin/firebase deploy --token "$FIREBASE_TOKEN" 58 | 59 | workflows: 60 | version: 2 61 | build-deploy: 62 | jobs: 63 | - build 64 | - deploy: 65 | requires: 66 | - build 67 | filters: 68 | branches: 69 | only: master 70 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "plugins": [ 5 | "babel", 6 | "react" 7 | ], 8 | 9 | "settings": { 10 | "react": { 11 | "version": "0.15.0" 12 | } 13 | }, 14 | 15 | "env": { 16 | "browser": true, 17 | "es6": true, 18 | "node": true 19 | }, 20 | 21 | "extends": [ 22 | "prettier" 23 | ], 24 | 25 | "parserOptions": { 26 | "arrowFunctions": true, 27 | "binaryLiterals": true, 28 | "blockBindings": true, 29 | "classes": true, 30 | "defaultParams": true, 31 | "destructuring": true, 32 | "experimentalObjectRestSpread": true, 33 | "forOf": true, 34 | "generators": true, 35 | "globalReturn": true, 36 | "jsx": true, 37 | "modules": true, 38 | "objectLiteralComputedProperties": true, 39 | "objectLiteralDuplicateProperties": true, 40 | "objectLiteralShorthandMethods": true, 41 | "objectLiteralShorthandProperties": true, 42 | "octalLiterals": true, 43 | "regexUFlag": true, 44 | "regexYFlag": true, 45 | "restParams": true, 46 | "spread": true, 47 | "superInFunctions": true, 48 | "templateStrings": true, 49 | "unicodeCodePointEscapes": true 50 | }, 51 | 52 | "rules": { 53 | "react/no-deprecated": 2, 54 | "react/no-did-mount-set-state": 2, 55 | "react/no-did-update-set-state": 2, 56 | "react/no-direct-mutation-state": 2, 57 | "react/no-string-refs": 2, 58 | "react/no-unknown-property": 2, 59 | "react/prefer-es6-class": 2, 60 | "react/prefer-stateless-function": 2, 61 | "react/prop-types": [2, {ignore: ["children"]}], 62 | "react/react-in-jsx-scope": 2, 63 | "react/self-closing-comp": 2, 64 | "react/sort-comp": [2, {"order": [ 65 | "static-methods", 66 | "lifecycle", 67 | "render", 68 | "everything-else" 69 | ]}], 70 | "react/jsx-boolean-value": 2, 71 | "react/jsx-handler-names": 2, 72 | "react/jsx-key": 2, 73 | "react/jsx-no-duplicate-props": 2, 74 | "react/jsx-no-literals": 2, 75 | "react/jsx-no-undef": 2, 76 | "react/jsx-pascal-case": 2, 77 | "react/jsx-uses-react": 2, 78 | "react/jsx-uses-vars": 2, 79 | 80 | "block-scoped-var": 0, 81 | "callback-return": 2, 82 | "camelcase": [2, {"properties": "always"}], 83 | "comma-dangle": 0, 84 | "comma-spacing": 0, 85 | "complexity": 0, 86 | "consistent-return": 0, 87 | "consistent-this": 0, 88 | "curly": [2, "all"], 89 | "default-case": 0, 90 | "dot-notation": 0, 91 | "eqeqeq": 2, 92 | "func-names": 0, 93 | "func-style": 0, 94 | "guard-for-in": 2, 95 | "handle-callback-err": [2, "error"], 96 | "id-length": 0, 97 | "id-match": [2, "^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$"], 98 | "indent": 0, 99 | "init-declarations": 0, 100 | "linebreak-style": 2, 101 | "lines-around-comment": 0, 102 | "max-depth": 0, 103 | "max-nested-callbacks": 0, 104 | "max-params": 0, 105 | "max-statements": 0, 106 | "new-cap": 0, 107 | "newline-after-var": 0, 108 | "no-alert": 2, 109 | "no-array-constructor": 2, 110 | "no-await-in-loop": 2, 111 | "no-bitwise": 0, 112 | "no-caller": 2, 113 | "no-catch-shadow": 0, 114 | "no-class-assign": 2, 115 | "no-cond-assign": 2, 116 | "no-console": 1, 117 | "no-const-assign": 2, 118 | "no-constant-condition": 2, 119 | "no-continue": 0, 120 | "no-control-regex": 0, 121 | "no-debugger": 1, 122 | "no-delete-var": 2, 123 | "no-div-regex": 2, 124 | "no-dupe-args": 2, 125 | "no-dupe-keys": 2, 126 | "no-duplicate-case": 2, 127 | "no-else-return": 2, 128 | "no-empty": 2, 129 | "no-empty-character-class": 2, 130 | "no-eq-null": 0, 131 | "no-eval": 2, 132 | "no-ex-assign": 2, 133 | "no-extend-native": 2, 134 | "no-extra-bind": 2, 135 | "no-extra-boolean-cast": 2, 136 | "no-extra-parens": 0, 137 | "no-fallthrough": 2, 138 | "no-floating-decimal": 2, 139 | "no-func-assign": 2, 140 | "no-implicit-coercion": 2, 141 | "no-implied-eval": 2, 142 | "no-inline-comments": 0, 143 | "no-inner-declarations": [2, "functions"], 144 | "no-invalid-regexp": 2, 145 | "no-invalid-this": 0, 146 | "no-irregular-whitespace": 2, 147 | "no-iterator": 2, 148 | "no-label-var": 2, 149 | "no-labels": [2, {"allowLoop": true}], 150 | "no-lone-blocks": 2, 151 | "no-lonely-if": 2, 152 | "no-loop-func": 0, 153 | "no-mixed-requires": [2, true], 154 | "no-multi-str": 2, 155 | "no-multiple-empty-lines": 0, 156 | "no-native-reassign": 0, 157 | "no-negated-in-lhs": 2, 158 | "no-nested-ternary": 0, 159 | "no-new": 2, 160 | "no-new-func": 0, 161 | "no-new-object": 2, 162 | "no-new-require": 2, 163 | "no-new-wrappers": 2, 164 | "no-obj-calls": 2, 165 | "no-octal": 2, 166 | "no-octal-escape": 2, 167 | "no-param-reassign": 2, 168 | "no-path-concat": 2, 169 | "no-plusplus": 0, 170 | "no-process-env": 0, 171 | "no-process-exit": 0, 172 | "no-proto": 2, 173 | "no-redeclare": 2, 174 | "no-regex-spaces": 2, 175 | "no-restricted-modules": 0, 176 | "no-return-assign": 2, 177 | "no-script-url": 2, 178 | "no-self-compare": 0, 179 | "no-sequences": 2, 180 | "no-shadow": 2, 181 | "no-shadow-restricted-names": 2, 182 | "no-sparse-arrays": 2, 183 | "no-sync": 2, 184 | "no-ternary": 0, 185 | "no-this-before-super": 2, 186 | "no-throw-literal": 2, 187 | "no-undef": 2, 188 | "no-undef-init": 2, 189 | "no-undefined": 0, 190 | "no-underscore-dangle": 0, 191 | "no-unexpected-multiline": 2, 192 | "no-unneeded-ternary": 2, 193 | "no-unreachable": 2, 194 | "no-unused-expressions": 2, 195 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 196 | "no-use-before-define": 0, 197 | "no-useless-call": 2, 198 | "no-useless-escape": 2, 199 | "no-useless-return": 2, 200 | "no-var": 2, 201 | "no-void": 2, 202 | "no-warning-comments": 0, 203 | "no-with": 2, 204 | "object-curly-spacing": [0, "always"], 205 | "object-shorthand": [2, "always"], 206 | "one-var": [2, "never"], 207 | "operator-assignment": [2, "always"], 208 | "padded-blocks": 0, 209 | "prefer-const": 2, 210 | "prefer-reflect": 0, 211 | "prefer-spread": 0, 212 | "radix": 2, 213 | "require-yield": 2, 214 | "sort-vars": 0, 215 | "space-in-parens": 0, 216 | "spaced-comment": [2, "always"], 217 | "strict": 0, 218 | "use-isnan": 2, 219 | "valid-jsdoc": 0, 220 | "valid-typeof": 2, 221 | "vars-on-top": 0, 222 | "wrap-regex": 0, 223 | "yoda": [2, "never", {"exceptRange": true}] 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/css/.* 3 | .*/dist/.* 4 | .*/coverage/.* 5 | .*/resources/.* 6 | .*/node_modules/graphql-language-service-interface/.* 7 | 8 | [include] 9 | 10 | [libs] 11 | 12 | [lints] 13 | 14 | [options] 15 | 16 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 17 | 18 | [strict] 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | dist/ 24 | /graphiqlExplorer.js 25 | /graphiqlExplorer.min.js 26 | 27 | .merlin 28 | lib/ 29 | /.bsb.lock 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Interactive explorer plugin for GraphiQL. 2 | 3 | Try it live at [https://www.onegraph.com/graphiql](https://www.onegraph.com/graphiql). 4 | 5 | [OneGraph](https://www.onegraph.com) provides easy, consistent access to the APIs that underlie your business--all through the power of GraphQL. 6 | 7 | Sign up at [https://www.onegraph.com](http://www.onegraph.com). 8 | 9 | [![npm version](http://img.shields.io/npm/v/graphiql-explorer.svg?style=flat)](https://npmjs.org/package/graphiql-explorer "View this project on npm") 10 | 11 | 12 | ## Example usage 13 | 14 | See the [example repo](https://github.com/OneGraph/graphiql-explorer-example) for how to use OneGraph's GraphiQL Explorer in your own GraphiQL instance. 15 | 16 | ![Preview](https://user-images.githubusercontent.com/476818/51567716-c00dfa00-1e4c-11e9-88f7-6d78b244d534.gif) 17 | 18 | [Read the rationale on the OneGraph blog](https://www.onegraph.com/blog/2019/01/24/How_OneGraph_onboards_users_new_to_GraphQL.html). 19 | 20 | 21 | ## Customizing styles 22 | 23 | The default styling matches for the Explorer matches the default styling for GraphiQL. If you've customized your GraphiQL styling, you can customize the Explorer's styling to match. 24 | 25 | ### Customizing colors 26 | 27 | The Explorer accepts a `colors` prop as a map of the class names in GraphiQL's css to hex colors. If you've edited the GraphiQL class names that control colors (e.g. `cm-def`, `cm-variable`, `cm-string`, etc.) use those same colors in the colors map. The naming of the keys in the colors map tries to align closely with the names of the class names in GraphiQL's css (note that the Explorer can't just apply the classes because of conflicts with how the css file styles inputs). 28 | 29 | Example style map: 30 | 31 | ```javascript 32 | 52 | ``` 53 | 54 | ### Customizing arrows and checkboxes 55 | 56 | The explorer accepts props for setting custom checkboxes (for leaf fields) and arrows (for object fields). 57 | 58 | The props are `arrowOpen`, `arrowClosed`, `checkboxChecked`, and `checkboxUnchecked`. You can pass any react node for those props. 59 | 60 | The defaults are 61 | 62 | arrowOpen 63 | ```javascript 64 | 65 | 66 | 67 | ``` 68 | 69 | arrowClosed 70 | ```javascript 71 | 72 | 73 | 74 | ``` 75 | 76 | checkboxChecked 77 | ``` 78 | 85 | 89 | 90 | ``` 91 | 92 | checkboxUnchecked 93 | ``` 94 | 101 | 105 | 106 | ``` 107 | 108 | ### Customizing the buttons to create new operations 109 | 110 | You can modify the styles for the buttons that allow you to create new operations. 111 | 112 | Pass the `styles` prop when you create the component. It's an object with two keys, `explorerActionsStyle` and `buttonStyle`. 113 | 114 | Example styles map: 115 | ```javascript 116 | 142 | ``` 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphiql-explorer", 3 | "version": "0.8.0", 4 | "homepage": "https://github.com/onegraph/graphiql-explorer", 5 | "bugs": { 6 | "url": "https://github.com/onegraph/graphiql-explorer/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "http://github.com/onegraph/graphiql-explorer.git" 11 | }, 12 | "license": "MIT", 13 | "main": "dist/index.js", 14 | "files": [ 15 | "dist", 16 | "graphiqlExplorer.js", 17 | "graphiqlExplorer.min.js", 18 | "README.md", 19 | "LICENSE" 20 | ], 21 | "browserify-shim": { 22 | "react": "global:React", 23 | "react-dom": "global:ReactDOM" 24 | }, 25 | "prettier": { 26 | "singleQuote": true, 27 | "trailingComma": "all", 28 | "bracketSpacing": false, 29 | "jsxBracketSameLine": true 30 | }, 31 | "lint-staged": { 32 | "*.js": [ 33 | "prettier --jsx-bracket-same-line --single-quote --trailing-comma=all --write", 34 | "git add" 35 | ] 36 | }, 37 | "peerDependencies": { 38 | "graphql": "^0.6.0 || ^0.7.0 || ^0.8.0-b || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", 39 | "react": "^15.6.0 || ^16.0.0", 40 | "react-dom": "^15.6.0 || ^16.0.0" 41 | }, 42 | "devDependencies": { 43 | "autoprefixer": "^7.0.0", 44 | "babel-cli": "6.24.1", 45 | "babel-eslint": "7.2.3", 46 | "babel-plugin-syntax-async-functions": "6.13.0", 47 | "babel-plugin-transform-class-properties": "6.24.1", 48 | "babel-plugin-transform-object-rest-spread": "6.22.0", 49 | "babel-plugin-transform-regenerator": "6.24.1", 50 | "babel-preset-es2015": "6.24.1", 51 | "babel-preset-react": "6.24.1", 52 | "babelify": "7.3.0", 53 | "browserify": "^14.4.0", 54 | "browserify-shim": "3.8.14", 55 | "chai": "4.1.1", 56 | "chai-subset": "1.5.0", 57 | "eslint": "4.4.1", 58 | "eslint-config-prettier": "^2.3.0", 59 | "eslint-plugin-babel": "4.1.2", 60 | "eslint-plugin-react": "7.2.0", 61 | "flow-bin": "^0.77.0", 62 | "graphql": "14.0.2", 63 | "husky": "^0.14.0", 64 | "jsdom": "11.1.0", 65 | "lint-staged": "^4.0.0", 66 | "postcss-cli": "4.1.0", 67 | "prettier": "^1.4.4", 68 | "prop-types": "15.5.8", 69 | "react": "15.5.4", 70 | "react-dom": "15.5.4", 71 | "react-test-renderer": "15.6.1", 72 | "uglify-js": "^3.0.20", 73 | "uglifyify": "^4.0.2", 74 | "watchify": "3.9.0" 75 | }, 76 | "scripts": { 77 | "build": "bash ./resources/build.sh", 78 | "check": "flow check", 79 | "test": "npm run check && npm run build" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /resources/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | if [ ! -d "node_modules/.bin" ]; then 7 | echo "Be sure to run \`npm install\` before building GraphiQL Explorer." 8 | exit 1 9 | fi 10 | 11 | rm -rf dist/ && mkdir -p dist/ 12 | babel src --ignore __tests__ --out-dir dist/ 13 | echo "Bundling graphiqlExplorer.js..." 14 | browserify -g browserify-shim -s GraphiQLExplorer dist/index.js > graphiqlExplorer.js 15 | echo "Bundling graphiqlExplorer.min.js..." 16 | browserify -g browserify-shim -t uglifyify -s GraphiQLExplorer dist/index.js | uglifyjs -c > graphiqlExplorer.min.js 17 | # echo "Bundling graphiql.css..." 18 | # postcss --no-map --use autoprefixer -d dist/ css/*.css 19 | # cat dist/*.css > graphiql.css 20 | echo "Done" 21 | -------------------------------------------------------------------------------- /resources/checkgit.sh: -------------------------------------------------------------------------------- 1 | # 2 | # This script determines if current git state is the up to date master. If so 3 | # it exits normally. If not it prompts for an explicit continue. This script 4 | # intends to protect from versioning for NPM without first pushing changes 5 | # and including any changes on master. 6 | # 7 | 8 | # First fetch to ensure git is up to date. Fail-fast if this fails. 9 | git fetch; 10 | if [[ $? -ne 0 ]]; then exit 1; fi; 11 | 12 | # Extract useful information. 13 | GITBRANCH=$(git branch -v 2> /dev/null | sed '/^[^*]/d'); 14 | GITBRANCHNAME=$(echo "$GITBRANCH" | sed 's/* \([A-Za-z0-9_\-]*\).*/\1/'); 15 | GITBRANCHSYNC=$(echo "$GITBRANCH" | sed 's/* [^[]*.\([^]]*\).*/\1/'); 16 | 17 | # Check if master is checked out 18 | if [ "$GITBRANCHNAME" != "master" ]; then 19 | read -p "Git not on master but $GITBRANCHNAME. Continue? (y|N) " yn; 20 | if [ "$yn" != "y" ]; then exit 1; fi; 21 | fi; 22 | 23 | # Check if branch is synced with remote 24 | if [ "$GITBRANCHSYNC" != "" ]; then 25 | read -p "Git not up to date but $GITBRANCHSYNC. Continue? (y|N) " yn; 26 | if [ "$yn" != "y" ]; then exit 1; fi; 27 | fi; 28 | -------------------------------------------------------------------------------- /resources/prepublish.sh: -------------------------------------------------------------------------------- 1 | # Because of a long-running npm issue (https://github.com/npm/npm/issues/3059) 2 | # prepublish runs after `npm install` and `npm pack`. 3 | # In order to only run prepublish before `npm publish`, we have to check argv. 4 | if node -e "process.exit(($npm_config_argv).original.length > 0 && ($npm_config_argv).original[0].indexOf('pu') === 0)"; then 5 | exit 0; 6 | fi 7 | 8 | # Publishing to NPM is currently supported by Travis CI, which ensures that all 9 | # tests pass first and the deployed module contains the correct file structure. 10 | # In order to prevent inadvertently circumventing this, we ensure that a CI 11 | # environment exists before continuing. 12 | if [ "$CI" != true ]; then 13 | echo "\n\n\n \033[101;30m Only CircleCI can publish to NPM. \033[0m" 1>&2; 14 | echo " Ensure git is left is a good state by backing out any commits and deleting any tags." 1>&2; 15 | echo " Then read CONTRIBUTING.md to learn how to publish to NPM.\n\n\n" 1>&2; 16 | exit 1; 17 | fi; 18 | 19 | npm run build; 20 | -------------------------------------------------------------------------------- /src/Explorer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // TODO: 1. Add default fields recursively 4 | // TODO: 2. Add default fields for all selections (not just fragments) 5 | // TODO: 3. Add stylesheet and remove inline styles 6 | // TODO: 4. Indication of when query in explorer diverges from query in editor pane 7 | // TODO: 5. Separate section for deprecated args, with support for 'beta' fields 8 | // TODO: 6. Custom default arg fields 9 | 10 | // Note: Attempted 1. and 2., but they were more annoying than helpful 11 | 12 | import * as React from 'react'; 13 | 14 | import { 15 | getNamedType, 16 | GraphQLObjectType, 17 | isEnumType, 18 | isInputObjectType, 19 | isInterfaceType, 20 | isLeafType, 21 | isNonNullType, 22 | isObjectType, 23 | isRequiredInputField, 24 | isScalarType, 25 | isUnionType, 26 | isWrappingType, 27 | parse, 28 | print, 29 | parseType, 30 | visit, 31 | } from 'graphql'; 32 | 33 | import type { 34 | ArgumentNode, 35 | DefinitionNode, 36 | DocumentNode, 37 | FieldNode, 38 | FragmentSpread, 39 | GraphQLArgument, 40 | GraphQLEnumType, 41 | GraphQLField, 42 | GraphQLFieldMap, 43 | GraphQLInputField, 44 | GraphQLInputType, 45 | GraphQLOutputType, 46 | GraphQLScalarType, 47 | GraphQLSchema, 48 | InlineFragmentNode, 49 | FragmentDefinitionNode, 50 | OperationDefinitionNode, 51 | ObjectFieldNode, 52 | ObjectValueNode, 53 | SelectionNode, 54 | SelectionSetNode, 55 | VariableDefinitionNode, 56 | ValueNode, 57 | } from 'graphql'; 58 | 59 | type Field = GraphQLField; 60 | 61 | type GetDefaultScalarArgValue = ( 62 | parentField: Field, 63 | arg: GraphQLArgument | GraphQLInputField, 64 | underlyingArgType: GraphQLEnumType | GraphQLScalarType, 65 | ) => ValueNode; 66 | 67 | type MakeDefaultArg = ( 68 | parentField: Field, 69 | arg: GraphQLArgument | GraphQLInputField, 70 | ) => boolean; 71 | 72 | type Colors = { 73 | keyword: string, 74 | def: string, 75 | property: string, 76 | qualifier: string, 77 | attribute: string, 78 | number: string, 79 | string: string, 80 | builtin: string, 81 | string2: string, 82 | variable: string, 83 | atom: string, 84 | }; 85 | 86 | type StyleMap = { 87 | [key: string]: any, 88 | }; 89 | 90 | type Styles = { 91 | explorerActionsStyle: StyleMap, 92 | buttonStyle: StyleMap, 93 | actionButtonStyle: StyleMap, 94 | }; 95 | 96 | type StyleConfig = { 97 | colors: Colors, 98 | arrowOpen: React.Node, 99 | arrowClosed: React.Node, 100 | checkboxChecked: React.Node, 101 | checkboxUnchecked: React.Node, 102 | styles: Styles, 103 | }; 104 | 105 | type Props = { 106 | query: string, 107 | width?: number, 108 | title?: string, 109 | schema?: ?GraphQLSchema, 110 | onEdit: string => void, 111 | getDefaultFieldNames?: ?(type: GraphQLObjectType) => Array, 112 | getDefaultScalarArgValue?: ?GetDefaultScalarArgValue, 113 | makeDefaultArg?: ?MakeDefaultArg, 114 | onToggleExplorer: () => void, 115 | explorerIsOpen: boolean, 116 | onRunOperation?: (name: ?string) => void, 117 | colors?: ?Colors, 118 | arrowOpen?: ?React.Node, 119 | arrowClosed?: ?React.Node, 120 | checkboxChecked?: ?React.Node, 121 | checkboxUnchecked?: ?React.Node, 122 | styles?: ?{ 123 | explorerActionsStyle?: StyleMap, 124 | buttonStyle?: StyleMap, 125 | actionButtonStyle?: StyleMap, 126 | }, 127 | showAttribution: boolean, 128 | hideActions?: boolean, 129 | externalFragments?: FragmentDefinitionNode[], 130 | }; 131 | 132 | type OperationType = 'query' | 'mutation' | 'subscription' | 'fragment'; 133 | type NewOperationType = 'query' | 'mutation' | 'subscription'; 134 | 135 | type State = {| 136 | operation: ?OperationDefinitionNode, 137 | newOperationType: NewOperationType, 138 | operationToScrollTo: ?string, 139 | |}; 140 | 141 | type Selections = $ReadOnlyArray; 142 | 143 | type AvailableFragments = {[key: string]: FragmentDefinitionNode}; 144 | 145 | function capitalize(string) { 146 | return string.charAt(0).toUpperCase() + string.slice(1); 147 | } 148 | 149 | // Names match class names in graphiql app.css 150 | // https://github.com/graphql/graphiql/blob/master/packages/graphiql/css/app.css 151 | const defaultColors: Colors = { 152 | keyword: '#B11A04', 153 | // OperationName, FragmentName 154 | def: '#D2054E', 155 | // FieldName 156 | property: '#1F61A0', 157 | // FieldAlias 158 | qualifier: '#1C92A9', 159 | // ArgumentName and ObjectFieldName 160 | attribute: '#8B2BB9', 161 | number: '#2882F9', 162 | string: '#D64292', 163 | // Boolean 164 | builtin: '#D47509', 165 | // Enum 166 | string2: '#0B7FC7', 167 | variable: '#397D13', 168 | // Type 169 | atom: '#CA9800', 170 | }; 171 | 172 | const defaultArrowOpen = ( 173 | 174 | 175 | 176 | ); 177 | 178 | const defaultArrowClosed = ( 179 | 180 | 181 | 182 | ); 183 | 184 | const defaultCheckboxChecked = ( 185 | 192 | 196 | 197 | ); 198 | 199 | const defaultCheckboxUnchecked = ( 200 | 207 | 211 | 212 | ); 213 | 214 | function Checkbox(props: {checked: boolean, styleConfig: StyleConfig}) { 215 | return props.checked 216 | ? props.styleConfig.checkboxChecked 217 | : props.styleConfig.checkboxUnchecked; 218 | } 219 | 220 | function defaultGetDefaultFieldNames(type: GraphQLObjectType): Array { 221 | const fields = type.getFields(); 222 | 223 | // Is there an `id` field? 224 | if (fields['id']) { 225 | const res = ['id']; 226 | if (fields['email']) { 227 | res.push('email'); 228 | } else if (fields['name']) { 229 | res.push('name'); 230 | } 231 | return res; 232 | } 233 | 234 | // Is there an `edges` field? 235 | if (fields['edges']) { 236 | return ['edges']; 237 | } 238 | 239 | // Is there an `node` field? 240 | if (fields['node']) { 241 | return ['node']; 242 | } 243 | 244 | if (fields['nodes']) { 245 | return ['nodes']; 246 | } 247 | 248 | // Include all leaf-type fields. 249 | const leafFieldNames = []; 250 | Object.keys(fields).forEach(fieldName => { 251 | if (isLeafType(fields[fieldName].type)) { 252 | leafFieldNames.push(fieldName); 253 | } 254 | }); 255 | 256 | if (!leafFieldNames.length) { 257 | // No leaf fields, add typename so that the query stays valid 258 | return ['__typename']; 259 | } 260 | return leafFieldNames.slice(0, 2); // Prevent too many fields from being added 261 | } 262 | 263 | function isRequiredArgument(arg: GraphQLArgument): boolean %checks { 264 | return isNonNullType(arg.type) && arg.defaultValue === undefined; 265 | } 266 | 267 | function unwrapOutputType(outputType: GraphQLOutputType): * { 268 | let unwrappedType = outputType; 269 | while (isWrappingType(unwrappedType)) { 270 | unwrappedType = unwrappedType.ofType; 271 | } 272 | return unwrappedType; 273 | } 274 | 275 | function unwrapInputType(inputType: GraphQLInputType): * { 276 | let unwrappedType = inputType; 277 | while (isWrappingType(unwrappedType)) { 278 | unwrappedType = unwrappedType.ofType; 279 | } 280 | return unwrappedType; 281 | } 282 | 283 | function coerceArgValue( 284 | argType: GraphQLScalarType | GraphQLEnumType, 285 | value: string | VariableDefinitionNode, 286 | ): ValueNode { 287 | // Handle the case where we're setting a variable as the value 288 | if (typeof value !== 'string' && value.kind === 'VariableDefinition') { 289 | return value.variable; 290 | } else if (isScalarType(argType)) { 291 | try { 292 | switch (argType.name) { 293 | case 'String': 294 | return { 295 | kind: 'StringValue', 296 | value: String(argType.parseValue(value)), 297 | }; 298 | case 'Float': 299 | return { 300 | kind: 'FloatValue', 301 | value: String(argType.parseValue(parseFloat(value))), 302 | }; 303 | case 'Int': 304 | return { 305 | kind: 'IntValue', 306 | value: String(argType.parseValue(parseInt(value, 10))), 307 | }; 308 | case 'Boolean': 309 | try { 310 | const parsed = JSON.parse(value); 311 | if (typeof parsed === 'boolean') { 312 | return {kind: 'BooleanValue', value: parsed}; 313 | } else { 314 | return {kind: 'BooleanValue', value: false}; 315 | } 316 | } catch (e) { 317 | return { 318 | kind: 'BooleanValue', 319 | value: false, 320 | }; 321 | } 322 | default: 323 | return { 324 | kind: 'StringValue', 325 | value: String(argType.parseValue(value)), 326 | }; 327 | } 328 | } catch (e) { 329 | console.error('error coercing arg value', e, value); 330 | return {kind: 'StringValue', value: value}; 331 | } 332 | } else { 333 | try { 334 | const parsedValue = argType.parseValue(value); 335 | if (parsedValue) { 336 | return {kind: 'EnumValue', value: String(parsedValue)}; 337 | } else { 338 | return {kind: 'EnumValue', value: argType.getValues()[0].name}; 339 | } 340 | } catch (e) { 341 | return {kind: 'EnumValue', value: argType.getValues()[0].name}; 342 | } 343 | } 344 | } 345 | 346 | type InputArgViewProps = {| 347 | arg: GraphQLArgument, 348 | selection: ObjectValueNode, 349 | parentField: Field, 350 | modifyFields: ( 351 | fields: $ReadOnlyArray, 352 | commit: boolean, 353 | ) => DocumentNode | null, 354 | getDefaultScalarArgValue: GetDefaultScalarArgValue, 355 | makeDefaultArg: ?MakeDefaultArg, 356 | onRunOperation: void => void, 357 | styleConfig: StyleConfig, 358 | onCommit: (newDoc: DocumentNode) => void, 359 | definition: FragmentDefinitionNode | OperationDefinitionNode, 360 | |}; 361 | 362 | class InputArgView extends React.PureComponent { 363 | _previousArgSelection: ?ObjectFieldNode; 364 | _getArgSelection = () => { 365 | return this.props.selection.fields.find( 366 | field => field.name.value === this.props.arg.name, 367 | ); 368 | }; 369 | 370 | _removeArg = () => { 371 | const {selection} = this.props; 372 | const argSelection = this._getArgSelection(); 373 | this._previousArgSelection = argSelection; 374 | this.props.modifyFields( 375 | selection.fields.filter(field => field !== argSelection), 376 | true, 377 | ); 378 | }; 379 | 380 | _addArg = () => { 381 | const { 382 | selection, 383 | arg, 384 | getDefaultScalarArgValue, 385 | parentField, 386 | makeDefaultArg, 387 | } = this.props; 388 | const argType = unwrapInputType(arg.type); 389 | 390 | let argSelection = null; 391 | if (this._previousArgSelection) { 392 | argSelection = this._previousArgSelection; 393 | } else if (isInputObjectType(argType)) { 394 | const fields = argType.getFields(); 395 | argSelection = { 396 | kind: 'ObjectField', 397 | name: {kind: 'Name', value: arg.name}, 398 | value: { 399 | kind: 'ObjectValue', 400 | fields: defaultInputObjectFields( 401 | getDefaultScalarArgValue, 402 | makeDefaultArg, 403 | parentField, 404 | Object.keys(fields).map(k => fields[k]), 405 | ), 406 | }, 407 | }; 408 | } else if (isLeafType(argType)) { 409 | argSelection = { 410 | kind: 'ObjectField', 411 | name: {kind: 'Name', value: arg.name}, 412 | value: getDefaultScalarArgValue(parentField, arg, argType), 413 | }; 414 | } 415 | 416 | if (!argSelection) { 417 | console.error('Unable to add arg for argType', argType); 418 | } else { 419 | return this.props.modifyFields( 420 | [...(selection.fields || []), argSelection], 421 | true, 422 | ); 423 | } 424 | }; 425 | 426 | _setArgValue = (event, options: ?{commit: boolean}) => { 427 | let settingToNull = false; 428 | let settingToVariable = false; 429 | let settingToLiteralValue = false; 430 | try { 431 | if (event.kind === 'VariableDefinition') { 432 | settingToVariable = true; 433 | } else if (event === null || typeof event === 'undefined') { 434 | settingToNull = true; 435 | } else if (typeof event.kind === 'string') { 436 | settingToLiteralValue = true; 437 | } 438 | } catch (e) {} 439 | 440 | const {selection} = this.props; 441 | 442 | const argSelection = this._getArgSelection(); 443 | 444 | if (!argSelection) { 445 | console.error('missing arg selection when setting arg value'); 446 | return; 447 | } 448 | const argType = unwrapInputType(this.props.arg.type); 449 | 450 | const handleable = 451 | isLeafType(argType) || 452 | settingToVariable || 453 | settingToNull || 454 | settingToLiteralValue; 455 | 456 | if (!handleable) { 457 | console.warn( 458 | 'Unable to handle non leaf types in InputArgView.setArgValue', 459 | event, 460 | ); 461 | return; 462 | } 463 | let targetValue: string | VariableDefinitionNode; 464 | let value: ?ValueNode; 465 | 466 | if (event === null || typeof event === 'undefined') { 467 | value = null; 468 | } else if ( 469 | !event.target && 470 | !!event.kind && 471 | event.kind === 'VariableDefinition' 472 | ) { 473 | targetValue = event; 474 | value = targetValue.variable; 475 | } else if (typeof event.kind === 'string') { 476 | value = event; 477 | } else if (event.target && typeof event.target.value === 'string') { 478 | targetValue = event.target.value; 479 | value = coerceArgValue(argType, targetValue); 480 | } 481 | 482 | const newDoc = this.props.modifyFields( 483 | (selection.fields || []).map(field => { 484 | const isTarget = field === argSelection; 485 | const newField = isTarget 486 | ? { 487 | ...field, 488 | value: value, 489 | } 490 | : field; 491 | 492 | return newField; 493 | }), 494 | options, 495 | ); 496 | 497 | return newDoc; 498 | }; 499 | 500 | _modifyChildFields = fields => { 501 | return this.props.modifyFields( 502 | this.props.selection.fields.map(field => 503 | field.name.value === this.props.arg.name 504 | ? { 505 | ...field, 506 | value: { 507 | kind: 'ObjectValue', 508 | fields: fields, 509 | }, 510 | } 511 | : field, 512 | ), 513 | true, 514 | ); 515 | }; 516 | 517 | render() { 518 | const {arg, parentField} = this.props; 519 | const argSelection = this._getArgSelection(); 520 | 521 | return ( 522 | 537 | ); 538 | } 539 | } 540 | 541 | type ArgViewProps = {| 542 | parentField: Field, 543 | arg: GraphQLArgument, 544 | selection: FieldNode, 545 | modifyArguments: ( 546 | argumentNodes: $ReadOnlyArray, 547 | commit: boolean, 548 | ) => DocumentNode | null, 549 | getDefaultScalarArgValue: GetDefaultScalarArgValue, 550 | makeDefaultArg: ?MakeDefaultArg, 551 | onRunOperation: void => void, 552 | styleConfig: StyleConfig, 553 | onCommit: (newDoc: DocumentNode) => void, 554 | definition: FragmentDefinitionNode | OperationDefinitionNode, 555 | |}; 556 | 557 | type ArgViewState = {||}; 558 | 559 | export function defaultValue( 560 | argType: GraphQLEnumType | GraphQLScalarType, 561 | ): ValueNode { 562 | if (isEnumType(argType)) { 563 | return {kind: 'EnumValue', value: argType.getValues()[0].name}; 564 | } else { 565 | switch (argType.name) { 566 | case 'String': 567 | return {kind: 'StringValue', value: ''}; 568 | case 'Float': 569 | return {kind: 'FloatValue', value: '1.5'}; 570 | case 'Int': 571 | return {kind: 'IntValue', value: '10'}; 572 | case 'Boolean': 573 | return {kind: 'BooleanValue', value: false}; 574 | default: 575 | return {kind: 'StringValue', value: ''}; 576 | } 577 | } 578 | } 579 | 580 | function defaultGetDefaultScalarArgValue( 581 | parentField: Field, 582 | arg: GraphQLArgument | GraphQLInputField, 583 | argType: GraphQLEnumType | GraphQLScalarType, 584 | ): ValueNode { 585 | return defaultValue(argType); 586 | } 587 | 588 | class ArgView extends React.PureComponent { 589 | _previousArgSelection: ?ArgumentNode; 590 | _getArgSelection = () => { 591 | const {selection} = this.props; 592 | 593 | return (selection.arguments || []).find( 594 | arg => arg.name.value === this.props.arg.name, 595 | ); 596 | }; 597 | _removeArg = (commit: boolean): DocumentNode | null => { 598 | const {selection} = this.props; 599 | const argSelection = this._getArgSelection(); 600 | this._previousArgSelection = argSelection; 601 | return this.props.modifyArguments( 602 | (selection.arguments || []).filter(arg => arg !== argSelection), 603 | commit, 604 | ); 605 | }; 606 | _addArg = (commit: boolean): DocumentNode | null => { 607 | const { 608 | selection, 609 | getDefaultScalarArgValue, 610 | makeDefaultArg, 611 | parentField, 612 | arg, 613 | } = this.props; 614 | const argType = unwrapInputType(arg.type); 615 | 616 | let argSelection = null; 617 | if (this._previousArgSelection) { 618 | argSelection = this._previousArgSelection; 619 | } else if (isInputObjectType(argType)) { 620 | const fields = argType.getFields(); 621 | argSelection = { 622 | kind: 'Argument', 623 | name: {kind: 'Name', value: arg.name}, 624 | value: { 625 | kind: 'ObjectValue', 626 | fields: defaultInputObjectFields( 627 | getDefaultScalarArgValue, 628 | makeDefaultArg, 629 | parentField, 630 | Object.keys(fields).map(k => fields[k]), 631 | ), 632 | }, 633 | }; 634 | } else if (isLeafType(argType)) { 635 | argSelection = { 636 | kind: 'Argument', 637 | name: {kind: 'Name', value: arg.name}, 638 | value: getDefaultScalarArgValue(parentField, arg, argType), 639 | }; 640 | } 641 | 642 | if (!argSelection) { 643 | console.error('Unable to add arg for argType', argType); 644 | return null; 645 | } else { 646 | return this.props.modifyArguments( 647 | [...(selection.arguments || []), argSelection], 648 | commit, 649 | ); 650 | } 651 | }; 652 | _setArgValue = ( 653 | event: SyntheticInputEvent<*> | VariableDefinitionNode, 654 | options: ?{commit: boolean}, 655 | ) => { 656 | let settingToNull = false; 657 | let settingToVariable = false; 658 | let settingToLiteralValue = false; 659 | try { 660 | if (event.kind === 'VariableDefinition') { 661 | settingToVariable = true; 662 | } else if (event === null || typeof event === 'undefined') { 663 | settingToNull = true; 664 | } else if (typeof event.kind === 'string') { 665 | settingToLiteralValue = true; 666 | } 667 | } catch (e) {} 668 | const {selection} = this.props; 669 | const argSelection = this._getArgSelection(); 670 | if (!argSelection && !settingToVariable) { 671 | console.error('missing arg selection when setting arg value'); 672 | return; 673 | } 674 | const argType = unwrapInputType(this.props.arg.type); 675 | 676 | const handleable = 677 | isLeafType(argType) || 678 | settingToVariable || 679 | settingToNull || 680 | settingToLiteralValue; 681 | 682 | if (!handleable) { 683 | console.warn('Unable to handle non leaf types in ArgView._setArgValue'); 684 | return; 685 | } 686 | 687 | let targetValue: string | VariableDefinitionNode; 688 | let value: ValueNode; 689 | 690 | if (event === null || typeof event === 'undefined') { 691 | value = null; 692 | } else if (event.target && typeof event.target.value === 'string') { 693 | targetValue = event.target.value; 694 | value = coerceArgValue(argType, targetValue); 695 | } else if (!event.target && event.kind === 'VariableDefinition') { 696 | targetValue = event; 697 | value = targetValue.variable; 698 | } else if (typeof event.kind === 'string') { 699 | value = event; 700 | } 701 | 702 | return this.props.modifyArguments( 703 | (selection.arguments || []).map(a => 704 | a === argSelection 705 | ? { 706 | ...a, 707 | value: value, 708 | } 709 | : a, 710 | ), 711 | options, 712 | ); 713 | }; 714 | 715 | _setArgFields = (fields, commit: boolean): DocumentNode | null => { 716 | const {selection} = this.props; 717 | const argSelection = this._getArgSelection(); 718 | if (!argSelection) { 719 | console.error('missing arg selection when setting arg value'); 720 | return; 721 | } 722 | 723 | return this.props.modifyArguments( 724 | (selection.arguments || []).map(a => 725 | a === argSelection 726 | ? { 727 | ...a, 728 | value: { 729 | kind: 'ObjectValue', 730 | fields, 731 | }, 732 | } 733 | : a, 734 | ), 735 | commit, 736 | ); 737 | }; 738 | 739 | render() { 740 | const {arg, parentField} = this.props; 741 | const argSelection = this._getArgSelection(); 742 | 743 | return ( 744 | 759 | ); 760 | } 761 | } 762 | 763 | function isRunShortcut(event) { 764 | return event.ctrlKey && event.key === 'Enter'; 765 | } 766 | 767 | function canRunOperation(operationName) { 768 | // it does not make sense to try to execute a fragment 769 | return operationName !== 'FragmentDefinition'; 770 | } 771 | 772 | type ScalarInputProps = {| 773 | arg: GraphQLArgument, 774 | argValue: ValueNode, 775 | setArgValue: ( 776 | SyntheticInputEvent<*> | VariableDefinitionNode, 777 | commit: boolean, 778 | ) => DocumentNode | null, 779 | onRunOperation: void => void, 780 | styleConfig: StyleConfig, 781 | |}; 782 | 783 | class ScalarInput extends React.PureComponent { 784 | _ref: ?any; 785 | _handleChange = event => { 786 | this.props.setArgValue(event, true); 787 | }; 788 | 789 | componentDidMount() { 790 | const input = this._ref; 791 | const activeElement = document.activeElement; 792 | if ( 793 | input && 794 | activeElement && 795 | !(activeElement instanceof HTMLTextAreaElement) 796 | ) { 797 | input.focus(); 798 | input.setSelectionRange(0, input.value.length); 799 | } 800 | } 801 | 802 | render() { 803 | const {arg, argValue, styleConfig} = this.props; 804 | const argType = unwrapInputType(arg.type); 805 | const value = typeof argValue.value === 'string' ? argValue.value : ''; 806 | const color = 807 | this.props.argValue.kind === 'StringValue' 808 | ? styleConfig.colors.string 809 | : styleConfig.colors.number; 810 | return ( 811 | 812 | {argType.name === 'String' ? '"' : ''} 813 | { 822 | this._ref = ref; 823 | }} 824 | type="text" 825 | onChange={this._handleChange} 826 | value={value} 827 | /> 828 | {argType.name === 'String' ? '"' : ''} 829 | 830 | ); 831 | } 832 | } 833 | 834 | type AbstractArgViewProps = {| 835 | argValue: ?ValueNode, 836 | arg: GraphQLArgument, 837 | parentField: Field, 838 | setArgValue: ( 839 | SyntheticInputEvent<*> | VariableDefinitionNode, 840 | commit: boolean, 841 | ) => DocumentNode | null, 842 | setArgFields: ( 843 | fields: $ReadOnlyArray, 844 | commit: boolean, 845 | ) => DocumentNode | null, 846 | addArg: (commit: boolean) => DocumentNode | null, 847 | removeArg: (commit: boolean) => DocumentNode | null, 848 | onCommit: (newDoc: DocumentNode) => void, 849 | getDefaultScalarArgValue: GetDefaultScalarArgValue, 850 | makeDefaultArg: ?MakeDefaultArg, 851 | onRunOperation: void => void, 852 | styleConfig: StyleConfig, 853 | definition: FragmentDefinitionNode | OperationDefinitionNode, 854 | |}; 855 | 856 | class AbstractArgView extends React.PureComponent< 857 | AbstractArgViewProps, 858 | {|displayArgActions: boolean|}, 859 | > { 860 | state = {displayArgActions: false}; 861 | render() { 862 | const {argValue, arg, styleConfig} = this.props; 863 | /* TODO: handle List types*/ 864 | const argType = unwrapInputType(arg.type); 865 | 866 | let input = null; 867 | if (argValue) { 868 | if (argValue.kind === 'Variable') { 869 | input = ( 870 | 871 | ${argValue.name.value} 872 | 873 | ); 874 | } else if (isScalarType(argType)) { 875 | if (argType.name === 'Boolean') { 876 | input = ( 877 | 892 | ); 893 | } else { 894 | input = ( 895 | 902 | ); 903 | } 904 | } else if (isEnumType(argType)) { 905 | if (argValue.kind === 'EnumValue') { 906 | input = ( 907 | 920 | ); 921 | } else { 922 | console.error( 923 | 'arg mismatch between arg and selection', 924 | argType, 925 | argValue, 926 | ); 927 | } 928 | } else if (isInputObjectType(argType)) { 929 | if (argValue.kind === 'ObjectValue') { 930 | const fields = argType.getFields(); 931 | input = ( 932 |
933 | {Object.keys(fields) 934 | .sort() 935 | .map(fieldName => ( 936 | 951 | ))} 952 |
953 | ); 954 | } else { 955 | console.error( 956 | 'arg mismatch between arg and selection', 957 | argType, 958 | argValue, 959 | ); 960 | } 961 | } 962 | } 963 | 964 | const variablize = () => { 965 | /** 966 | 1. Find current operation variables 967 | 2. Find current arg value 968 | 3. Create a new variable 969 | 4. Replace current arg value with variable 970 | 5. Add variable to operation 971 | */ 972 | 973 | const baseVariableName = arg.name; 974 | const conflictingNameCount = ( 975 | this.props.definition.variableDefinitions || [] 976 | ).filter(varDef => 977 | varDef.variable.name.value.startsWith(baseVariableName), 978 | ).length; 979 | 980 | let variableName; 981 | if (conflictingNameCount > 0) { 982 | variableName = `${baseVariableName}${conflictingNameCount}`; 983 | } else { 984 | variableName = baseVariableName; 985 | } 986 | // To get an AST definition of our variable from the instantiated arg, 987 | // we print it to a string, then parseType to get our AST. 988 | const argPrintedType = arg.type.toString(); 989 | const argType = parseType(argPrintedType); 990 | 991 | const base: VariableDefinitionNode = { 992 | kind: 'VariableDefinition', 993 | variable: { 994 | kind: 'Variable', 995 | name: { 996 | kind: 'Name', 997 | value: variableName, 998 | }, 999 | }, 1000 | type: argType, 1001 | directives: [], 1002 | }; 1003 | 1004 | const variableDefinitionByName = name => 1005 | (this.props.definition.variableDefinitions || []).find( 1006 | varDef => varDef.variable.name.value === name, 1007 | ); 1008 | 1009 | let variable: ?VariableDefinitionNode; 1010 | 1011 | let subVariableUsageCountByName: { 1012 | [key: string]: number, 1013 | } = {}; 1014 | 1015 | if (typeof argValue !== 'undefined' && argValue !== null) { 1016 | /** In the process of devariabilizing descendent selections, 1017 | * we may have caused their variable definitions to become unused. 1018 | * Keep track and remove any variable definitions with 1 or fewer usages. 1019 | * */ 1020 | const cleanedDefaultValue = visit(argValue, { 1021 | Variable(node) { 1022 | const varName = node.name.value; 1023 | const varDef = variableDefinitionByName(varName); 1024 | 1025 | subVariableUsageCountByName[varName] = 1026 | subVariableUsageCountByName[varName] + 1 || 1; 1027 | 1028 | if (!varDef) { 1029 | return; 1030 | } 1031 | 1032 | return varDef.defaultValue; 1033 | }, 1034 | }); 1035 | 1036 | const isNonNullable = base.type.kind === 'NonNullType'; 1037 | 1038 | // We're going to give the variable definition a default value, so we must make its type nullable 1039 | const unwrappedBase = isNonNullable 1040 | ? {...base, type: base.type.type} 1041 | : base; 1042 | 1043 | variable = {...unwrappedBase, defaultValue: cleanedDefaultValue}; 1044 | } else { 1045 | variable = base; 1046 | } 1047 | 1048 | const newlyUnusedVariables = Object.entries(subVariableUsageCountByName) 1049 | // $FlowFixMe: Can't get Object.entries to realize usageCount *must* be a number 1050 | .filter(([_, usageCount]: [string, number]) => usageCount < 2) 1051 | .map(([varName: string, _]) => varName); 1052 | 1053 | if (variable) { 1054 | const newDoc: ?DocumentNode = this.props.setArgValue(variable, false); 1055 | 1056 | if (newDoc) { 1057 | const targetOperation = newDoc.definitions.find(definition => { 1058 | if ( 1059 | !!definition.operation && 1060 | !!definition.name && 1061 | !!definition.name.value && 1062 | // 1063 | !!this.props.definition.name && 1064 | !!this.props.definition.name.value 1065 | ) { 1066 | return definition.name.value === this.props.definition.name.value; 1067 | } else { 1068 | return false; 1069 | } 1070 | }); 1071 | 1072 | const newVariableDefinitions: Array = [ 1073 | ...(targetOperation.variableDefinitions || []), 1074 | variable, 1075 | ].filter( 1076 | varDef => 1077 | newlyUnusedVariables.indexOf(varDef.variable.name.value) === -1, 1078 | ); 1079 | 1080 | const newOperation = { 1081 | ...targetOperation, 1082 | variableDefinitions: newVariableDefinitions, 1083 | }; 1084 | 1085 | const existingDefs = newDoc.definitions; 1086 | 1087 | const newDefinitions = existingDefs.map(existingOperation => { 1088 | if (targetOperation === existingOperation) { 1089 | return newOperation; 1090 | } else { 1091 | return existingOperation; 1092 | } 1093 | }); 1094 | 1095 | const finalDoc = { 1096 | ...newDoc, 1097 | definitions: newDefinitions, 1098 | }; 1099 | 1100 | this.props.onCommit(finalDoc); 1101 | } 1102 | } 1103 | }; 1104 | 1105 | const devariablize = () => { 1106 | /** 1107 | * 1. Find the current variable definition in the operation def 1108 | * 2. Extract its value 1109 | * 3. Replace the current arg value 1110 | * 4. Visit the resulting operation to see if there are any other usages of the variable 1111 | * 5. If not, remove the variableDefinition 1112 | */ 1113 | if (!argValue || !argValue.name || !argValue.name.value) { 1114 | return; 1115 | } 1116 | 1117 | const variableName = argValue.name.value; 1118 | const variableDefinition = ( 1119 | this.props.definition.variableDefinitions || [] 1120 | ).find(varDef => varDef.variable.name.value === variableName); 1121 | 1122 | if (!variableDefinition) { 1123 | return; 1124 | } 1125 | 1126 | const defaultValue = variableDefinition.defaultValue; 1127 | 1128 | const newDoc: ?DocumentNode = this.props.setArgValue(defaultValue, { 1129 | commit: false, 1130 | }); 1131 | 1132 | if (newDoc) { 1133 | const targetOperation: ?OperationDefinitionNode = newDoc.definitions.find( 1134 | definition => 1135 | definition.name.value === this.props.definition.name.value, 1136 | ); 1137 | 1138 | if (!targetOperation) { 1139 | return; 1140 | } 1141 | 1142 | // After de-variabilizing, see if the variable is still in use. If not, remove it. 1143 | let variableUseCount = 0; 1144 | 1145 | visit(targetOperation, { 1146 | Variable(node) { 1147 | if (node.name.value === variableName) { 1148 | variableUseCount = variableUseCount + 1; 1149 | } 1150 | }, 1151 | }); 1152 | 1153 | let newVariableDefinitions = targetOperation.variableDefinitions || []; 1154 | 1155 | // A variable is in use if it shows up at least twice (once in the definition, once in the selection) 1156 | if (variableUseCount < 2) { 1157 | newVariableDefinitions = newVariableDefinitions.filter( 1158 | varDef => varDef.variable.name.value !== variableName, 1159 | ); 1160 | } 1161 | 1162 | const newOperation = { 1163 | ...targetOperation, 1164 | variableDefinitions: newVariableDefinitions, 1165 | }; 1166 | 1167 | const existingDefs = newDoc.definitions; 1168 | 1169 | const newDefinitions = existingDefs.map(existingOperation => { 1170 | if (targetOperation === existingOperation) { 1171 | return newOperation; 1172 | } else { 1173 | return existingOperation; 1174 | } 1175 | }); 1176 | 1177 | const finalDoc = { 1178 | ...newDoc, 1179 | definitions: newDefinitions, 1180 | }; 1181 | 1182 | this.props.onCommit(finalDoc); 1183 | } 1184 | }; 1185 | 1186 | const isArgValueVariable = argValue && argValue.kind === 'Variable'; 1187 | 1188 | const variablizeActionButton = !this.state.displayArgActions ? null : ( 1189 | 1210 | ); 1211 | 1212 | return ( 1213 |
1223 | { 1226 | const shouldAdd = !argValue; 1227 | if (shouldAdd) { 1228 | this.props.addArg(true); 1229 | } else { 1230 | this.props.removeArg(true); 1231 | } 1232 | this.setState({displayArgActions: shouldAdd}); 1233 | }}> 1234 | {isInputObjectType(argType) ? ( 1235 | 1236 | {!!argValue 1237 | ? this.props.styleConfig.arrowOpen 1238 | : this.props.styleConfig.arrowClosed} 1239 | 1240 | ) : ( 1241 | 1245 | )} 1246 | { 1250 | // Make implementation a bit easier and only show 'variablize' action if arg is already added 1251 | if (argValue !== null && typeof argValue !== 'undefined') { 1252 | this.setState({displayArgActions: true}); 1253 | } 1254 | }} 1255 | onMouseLeave={() => this.setState({displayArgActions: false})}> 1256 | {arg.name} 1257 | {isRequiredArgument(arg) ? '*' : ''}: {variablizeActionButton}{' '} 1258 | {' '} 1259 | 1260 | {input || }{' '} 1261 |
1262 | ); 1263 | } 1264 | } 1265 | 1266 | type AbstractViewProps = {| 1267 | implementingType: GraphQLObjectType, 1268 | selections: Selections, 1269 | modifySelections: ( 1270 | selections: Selections, 1271 | ?{commit: boolean}, 1272 | ) => DocumentNode | null, 1273 | schema: GraphQLSchema, 1274 | getDefaultFieldNames: (type: GraphQLObjectType) => Array, 1275 | getDefaultScalarArgValue: GetDefaultScalarArgValue, 1276 | makeDefaultArg: ?MakeDefaultArg, 1277 | onRunOperation: void => void, 1278 | onCommit: (newDoc: DocumentNode) => void, 1279 | styleConfig: StyleConfig, 1280 | definition: FragmentDefinitionNode | OperationDefinitionNode, 1281 | |}; 1282 | 1283 | class AbstractView extends React.PureComponent { 1284 | _previousSelection: ?InlineFragmentNode; 1285 | _addFragment = () => { 1286 | this.props.modifySelections([ 1287 | ...this.props.selections, 1288 | this._previousSelection || { 1289 | kind: 'InlineFragment', 1290 | typeCondition: { 1291 | kind: 'NamedType', 1292 | name: {kind: 'Name', value: this.props.implementingType.name}, 1293 | }, 1294 | selectionSet: { 1295 | kind: 'SelectionSet', 1296 | selections: this.props 1297 | .getDefaultFieldNames(this.props.implementingType) 1298 | .map(fieldName => ({ 1299 | kind: 'Field', 1300 | name: {kind: 'Name', value: fieldName}, 1301 | })), 1302 | }, 1303 | }, 1304 | ]); 1305 | }; 1306 | _removeFragment = () => { 1307 | const thisSelection = this._getSelection(); 1308 | this._previousSelection = thisSelection; 1309 | this.props.modifySelections( 1310 | this.props.selections.filter(s => s !== thisSelection), 1311 | ); 1312 | }; 1313 | _getSelection = (): ?InlineFragmentNode => { 1314 | const selection = this.props.selections.find( 1315 | selection => 1316 | selection.kind === 'InlineFragment' && 1317 | selection.typeCondition && 1318 | this.props.implementingType.name === selection.typeCondition.name.value, 1319 | ); 1320 | if (!selection) { 1321 | return null; 1322 | } 1323 | if (selection.kind === 'InlineFragment') { 1324 | return selection; 1325 | } 1326 | }; 1327 | 1328 | _modifyChildSelections = ( 1329 | selections: Selections, 1330 | options: ?{commit: boolean}, 1331 | ): DocumentNode | null => { 1332 | const thisSelection = this._getSelection(); 1333 | return this.props.modifySelections( 1334 | this.props.selections.map(selection => { 1335 | if (selection === thisSelection) { 1336 | return { 1337 | directives: selection.directives, 1338 | kind: 'InlineFragment', 1339 | typeCondition: { 1340 | kind: 'NamedType', 1341 | name: {kind: 'Name', value: this.props.implementingType.name}, 1342 | }, 1343 | selectionSet: { 1344 | kind: 'SelectionSet', 1345 | selections, 1346 | }, 1347 | }; 1348 | } 1349 | return selection; 1350 | }), 1351 | options, 1352 | ); 1353 | }; 1354 | 1355 | render() { 1356 | const { 1357 | implementingType, 1358 | schema, 1359 | getDefaultFieldNames, 1360 | styleConfig, 1361 | } = this.props; 1362 | const selection = this._getSelection(); 1363 | const fields = implementingType.getFields(); 1364 | const childSelections = selection 1365 | ? selection.selectionSet 1366 | ? selection.selectionSet.selections 1367 | : [] 1368 | : []; 1369 | 1370 | return ( 1371 |
1372 | 1375 | 1379 | 1380 | {this.props.implementingType.name} 1381 | 1382 | 1383 | {selection ? ( 1384 |
1385 | {Object.keys(fields) 1386 | .sort() 1387 | .map(fieldName => ( 1388 | 1403 | ))} 1404 |
1405 | ) : null} 1406 |
1407 | ); 1408 | } 1409 | } 1410 | 1411 | type FragmentViewProps = {| 1412 | fragment: FragmentDefinitionNode, 1413 | selections: Selections, 1414 | modifySelections: ( 1415 | selections: Selections, 1416 | ?{commit: boolean}, 1417 | ) => DocumentNode | null, 1418 | onCommit: (newDoc: DocumentNode) => void, 1419 | schema: GraphQLSchema, 1420 | styleConfig: StyleConfig, 1421 | |}; 1422 | 1423 | class FragmentView extends React.PureComponent { 1424 | _previousSelection: ?InlineFragmentNode; 1425 | _addFragment = () => { 1426 | this.props.modifySelections([ 1427 | ...this.props.selections, 1428 | this._previousSelection || { 1429 | kind: 'FragmentSpread', 1430 | name: this.props.fragment.name, 1431 | }, 1432 | ]); 1433 | }; 1434 | _removeFragment = () => { 1435 | const thisSelection = this._getSelection(); 1436 | this._previousSelection = thisSelection; 1437 | this.props.modifySelections( 1438 | this.props.selections.filter(s => { 1439 | const isTargetSelection = 1440 | s.kind === 'FragmentSpread' && 1441 | s.name.value === this.props.fragment.name.value; 1442 | 1443 | return !isTargetSelection; 1444 | }), 1445 | ); 1446 | }; 1447 | _getSelection = (): ?FragmentSpread => { 1448 | const selection = this.props.selections.find(selection => { 1449 | return ( 1450 | selection.kind === 'FragmentSpread' && 1451 | selection.name.value === this.props.fragment.name.value 1452 | ); 1453 | }); 1454 | 1455 | return selection; 1456 | }; 1457 | 1458 | render() { 1459 | const {styleConfig} = this.props; 1460 | const selection = this._getSelection(); 1461 | return ( 1462 |
1463 | 1466 | 1470 | 1473 | {this.props.fragment.name.value} 1474 | 1475 | 1476 |
1477 | ); 1478 | } 1479 | } 1480 | 1481 | type FieldViewProps = {| 1482 | field: Field, 1483 | selections: Selections, 1484 | modifySelections: ( 1485 | selections: Selections, 1486 | ?{commit: boolean}, 1487 | ) => DocumentNode | null, 1488 | schema: GraphQLSchema, 1489 | getDefaultFieldNames: (type: GraphQLObjectType) => Array, 1490 | getDefaultScalarArgValue: GetDefaultScalarArgValue, 1491 | makeDefaultArg: ?MakeDefaultArg, 1492 | onRunOperation: void => void, 1493 | styleConfig: StyleConfig, 1494 | onCommit: (newDoc: DocumentNode) => void, 1495 | definition: FragmentDefinitionNode | OperationDefinitionNode, 1496 | availableFragments: AvailableFragments, 1497 | |}; 1498 | 1499 | function defaultInputObjectFields( 1500 | getDefaultScalarArgValue: GetDefaultScalarArgValue, 1501 | makeDefaultArg: ?MakeDefaultArg, 1502 | parentField: Field, 1503 | fields: Array, 1504 | ): Array { 1505 | const nodes = []; 1506 | for (const field of fields) { 1507 | if ( 1508 | isRequiredInputField(field) || 1509 | (makeDefaultArg && makeDefaultArg(parentField, field)) 1510 | ) { 1511 | const fieldType = unwrapInputType(field.type); 1512 | if (isInputObjectType(fieldType)) { 1513 | const fields = fieldType.getFields(); 1514 | nodes.push({ 1515 | kind: 'ObjectField', 1516 | name: {kind: 'Name', value: field.name}, 1517 | value: { 1518 | kind: 'ObjectValue', 1519 | fields: defaultInputObjectFields( 1520 | getDefaultScalarArgValue, 1521 | makeDefaultArg, 1522 | parentField, 1523 | Object.keys(fields).map(k => fields[k]), 1524 | ), 1525 | }, 1526 | }); 1527 | } else if (isLeafType(fieldType)) { 1528 | nodes.push({ 1529 | kind: 'ObjectField', 1530 | name: {kind: 'Name', value: field.name}, 1531 | value: getDefaultScalarArgValue(parentField, field, fieldType), 1532 | }); 1533 | } 1534 | } 1535 | } 1536 | return nodes; 1537 | } 1538 | 1539 | function defaultArgs( 1540 | getDefaultScalarArgValue: GetDefaultScalarArgValue, 1541 | makeDefaultArg: ?MakeDefaultArg, 1542 | field: Field, 1543 | ): Array { 1544 | const args = []; 1545 | for (const arg of field.args) { 1546 | if ( 1547 | isRequiredArgument(arg) || 1548 | (makeDefaultArg && makeDefaultArg(field, arg)) 1549 | ) { 1550 | const argType = unwrapInputType(arg.type); 1551 | if (isInputObjectType(argType)) { 1552 | const fields = argType.getFields(); 1553 | args.push({ 1554 | kind: 'Argument', 1555 | name: {kind: 'Name', value: arg.name}, 1556 | value: { 1557 | kind: 'ObjectValue', 1558 | fields: defaultInputObjectFields( 1559 | getDefaultScalarArgValue, 1560 | makeDefaultArg, 1561 | field, 1562 | Object.keys(fields).map(k => fields[k]), 1563 | ), 1564 | }, 1565 | }); 1566 | } else if (isLeafType(argType)) { 1567 | args.push({ 1568 | kind: 'Argument', 1569 | name: {kind: 'Name', value: arg.name}, 1570 | value: getDefaultScalarArgValue(field, arg, argType), 1571 | }); 1572 | } 1573 | } 1574 | } 1575 | return args; 1576 | } 1577 | 1578 | class FieldView extends React.PureComponent< 1579 | FieldViewProps, 1580 | {|displayFieldActions: boolean|}, 1581 | > { 1582 | state = {displayFieldActions: false}; 1583 | 1584 | _previousSelection: ?SelectionNode; 1585 | _addAllFieldsToSelections = rawSubfields => { 1586 | const subFields: Array = !!rawSubfields 1587 | ? Object.keys(rawSubfields).map(fieldName => { 1588 | return { 1589 | kind: 'Field', 1590 | name: {kind: 'Name', value: fieldName}, 1591 | arguments: [], 1592 | }; 1593 | }) 1594 | : []; 1595 | 1596 | const subSelectionSet: SelectionSetNode = { 1597 | kind: 'SelectionSet', 1598 | selections: subFields, 1599 | }; 1600 | 1601 | const nextSelections = [ 1602 | ...this.props.selections.filter(selection => { 1603 | if (selection.kind === 'InlineFragment') { 1604 | return true; 1605 | } else { 1606 | // Remove the current selection set for the target field 1607 | return selection.name.value !== this.props.field.name; 1608 | } 1609 | }), 1610 | { 1611 | kind: 'Field', 1612 | name: {kind: 'Name', value: this.props.field.name}, 1613 | arguments: defaultArgs( 1614 | this.props.getDefaultScalarArgValue, 1615 | this.props.makeDefaultArg, 1616 | this.props.field, 1617 | ), 1618 | selectionSet: subSelectionSet, 1619 | }, 1620 | ]; 1621 | 1622 | this.props.modifySelections(nextSelections); 1623 | }; 1624 | 1625 | _addFieldToSelections = rawSubfields => { 1626 | const nextSelections = [ 1627 | ...this.props.selections, 1628 | this._previousSelection || { 1629 | kind: 'Field', 1630 | name: {kind: 'Name', value: this.props.field.name}, 1631 | arguments: defaultArgs( 1632 | this.props.getDefaultScalarArgValue, 1633 | this.props.makeDefaultArg, 1634 | this.props.field, 1635 | ), 1636 | }, 1637 | ]; 1638 | 1639 | this.props.modifySelections(nextSelections); 1640 | }; 1641 | 1642 | _handleUpdateSelections = event => { 1643 | const selection = this._getSelection(); 1644 | if (selection && !event.altKey) { 1645 | this._removeFieldFromSelections(); 1646 | } else { 1647 | const fieldType = getNamedType(this.props.field.type); 1648 | const rawSubfields = isObjectType(fieldType) && fieldType.getFields(); 1649 | 1650 | const shouldSelectAllSubfields = !!rawSubfields && event.altKey; 1651 | 1652 | shouldSelectAllSubfields 1653 | ? this._addAllFieldsToSelections(rawSubfields) 1654 | : this._addFieldToSelections(rawSubfields); 1655 | } 1656 | }; 1657 | 1658 | _removeFieldFromSelections = () => { 1659 | const previousSelection = this._getSelection(); 1660 | this._previousSelection = previousSelection; 1661 | this.props.modifySelections( 1662 | this.props.selections.filter( 1663 | selection => selection !== previousSelection, 1664 | ), 1665 | ); 1666 | }; 1667 | _getSelection = (): ?FieldNode => { 1668 | const selection = this.props.selections.find( 1669 | selection => 1670 | selection.kind === 'Field' && 1671 | this.props.field.name === selection.name.value, 1672 | ); 1673 | if (!selection) { 1674 | return null; 1675 | } 1676 | if (selection.kind === 'Field') { 1677 | return selection; 1678 | } 1679 | }; 1680 | 1681 | _setArguments = ( 1682 | argumentNodes: $ReadOnlyArray, 1683 | options: ?{commit: boolean}, 1684 | ): DocumentNode | null => { 1685 | const selection = this._getSelection(); 1686 | if (!selection) { 1687 | console.error('Missing selection when setting arguments', argumentNodes); 1688 | return; 1689 | } 1690 | return this.props.modifySelections( 1691 | this.props.selections.map(s => 1692 | s === selection 1693 | ? { 1694 | alias: selection.alias, 1695 | arguments: argumentNodes, 1696 | directives: selection.directives, 1697 | kind: 'Field', 1698 | name: selection.name, 1699 | selectionSet: selection.selectionSet, 1700 | } 1701 | : s, 1702 | ), 1703 | options, 1704 | ); 1705 | }; 1706 | 1707 | _modifyChildSelections = ( 1708 | selections: Selections, 1709 | options: ?{commit: boolean}, 1710 | ): DocumentNode | null => { 1711 | return this.props.modifySelections( 1712 | this.props.selections.map(selection => { 1713 | if ( 1714 | selection.kind === 'Field' && 1715 | this.props.field.name === selection.name.value 1716 | ) { 1717 | if (selection.kind !== 'Field') { 1718 | throw new Error('invalid selection'); 1719 | } 1720 | return { 1721 | alias: selection.alias, 1722 | arguments: selection.arguments, 1723 | directives: selection.directives, 1724 | kind: 'Field', 1725 | name: selection.name, 1726 | selectionSet: { 1727 | kind: 'SelectionSet', 1728 | selections, 1729 | }, 1730 | }; 1731 | } 1732 | return selection; 1733 | }), 1734 | options, 1735 | ); 1736 | }; 1737 | 1738 | render() { 1739 | const {field, schema, getDefaultFieldNames, styleConfig} = this.props; 1740 | const selection = this._getSelection(); 1741 | const type = unwrapOutputType(field.type); 1742 | const args = field.args.sort((a, b) => a.name.localeCompare(b.name)); 1743 | let className = `graphiql-explorer-node graphiql-explorer-${field.name}`; 1744 | 1745 | if (field.isDeprecated) { 1746 | className += ' graphiql-explorer-deprecated'; 1747 | } 1748 | 1749 | const applicableFragments = 1750 | isObjectType(type) || isInterfaceType(type) || isUnionType(type) 1751 | ? this.props.availableFragments && 1752 | this.props.availableFragments[type.name] 1753 | : null; 1754 | 1755 | const node = ( 1756 |
1757 | { 1771 | const containsMeaningfulSubselection = 1772 | isObjectType(type) && 1773 | selection && 1774 | selection.selectionSet && 1775 | selection.selectionSet.selections.filter( 1776 | selection => selection.kind !== 'FragmentSpread', 1777 | ).length > 0; 1778 | 1779 | if (containsMeaningfulSubselection) { 1780 | this.setState({displayFieldActions: true}); 1781 | } 1782 | }} 1783 | onMouseLeave={() => this.setState({displayFieldActions: false})}> 1784 | {isObjectType(type) ? ( 1785 | 1786 | {!!selection 1787 | ? this.props.styleConfig.arrowOpen 1788 | : this.props.styleConfig.arrowClosed} 1789 | 1790 | ) : null} 1791 | {isObjectType(type) ? null : ( 1792 | 1796 | )} 1797 | 1800 | {field.name} 1801 | 1802 | {!this.state.displayFieldActions ? null : ( 1803 | 1885 | )} 1886 | 1887 | {selection && args.length ? ( 1888 |
1891 | {args.map(arg => ( 1892 | 1905 | ))} 1906 |
1907 | ) : null} 1908 |
1909 | ); 1910 | 1911 | if ( 1912 | selection && 1913 | (isObjectType(type) || isInterfaceType(type) || isUnionType(type)) 1914 | ) { 1915 | const fields = isUnionType(type) ? {} : type.getFields(); 1916 | const childSelections = selection 1917 | ? selection.selectionSet 1918 | ? selection.selectionSet.selections 1919 | : [] 1920 | : []; 1921 | return ( 1922 |
1923 | {node} 1924 |
1925 | {!!applicableFragments 1926 | ? applicableFragments.map(fragment => { 1927 | const type = schema.getType( 1928 | fragment.typeCondition.name.value, 1929 | ); 1930 | const fragmentName = fragment.name.value; 1931 | return !type ? null : ( 1932 | 1941 | ); 1942 | }) 1943 | : null} 1944 | {Object.keys(fields) 1945 | .sort() 1946 | .map(fieldName => ( 1947 | 1962 | ))} 1963 | {isInterfaceType(type) || isUnionType(type) 1964 | ? schema 1965 | .getPossibleTypes(type) 1966 | .map(type => ( 1967 | 1983 | )) 1984 | : null} 1985 |
1986 |
1987 | ); 1988 | } 1989 | return node; 1990 | } 1991 | } 1992 | 1993 | function parseQuery(text: string): ?DocumentNode | Error { 1994 | try { 1995 | if (!text.trim()) { 1996 | return null; 1997 | } 1998 | return parse( 1999 | text, 2000 | // Tell graphql to not bother track locations when parsing, we don't need 2001 | // it and it's a tiny bit more expensive. 2002 | {noLocation: true}, 2003 | ); 2004 | } catch (e) { 2005 | return new Error(e); 2006 | } 2007 | } 2008 | 2009 | const DEFAULT_OPERATION = { 2010 | kind: 'OperationDefinition', 2011 | operation: 'query', 2012 | variableDefinitions: [], 2013 | name: {kind: 'Name', value: 'MyQuery'}, 2014 | directives: [], 2015 | selectionSet: { 2016 | kind: 'SelectionSet', 2017 | selections: [], 2018 | }, 2019 | }; 2020 | const DEFAULT_DOCUMENT = { 2021 | kind: 'Document', 2022 | definitions: [DEFAULT_OPERATION], 2023 | }; 2024 | let parseQueryMemoize: ?[string, DocumentNode] = null; 2025 | function memoizeParseQuery(query: string): DocumentNode { 2026 | if (parseQueryMemoize && parseQueryMemoize[0] === query) { 2027 | return parseQueryMemoize[1]; 2028 | } else { 2029 | const result = parseQuery(query); 2030 | if (!result) { 2031 | return DEFAULT_DOCUMENT; 2032 | } else if (result instanceof Error) { 2033 | if (parseQueryMemoize) { 2034 | // Most likely a temporarily invalid query while they type 2035 | return parseQueryMemoize[1]; 2036 | } else { 2037 | return DEFAULT_DOCUMENT; 2038 | } 2039 | } else { 2040 | parseQueryMemoize = [query, result]; 2041 | return result; 2042 | } 2043 | } 2044 | } 2045 | 2046 | const defaultStyles = { 2047 | buttonStyle: { 2048 | fontSize: '1.2em', 2049 | padding: '0px', 2050 | backgroundColor: 'white', 2051 | border: 'none', 2052 | margin: '5px 0px', 2053 | height: '40px', 2054 | width: '100%', 2055 | display: 'block', 2056 | maxWidth: 'none', 2057 | }, 2058 | 2059 | actionButtonStyle: { 2060 | padding: '0px', 2061 | backgroundColor: 'white', 2062 | border: 'none', 2063 | margin: '0px', 2064 | maxWidth: 'none', 2065 | height: '15px', 2066 | width: '15px', 2067 | display: 'inline-block', 2068 | fontSize: 'smaller', 2069 | }, 2070 | 2071 | explorerActionsStyle: { 2072 | margin: '4px -8px -8px', 2073 | paddingLeft: '8px', 2074 | bottom: '0px', 2075 | width: '100%', 2076 | textAlign: 'center', 2077 | background: 'none', 2078 | borderTop: 'none', 2079 | borderBottom: 'none', 2080 | }, 2081 | }; 2082 | 2083 | type RootViewProps = {| 2084 | schema: GraphQLSchema, 2085 | isLast: boolean, 2086 | fields: ?GraphQLFieldMap, 2087 | operationType: OperationType, 2088 | name: ?string, 2089 | onTypeName: ?string, 2090 | definition: FragmentDefinitionNode | OperationDefinitionNode, 2091 | onEdit: ( 2092 | operationDef: ?OperationDefinitionNode | ?FragmentDefinitionNode, 2093 | commit: boolean, 2094 | ) => DocumentNode, 2095 | onCommit: (document: DocumentNode) => void, 2096 | onOperationRename: (query: string) => void, 2097 | onOperationDestroy: () => void, 2098 | onOperationClone: () => void, 2099 | onRunOperation: (name: ?string) => void, 2100 | onMount: (rootViewElId: string) => void, 2101 | getDefaultFieldNames: (type: GraphQLObjectType) => Array, 2102 | getDefaultScalarArgValue: GetDefaultScalarArgValue, 2103 | makeDefaultArg: ?MakeDefaultArg, 2104 | styleConfig: StyleConfig, 2105 | availableFragments: AvailableFragments, 2106 | |}; 2107 | 2108 | class RootView extends React.PureComponent< 2109 | RootViewProps, 2110 | {|newOperationType: NewOperationType, displayTitleActions: boolean|}, 2111 | > { 2112 | state = {newOperationType: 'query', displayTitleActions: false}; 2113 | _previousOperationDef: ?OperationDefinitionNode | ?FragmentDefinitionNode; 2114 | 2115 | _modifySelections = ( 2116 | selections: Selections, 2117 | options: ?{commit: boolean}, 2118 | ): DocumentNode => { 2119 | let operationDef: FragmentDefinitionNode | OperationDefinitionNode = this 2120 | .props.definition; 2121 | 2122 | if ( 2123 | operationDef.selectionSet.selections.length === 0 && 2124 | this._previousOperationDef 2125 | ) { 2126 | operationDef = this._previousOperationDef; 2127 | } 2128 | 2129 | let newOperationDef: ?OperationDefinitionNode | ?FragmentDefinitionNode; 2130 | 2131 | if (operationDef.kind === 'FragmentDefinition') { 2132 | newOperationDef = { 2133 | ...operationDef, 2134 | selectionSet: { 2135 | ...operationDef.selectionSet, 2136 | selections, 2137 | }, 2138 | }; 2139 | } else if (operationDef.kind === 'OperationDefinition') { 2140 | let cleanedSelections = selections.filter(selection => { 2141 | return !( 2142 | selection.kind === 'Field' && selection.name.value === '__typename' 2143 | ); 2144 | }); 2145 | 2146 | if (cleanedSelections.length === 0) { 2147 | cleanedSelections = [ 2148 | { 2149 | kind: 'Field', 2150 | name: { 2151 | kind: 'Name', 2152 | value: '__typename ## Placeholder value', 2153 | }, 2154 | }, 2155 | ]; 2156 | } 2157 | 2158 | newOperationDef = { 2159 | ...operationDef, 2160 | selectionSet: { 2161 | ...operationDef.selectionSet, 2162 | selections: cleanedSelections, 2163 | }, 2164 | }; 2165 | } 2166 | 2167 | return this.props.onEdit(newOperationDef, options); 2168 | }; 2169 | 2170 | _onOperationRename = event => 2171 | this.props.onOperationRename(event.target.value); 2172 | 2173 | _handlePotentialRun = event => { 2174 | if (isRunShortcut(event) && canRunOperation(this.props.definition.kind)) { 2175 | this.props.onRunOperation(this.props.name); 2176 | } 2177 | }; 2178 | 2179 | _rootViewElId = () => { 2180 | const {operationType, name} = this.props; 2181 | const rootViewElId = `${operationType}-${name || 'unknown'}`; 2182 | return rootViewElId; 2183 | }; 2184 | 2185 | componentDidMount() { 2186 | const rootViewElId = this._rootViewElId(); 2187 | 2188 | this.props.onMount(rootViewElId); 2189 | } 2190 | 2191 | render() { 2192 | const { 2193 | operationType, 2194 | definition, 2195 | schema, 2196 | getDefaultFieldNames, 2197 | styleConfig, 2198 | } = this.props; 2199 | const rootViewElId = this._rootViewElId(); 2200 | 2201 | const fields = this.props.fields || {}; 2202 | const operationDef = definition; 2203 | const selections = operationDef.selectionSet.selections; 2204 | 2205 | const operationDisplayName = 2206 | this.props.name || `${capitalize(operationType)} Name`; 2207 | 2208 | return ( 2209 |
2219 |
this.setState({displayTitleActions: true})} 2223 | onMouseLeave={() => this.setState({displayTitleActions: false})}> 2224 | {operationType}{' '} 2225 | 2226 | 2240 | 2241 | {!!this.props.onTypeName ? ( 2242 | 2243 |
2244 | {`on ${this.props.onTypeName}`} 2245 |
2246 | ) : ( 2247 | '' 2248 | )} 2249 | {!!this.state.displayTitleActions ? ( 2250 | 2251 | 2260 | 2269 | 2270 | ) : ( 2271 | '' 2272 | )} 2273 |
2274 | 2275 | {Object.keys(fields) 2276 | .sort() 2277 | .map((fieldName: string) => ( 2278 | 2293 | ))} 2294 |
2295 | ); 2296 | } 2297 | } 2298 | 2299 | function Attribution() { 2300 | return ( 2301 |
2312 |
2319 | GraphiQL Explorer by OneGraph 2320 |
2321 |
2322 | Contribute on{' '} 2323 | GitHub 2324 |
2325 |
2326 | ); 2327 | } 2328 | 2329 | class Explorer extends React.PureComponent { 2330 | static defaultProps = { 2331 | getDefaultFieldNames: defaultGetDefaultFieldNames, 2332 | getDefaultScalarArgValue: defaultGetDefaultScalarArgValue, 2333 | }; 2334 | 2335 | state = { 2336 | newOperationType: 'query', 2337 | operation: null, 2338 | operationToScrollTo: null, 2339 | }; 2340 | 2341 | _ref: ?any; 2342 | _resetScroll = () => { 2343 | const container = this._ref; 2344 | if (container) { 2345 | container.scrollLeft = 0; 2346 | } 2347 | }; 2348 | componentDidMount() { 2349 | this._resetScroll(); 2350 | } 2351 | 2352 | _onEdit = (query: string): void => this.props.onEdit(query); 2353 | 2354 | _setAddOperationType = (value: NewOperationType) => { 2355 | this.setState({newOperationType: value}); 2356 | }; 2357 | 2358 | _handleRootViewMount = (rootViewElId: string) => { 2359 | if ( 2360 | !!this.state.operationToScrollTo && 2361 | this.state.operationToScrollTo === rootViewElId 2362 | ) { 2363 | var selector = `.graphiql-explorer-root #${rootViewElId}`; 2364 | 2365 | var el = document.querySelector(selector); 2366 | el && el.scrollIntoView(); 2367 | } 2368 | }; 2369 | 2370 | render() { 2371 | const {schema, query, makeDefaultArg} = this.props; 2372 | 2373 | if (!schema) { 2374 | return ( 2375 |
2376 | No Schema Available 2377 |
2378 | ); 2379 | } 2380 | const styleConfig = { 2381 | colors: this.props.colors || defaultColors, 2382 | checkboxChecked: this.props.checkboxChecked || defaultCheckboxChecked, 2383 | checkboxUnchecked: 2384 | this.props.checkboxUnchecked || defaultCheckboxUnchecked, 2385 | arrowClosed: this.props.arrowClosed || defaultArrowClosed, 2386 | arrowOpen: this.props.arrowOpen || defaultArrowOpen, 2387 | styles: this.props.styles 2388 | ? { 2389 | ...defaultStyles, 2390 | ...this.props.styles, 2391 | } 2392 | : defaultStyles, 2393 | }; 2394 | const queryType = schema.getQueryType(); 2395 | const mutationType = schema.getMutationType(); 2396 | const subscriptionType = schema.getSubscriptionType(); 2397 | if (!queryType && !mutationType && !subscriptionType) { 2398 | return
Missing query type
; 2399 | } 2400 | const queryFields = queryType && queryType.getFields(); 2401 | const mutationFields = mutationType && mutationType.getFields(); 2402 | const subscriptionFields = subscriptionType && subscriptionType.getFields(); 2403 | 2404 | const parsedQuery: DocumentNode = memoizeParseQuery(query); 2405 | const getDefaultFieldNames = 2406 | this.props.getDefaultFieldNames || defaultGetDefaultFieldNames; 2407 | const getDefaultScalarArgValue = 2408 | this.props.getDefaultScalarArgValue || defaultGetDefaultScalarArgValue; 2409 | 2410 | const definitions = parsedQuery.definitions; 2411 | 2412 | const _relevantOperations = definitions 2413 | .map(definition => { 2414 | if (definition.kind === 'FragmentDefinition') { 2415 | return definition; 2416 | } else if (definition.kind === 'OperationDefinition') { 2417 | return definition; 2418 | } else { 2419 | return null; 2420 | } 2421 | }) 2422 | .filter(Boolean); 2423 | 2424 | const relevantOperations = 2425 | // If we don't have any relevant definitions from the parsed document, 2426 | // then at least show an expanded Query selection 2427 | _relevantOperations.length === 0 2428 | ? DEFAULT_DOCUMENT.definitions 2429 | : _relevantOperations; 2430 | 2431 | const renameOperation = (targetOperation, name) => { 2432 | const newName = 2433 | name == null || name === '' 2434 | ? null 2435 | : {kind: 'Name', value: name, loc: undefined}; 2436 | const newOperation = {...targetOperation, name: newName}; 2437 | 2438 | const existingDefs = parsedQuery.definitions; 2439 | 2440 | const newDefinitions = existingDefs.map(existingOperation => { 2441 | if (targetOperation === existingOperation) { 2442 | return newOperation; 2443 | } else { 2444 | return existingOperation; 2445 | } 2446 | }); 2447 | 2448 | return { 2449 | ...parsedQuery, 2450 | definitions: newDefinitions, 2451 | }; 2452 | }; 2453 | 2454 | const cloneOperation = ( 2455 | targetOperation: OperationDefinitionNode | FragmentDefinitionNode, 2456 | ) => { 2457 | let kind; 2458 | if (targetOperation.kind === 'FragmentDefinition') { 2459 | kind = 'fragment'; 2460 | } else { 2461 | kind = targetOperation.operation; 2462 | } 2463 | 2464 | const newOperationName = 2465 | ((targetOperation.name && targetOperation.name.value) || '') + 'Copy'; 2466 | 2467 | const newName = { 2468 | kind: 'Name', 2469 | value: newOperationName, 2470 | loc: undefined, 2471 | }; 2472 | 2473 | const newOperation = {...targetOperation, name: newName}; 2474 | 2475 | const existingDefs = parsedQuery.definitions; 2476 | 2477 | const newDefinitions = [...existingDefs, newOperation]; 2478 | 2479 | this.setState({operationToScrollTo: `${kind}-${newOperationName}`}); 2480 | 2481 | return { 2482 | ...parsedQuery, 2483 | definitions: newDefinitions, 2484 | }; 2485 | }; 2486 | 2487 | const destroyOperation = targetOperation => { 2488 | const existingDefs = parsedQuery.definitions; 2489 | 2490 | const newDefinitions = existingDefs.filter(existingOperation => { 2491 | if (targetOperation === existingOperation) { 2492 | return false; 2493 | } else { 2494 | return true; 2495 | } 2496 | }); 2497 | 2498 | return { 2499 | ...parsedQuery, 2500 | definitions: newDefinitions, 2501 | }; 2502 | }; 2503 | 2504 | const addOperation = (kind: NewOperationType) => { 2505 | const existingDefs = parsedQuery.definitions; 2506 | 2507 | const viewingDefaultOperation = 2508 | parsedQuery.definitions.length === 1 && 2509 | parsedQuery.definitions[0] === DEFAULT_DOCUMENT.definitions[0]; 2510 | 2511 | const MySiblingDefs = viewingDefaultOperation 2512 | ? [] 2513 | : existingDefs.filter(def => { 2514 | if (def.kind === 'OperationDefinition') { 2515 | return def.operation === kind; 2516 | } else { 2517 | // Don't support adding fragments from explorer 2518 | return false; 2519 | } 2520 | }); 2521 | 2522 | const newOperationName = `My${capitalize(kind)}${ 2523 | MySiblingDefs.length === 0 ? '' : MySiblingDefs.length + 1 2524 | }`; 2525 | 2526 | // Add this as the default field as it guarantees a valid selectionSet 2527 | const firstFieldName = '__typename # Placeholder value'; 2528 | 2529 | const selectionSet = { 2530 | kind: 'SelectionSet', 2531 | selections: [ 2532 | { 2533 | kind: 'Field', 2534 | name: { 2535 | kind: 'Name', 2536 | value: firstFieldName, 2537 | loc: null, 2538 | }, 2539 | arguments: [], 2540 | directives: [], 2541 | selectionSet: null, 2542 | loc: null, 2543 | }, 2544 | ], 2545 | loc: null, 2546 | }; 2547 | 2548 | const newDefinition = { 2549 | kind: 'OperationDefinition', 2550 | operation: kind, 2551 | name: {kind: 'Name', value: newOperationName}, 2552 | variableDefinitions: [], 2553 | directives: [], 2554 | selectionSet: selectionSet, 2555 | loc: null, 2556 | }; 2557 | 2558 | const newDefinitions = 2559 | // If we only have our default operation in the document right now, then 2560 | // just replace it with our new definition 2561 | viewingDefaultOperation 2562 | ? [newDefinition] 2563 | : [...parsedQuery.definitions, newDefinition]; 2564 | 2565 | const newOperationDef = { 2566 | ...parsedQuery, 2567 | definitions: newDefinitions, 2568 | }; 2569 | 2570 | this.setState({operationToScrollTo: `${kind}-${newOperationName}`}); 2571 | 2572 | this.props.onEdit(print(newOperationDef)); 2573 | }; 2574 | 2575 | const actionsOptions = [ 2576 | !!queryFields ? ( 2577 | 2585 | ) : null, 2586 | !!mutationFields ? ( 2587 | 2595 | ) : null, 2596 | !!subscriptionFields ? ( 2597 | 2605 | ) : null, 2606 | ].filter(Boolean); 2607 | 2608 | const actionsEl = 2609 | actionsOptions.length === 0 || this.props.hideActions ? null : ( 2610 |
2616 |
event.preventDefault()}> 2626 | 2632 | Add new{' '} 2633 | 2634 | 2640 | 2655 |
2656 |
2657 | ); 2658 | 2659 | const externalFragments = 2660 | this.props.externalFragments && 2661 | this.props.externalFragments.reduce((acc, fragment) => { 2662 | if (fragment.kind === 'FragmentDefinition') { 2663 | const fragmentTypeName = fragment.typeCondition.name.value; 2664 | const existingFragmentsForType = acc[fragmentTypeName] || []; 2665 | const newFragmentsForType = [ 2666 | ...existingFragmentsForType, 2667 | fragment, 2668 | ].sort((a, b) => a.name.value.localeCompare(b.name.value)); 2669 | return { 2670 | ...acc, 2671 | [fragmentTypeName]: newFragmentsForType, 2672 | }; 2673 | } 2674 | 2675 | return acc; 2676 | }, {}); 2677 | 2678 | const documentFragments: AvailableFragments = relevantOperations.reduce( 2679 | (acc, operation) => { 2680 | if (operation.kind === 'FragmentDefinition') { 2681 | const fragmentTypeName = operation.typeCondition.name.value; 2682 | const existingFragmentsForType = acc[fragmentTypeName] || []; 2683 | const newFragmentsForType = [ 2684 | ...existingFragmentsForType, 2685 | operation, 2686 | ].sort((a, b) => a.name.value.localeCompare(b.name.value)); 2687 | return { 2688 | ...acc, 2689 | [fragmentTypeName]: newFragmentsForType, 2690 | }; 2691 | } 2692 | 2693 | return acc; 2694 | }, 2695 | {}, 2696 | ); 2697 | 2698 | const availableFragments = {...documentFragments, ...externalFragments}; 2699 | 2700 | const attribution = this.props.showAttribution ? : null; 2701 | 2702 | return ( 2703 |
{ 2705 | this._ref = ref; 2706 | }} 2707 | style={{ 2708 | fontSize: 12, 2709 | textOverflow: 'ellipsis', 2710 | whiteSpace: 'nowrap', 2711 | margin: 0, 2712 | padding: 8, 2713 | fontFamily: 2714 | 'Consolas, Inconsolata, "Droid Sans Mono", Monaco, monospace', 2715 | display: 'flex', 2716 | flexDirection: 'column', 2717 | height: '100%', 2718 | }} 2719 | className="graphiql-explorer-root"> 2720 |
2725 | {relevantOperations.map( 2726 | ( 2727 | operation: OperationDefinitionNode | FragmentDefinitionNode, 2728 | index, 2729 | ) => { 2730 | const operationName = 2731 | operation && operation.name && operation.name.value; 2732 | 2733 | const operationType = 2734 | operation.kind === 'FragmentDefinition' 2735 | ? 'fragment' 2736 | : (operation && operation.operation) || 'query'; 2737 | 2738 | const onOperationRename = newName => { 2739 | const newOperationDef = renameOperation(operation, newName); 2740 | this.props.onEdit(print(newOperationDef)); 2741 | }; 2742 | 2743 | const onOperationClone = () => { 2744 | const newOperationDef = cloneOperation(operation); 2745 | this.props.onEdit(print(newOperationDef)); 2746 | }; 2747 | 2748 | const onOperationDestroy = () => { 2749 | const newOperationDef = destroyOperation(operation); 2750 | this.props.onEdit(print(newOperationDef)); 2751 | }; 2752 | 2753 | const fragmentType = 2754 | operation.kind === 'FragmentDefinition' && 2755 | operation.typeCondition.kind === 'NamedType' && 2756 | schema.getType(operation.typeCondition.name.value); 2757 | 2758 | const fragmentFields = 2759 | fragmentType instanceof GraphQLObjectType 2760 | ? fragmentType.getFields() 2761 | : null; 2762 | 2763 | const fields = 2764 | operationType === 'query' 2765 | ? queryFields 2766 | : operationType === 'mutation' 2767 | ? mutationFields 2768 | : operationType === 'subscription' 2769 | ? subscriptionFields 2770 | : operation.kind === 'FragmentDefinition' 2771 | ? fragmentFields 2772 | : null; 2773 | 2774 | const fragmentTypeName = 2775 | operation.kind === 'FragmentDefinition' 2776 | ? operation.typeCondition.name.value 2777 | : null; 2778 | 2779 | const onCommit = (parsedDocument: DocumentNode) => { 2780 | const textualNewDocument = print(parsedDocument); 2781 | 2782 | this.props.onEdit(textualNewDocument); 2783 | }; 2784 | 2785 | return ( 2786 | { 2803 | let commit; 2804 | if ( 2805 | typeof options === 'object' && 2806 | typeof options.commit !== 'undefined' 2807 | ) { 2808 | commit = options.commit; 2809 | } else { 2810 | commit = true; 2811 | } 2812 | 2813 | if (!!newDefinition) { 2814 | const newQuery: DocumentNode = { 2815 | ...parsedQuery, 2816 | definitions: parsedQuery.definitions.map( 2817 | existingDefinition => 2818 | existingDefinition === operation 2819 | ? newDefinition 2820 | : existingDefinition, 2821 | ), 2822 | }; 2823 | 2824 | if (commit) { 2825 | onCommit(newQuery); 2826 | return newQuery; 2827 | } else { 2828 | return newQuery; 2829 | } 2830 | } else { 2831 | return parsedQuery; 2832 | } 2833 | }} 2834 | schema={schema} 2835 | getDefaultFieldNames={getDefaultFieldNames} 2836 | getDefaultScalarArgValue={getDefaultScalarArgValue} 2837 | makeDefaultArg={makeDefaultArg} 2838 | onRunOperation={() => { 2839 | if (!!this.props.onRunOperation) { 2840 | this.props.onRunOperation(operationName); 2841 | } 2842 | }} 2843 | styleConfig={styleConfig} 2844 | availableFragments={availableFragments} 2845 | /> 2846 | ); 2847 | }, 2848 | )} 2849 | {attribution} 2850 |
2851 | 2852 | {actionsEl} 2853 |
2854 | ); 2855 | } 2856 | } 2857 | 2858 | class ErrorBoundary extends React.Component< 2859 | *, 2860 | {hasError: boolean, error: any, errorInfo: any}, 2861 | > { 2862 | state = {hasError: false, error: null, errorInfo: null}; 2863 | 2864 | componentDidCatch(error, errorInfo) { 2865 | this.setState({hasError: true, error: error, errorInfo: errorInfo}); 2866 | console.error('Error in component', error, errorInfo); 2867 | } 2868 | 2869 | render() { 2870 | if (this.state.hasError) { 2871 | return ( 2872 |
2873 |
Something went wrong
2874 |
2875 | {this.state.error ? this.state.error.toString() : null} 2876 |
2877 | {this.state.errorInfo ? this.state.errorInfo.componentStack : null} 2878 |
2879 |
2880 | ); 2881 | } 2882 | return this.props.children; 2883 | } 2884 | } 2885 | 2886 | class ExplorerWrapper extends React.PureComponent { 2887 | static defaultValue = defaultValue; 2888 | static defaultProps = { 2889 | width: 320, 2890 | title: 'Explorer', 2891 | }; 2892 | 2893 | render() { 2894 | return ( 2895 |
2906 |
2907 |
{this.props.title}
2908 |
2909 |
2912 | {'\u2715'} 2913 |
2914 |
2915 |
2916 |
2924 | 2925 | 2926 | 2927 |
2928 |
2929 | ); 2930 | } 2931 | } 2932 | 2933 | export default ExplorerWrapper; 2934 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Explorer from './Explorer'; 2 | 3 | export {Explorer}; 4 | export default Explorer; 5 | --------------------------------------------------------------------------------