diff --git a/package.json b/package.json
index 06f7b2fb049..ecc6bd092e8 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"npm": "please_use_yarn_instead"
},
"scripts": {
- "build": "yarn build:clean && yarn build:cm6-graphql && yarn build:packages && yarn build:graphiql-react && yarn build:graphiql-plugin-explorer && yarn build:graphiql-plugin-code-exporter && yarn build:graphiql",
+ "build": "yarn build:clean && yarn build:cm6-graphql && yarn build:packages && yarn build:graphiql-react && yarn build:graphiql-plugin-explorer && yarn build:graphiql-plugin-code-exporter && yarn build:graphiql-plugin-batch-request && yarn build:graphiql",
"build-bundles": "yarn prebuild-bundles && wsrun -p -m -s build-bundles",
"build-bundles-clean": "rimraf '{packages,examples,plugins}/**/{bundle,cdn,webpack}' && yarn workspace graphiql run build-bundles-clean",
"build-clean": "wsrun -m build-clean ",
@@ -40,6 +40,7 @@
"build:graphiql": "yarn tsc resources/tsconfig.graphiql.json",
"build:graphiql-plugin-explorer": "yarn workspace @graphiql/plugin-explorer run build",
"build:graphiql-plugin-code-exporter": "yarn workspace @graphiql/plugin-code-exporter run build",
+ "build:graphiql-plugin-batch-request": "yarn workspace @graphiql/plugin-batch-request run build",
"build:graphiql-react": "yarn workspace @graphiql/react run build",
"build:packages": "yarn tsc",
"build:watch": "yarn tsc --watch",
diff --git a/packages/graphiql-plugin-batch-request/.eslintrc b/packages/graphiql-plugin-batch-request/.eslintrc
new file mode 100644
index 00000000000..98f785433f6
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "extends": ["plugin:react/jsx-runtime"]
+}
diff --git a/packages/graphiql-plugin-batch-request/.gitignore b/packages/graphiql-plugin-batch-request/.gitignore
new file mode 100644
index 00000000000..81275974cf0
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/.gitignore
@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+types
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/packages/graphiql-plugin-batch-request/CHANGELOG.md b/packages/graphiql-plugin-batch-request/CHANGELOG.md
new file mode 100644
index 00000000000..f28cf288d54
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/CHANGELOG.md
@@ -0,0 +1 @@
+# @graphiql/plugin-batch-request
\ No newline at end of file
diff --git a/packages/graphiql-plugin-batch-request/README.md b/packages/graphiql-plugin-batch-request/README.md
new file mode 100644
index 00000000000..f86dc41b14e
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/README.md
@@ -0,0 +1,132 @@
+# GraphiQL Batch Request Plugin
+
+This package provides a plugin that allows sending a batch request to a GraphQL Server and thence into the GraphiQI UI.
+
+The plugin scope is for sending multiple GraphQL using 2 main batching strategy:
+1. Single operation.
+2. Array of operations (GraphQL server must be configured to allow arrays).
+
+### Single operation
+Combine multiple operations ad execute them as one.
+
+For example, given the following GraphQL operations:
+
+```graphql
+query Query1($arg: String) {
+ field1
+ field2(input: $arg)
+}
+
+query Query2($arg: String) {
+ field2(input: $arg)
+ alias: field3
+}
+```
+
+These can be merged into one operation:
+
+```graphql
+query ($_0_arg: String, $_1_arg: String) {
+ _0_field1: field1
+ _0_field2: field2(input: $_0_arg)
+ _1_field2: field3(input: $_1_arg)
+ _1_alias: field3
+}
+```
+
+### Array of operations
+Combine multiple GraphQL Requests and combine them into one GraphQL Request using an array, having the server recognize the request as an array of operations instead of a single one, and handle each operation separately.
+
+For example, given the following GraphQL Requests:
+
+```json
+{
+ "operationName": "Query1",
+ "query": "query Query1($arg: String) { ... }",
+ "variables": {
+ "arg": "foo"
+ }
+}
+
+{
+ "operationName": "Query2",
+ "query": "query Query2($arg: String) { ... }",
+ "variables": {
+ "arg": "foo"
+ }
+}
+
+```
+
+These can be merged into one GraphQL Array Request:
+
+```json
+[
+ {
+ "operationName": "Query1",
+ "query": "query Query1($arg: String) { ... }",
+ "variables": {
+ "arg": "foo"
+ }
+ },
+ {
+ "operationName": "Query2",
+ "query": "query Query2($arg: String) { ... }",
+ "variables": {
+ "arg": "foo"
+ }
+ }
+]
+```
+
+## Install
+
+Use your favorite package manager to install the package:
+
+```sh
+npm i -S @graphiql/plugin-batch-request
+```
+
+The following packages are peer dependencies, so make sure you have them installed as well:
+
+```sh
+npm i -S react react-dom graphql
+```
+
+## Usage
+
+```jsx
+import { useBatchRequestPlugin } from '@graphiql/plugin-batch-request';
+import { createGraphiQLFetcher } from '@graphiql/toolkit';
+import { GraphiQL } from 'graphiql';
+import { useState } from 'react';
+
+import 'graphiql/graphiql.css';
+import '@graphiql/plugin-batch-request/dist/style.css';
+
+const url = 'https://countries.trevorblades.com/graphql';
+
+const fetcher = createGraphiQLFetcher({
+ url
+});
+
+function GraphiQLWithExplorer() {
+ const [query, setQuery] = useState(DEFAULT_QUERY);
+ const batchRequestPlugin = useBatchRequestPlugin({ url });
+ return (
+
+ );
+}
+```
+
+
+### Example
+
+Sending a batch request to the countries GraphQL server:
+
+https://user-images.githubusercontent.com/6611331/216736177-2d8d6153-b246-48ef-8e97-687beea6f9fc.mov
\ No newline at end of file
diff --git a/packages/graphiql-plugin-batch-request/index.html b/packages/graphiql-plugin-batch-request/index.html
new file mode 100644
index 00000000000..6ca3d557708
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/graphiql-plugin-batch-request/jest.config.js b/packages/graphiql-plugin-batch-request/jest.config.js
new file mode 100644
index 00000000000..d5f2ae82f35
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/jest.config.js
@@ -0,0 +1,5 @@
+const base = require('../../jest.config.base')(__dirname);
+
+module.exports = {
+ ...base,
+};
diff --git a/packages/graphiql-plugin-batch-request/package.json b/packages/graphiql-plugin-batch-request/package.json
new file mode 100644
index 00000000000..25dd2e060eb
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@graphiql/plugin-batch-request",
+ "version": "0.0.1",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/graphql/graphiql",
+ "directory": "packages/graphiql-plugin-batch-request"
+ },
+ "main": "dist/graphiql-plugin-batch-request.cjs.js",
+ "module": "dist/graphiql-plugin-batch-request.es.js",
+ "types": "types/index.d.ts",
+ "license": "MIT",
+ "keywords": [
+ "react",
+ "graphql",
+ "graphiql",
+ "plugin",
+ "batch-request"
+ ],
+ "files": [
+ "dist",
+ "src",
+ "types"
+ ],
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc --emitDeclarationOnly && node resources/copy-types.mjs && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@graphiql/react": "^0.15.0",
+ "@fortawesome/fontawesome-free": "6.2.1",
+ "@graphql-tools/utils": "9.1.4",
+ "react-checkbox-tree": "1.8.0"
+ },
+ "peerDependencies": {
+ "graphql": "^15.5.0 || ^16.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^1.3.0",
+ "typescript": "^4.6.3",
+ "vite": "^2.9.13",
+ "graphql": "^16.4.0",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2",
+ "@graphiql/toolkit": "^0.8.0",
+ "graphiql": "^2.2.0"
+ }
+}
diff --git a/packages/graphiql-plugin-batch-request/resources/copy-types.mjs b/packages/graphiql-plugin-batch-request/resources/copy-types.mjs
new file mode 100644
index 00000000000..79f3f5a619d
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/resources/copy-types.mjs
@@ -0,0 +1,11 @@
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const base = path.resolve(path.dirname(__filename), '..');
+
+fs.copyFileSync(
+ path.resolve(base, 'src', 'graphiql-batch-request.d.ts'),
+ path.resolve(base, 'types', 'graphiql-batch-request.d.ts'),
+);
diff --git a/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx b/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx
new file mode 100644
index 00000000000..a1bded8d67d
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/src/batch-request-plugin.tsx
@@ -0,0 +1,242 @@
+import { Button, ButtonGroup, PlayIcon, Spinner, StopIcon, useEditorContext } from '@graphiql/react';
+import { FetcherParamsWithDocument, GraphiQLBatchRequestProps, TabsWithOperations } from 'graphiql-batch-request';
+import { GraphQLError, parse, print } from 'graphql';
+import { Kind } from 'graphql/language';
+import { useState } from 'react';
+import CheckboxTree from 'react-checkbox-tree';
+import { filterDocumentByOperationName } from './filter-document';
+import { mergeRequests } from './merge-requests';
+
+import "@fortawesome/fontawesome-free/css/all.css";
+import 'react-checkbox-tree/lib/react-checkbox-tree.css';
+import './graphiql-batch-request.d.ts';
+import './index.css';
+import { FetcherParams } from '@graphiql/toolkit';
+
+enum BatchStrategy { ALIASES, ARRAY };
+
+export function BatchRequestPlugin({
+ url,
+ useAllOperations = false
+}: GraphiQLBatchRequestProps) {
+ const { tabs, responseEditor } = useEditorContext({ nonNull: true });
+
+ let parsingError = '';
+ let tabsWithOperations: TabsWithOperations = {};
+ try {
+ tabsWithOperations = tabs
+ .filter(tab => tab.query !== '' && tab.query !== null)
+ .map(tab => {
+ const document = parse(tab.query as string);
+ return {
+ id: tab.id,
+ document,
+ operations: document.definitions.filter(
+ definition => definition.kind === Kind.OPERATION_DEFINITION
+ ),
+ variables: tab.variables && tab.variables.trim() !== ''
+ ? JSON.parse(tab.variables) :
+ undefined,
+ headers: tab.headers && tab.headers.trim() !== ''
+ ? JSON.parse(tab.headers) :
+ undefined
+ }
+ })
+ .reduce((acc, tabWithOperations) => {
+ acc[tabWithOperations.id] = tabWithOperations;
+ return acc;
+ }, {} as any);
+ } catch(e: unknown) {
+ if (e instanceof GraphQLError || e instanceof SyntaxError) {
+ parsingError = e.message;
+ }
+ }
+
+ const operationValues: string[] = [];
+ const nodes = Object.values(tabsWithOperations).map(
+ (tabWithOperations, i) => ({
+ value: tabWithOperations.id,
+ label: `Tab ${i + 1}`,
+ children: tabWithOperations.operations.map((operation, j) => {
+ const operationValue = operation.name?.value
+ ? `${tabWithOperations.id}|${operation.name.value}`
+ : `${tabWithOperations.id}|${j}`;
+ operationValues.push(operationValue);
+
+ return {
+ value: operationValue,
+ label: operation.name?.value ?? 'Unnamed operation'
+ }
+ })
+ })
+ );
+
+ const [batchingStrategy, setBatchingStrategy] = useState(BatchStrategy.ARRAY);
+ const [batchResponseLoading, setBatchResponseLoading] = useState(false);
+ const [executeButtonDisabled, setExecuteButtonDisabled] = useState(
+ useAllOperations === false
+ );
+ const [selectedOperations, setSelectedOperations] = useState(
+ useAllOperations ? operationValues : []
+ );
+ const [expandedOperations, setExpandedOperations] = useState(
+ Object.keys(tabsWithOperations)
+ );
+
+
+ if (parsingError !== '') {
+ return (
+ <>
+ Error parsing queries, verify your queries syntax in the tabs:
+ {parsingError}
+ >
+ )
+ }
+
+ const sendBatchRequest = () => {
+ const operations: FetcherParamsWithDocument[] = [];
+ let headers = {};
+ for (const selectedOperation of selectedOperations) {
+ const [tabId, selectedOperationName] = selectedOperation.split('|');
+ const tab = tabsWithOperations[tabId]
+ if (tab) {
+ const selectedOperationDefinition = tab.operations.find(
+ (operation, i) =>
+ operation.name?.value === selectedOperationName ||
+ `${tab.id}|${i}` === selectedOperation
+ )
+ if (selectedOperationDefinition) {
+ headers = {...headers, ...tab.headers};
+ const document = filterDocumentByOperationName(tab.document, selectedOperationDefinition.name?.value);
+ console.log(`filtered document op: ${selectedOperationDefinition.name?.value}\n`, print(document));
+ operations.push({
+ document,
+ operationName: selectedOperationDefinition.name?.value,
+ query: print(document),
+ variables: tab.variables
+ })
+ };
+ }
+ }
+
+ let payload: FetcherParams[] | FetcherParams = [];
+ if (batchingStrategy === BatchStrategy.ARRAY) {
+ payload = operations.map(({query, operationName, variables}) => ({ operationName, query, variables }));
+ } else if (batchingStrategy === BatchStrategy.ALIASES) {
+ const mergedRequests = mergeRequests(
+ operations.map(({document, operationName, variables}) => ({
+ operationName,
+ document,
+ variables
+ }))
+ );
+
+ console.log('merged requests:\n', print(mergedRequests.document));
+
+ payload = {
+ query: print(mergedRequests.document),
+ operationName: mergedRequests.operationName,
+ variables: mergedRequests.variables
+ }
+ }
+
+ setBatchResponseLoading(true);
+
+ window.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ headers: {
+ 'content-type': 'application/json',
+ ...headers
+ }
+ }).then(response => response.json())
+ .then(json => {
+ setBatchResponseLoading(false);
+ responseEditor?.setValue(JSON.stringify(json, null, 2))
+ })
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ A batch GraphQL request requires at least 1 operation.
+
+
0 ? 'block' : 'none'
+ }}>
+ You have selected {selectedOperations.length === 1 ? `${selectedOperations.length} operation.` : `${selectedOperations.length} operations.`}
+
+
+
+
+
Batch strategy
+
+ How to batch operations.
+
+
+
+
+
+
+
+
+
+
,
+ expandOpen:
,
+ parentClose: null,
+ parentOpen: null,
+ leaf: null
+ }}
+ nodes={nodes}
+ checked={selectedOperations}
+ expanded={expandedOperations}
+ onCheck={checked => {
+ setSelectedOperations(checked);
+ setExecuteButtonDisabled(checked.length === 0);
+ }}
+ onExpand={setExpandedOperations}
+ expandOnClick
+ />
+ { batchResponseLoading ?
: null }
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/graphiql-plugin-batch-request/src/filter-document.ts b/packages/graphiql-plugin-batch-request/src/filter-document.ts
new file mode 100644
index 00000000000..6b180e36158
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/src/filter-document.ts
@@ -0,0 +1,70 @@
+import {
+ DocumentNode, FragmentDefinitionNode, Kind, OperationDefinitionNode,
+ SelectionSetNode
+} from 'graphql';
+
+export const filterDocumentByOperationName = (
+ document: DocumentNode,
+ operationName?: string
+): DocumentNode => {
+ let filteredOperation: OperationDefinitionNode | undefined;
+ const fragments: Record = {};
+
+ for (const definition of document.definitions) {
+ if (
+ definition.kind === Kind.OPERATION_DEFINITION &&
+ definition.name?.value === operationName
+ ) {
+ filteredOperation = definition;
+ } else if (definition.kind === Kind.FRAGMENT_DEFINITION) {
+ fragments[definition.name.value] = definition;
+ }
+ }
+
+ const getSelectedFragments = (
+ selectionSet: SelectionSetNode | undefined
+ ): Record => {
+
+ if (!selectionSet) {
+ return {};
+ }
+
+ let selectedFragments: Record = {};
+
+ for(const selection of selectionSet.selections) {
+ if(selection.kind === Kind.FRAGMENT_SPREAD) {
+ const selectedFragment = fragments[selection.name.value];
+ selectedFragments = {
+ ...selectedFragments,
+ [selection.name.value]: selectedFragment,
+ ...getSelectedFragments(selectedFragment.selectionSet)
+ };
+ } else {
+ // technically at this point the only SelectionNode types we are looking for are
+ // FieldNode (Kind.FIELD) and InlineFragmentNode (Kind.INLINE_FRAGMENT)
+ // but leting validation handle that.
+ selectedFragments = {
+ ...selectedFragments,
+ ...getSelectedFragments(selection.selectionSet)
+ };
+ }
+ }
+
+ return selectedFragments;
+ }
+
+ if (filteredOperation) {
+ return {
+ kind: Kind.DOCUMENT,
+ definitions: [
+ ...Object.values(getSelectedFragments(filteredOperation.selectionSet)),
+ filteredOperation
+ ]
+ };
+ }
+
+ return {
+ kind: Kind.DOCUMENT,
+ definitions: []
+ };
+}
\ No newline at end of file
diff --git a/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts b/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts
new file mode 100644
index 00000000000..c8b59805aa1
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/src/graphiql-batch-request.d.ts
@@ -0,0 +1,26 @@
+declare module 'graphiql-batch-request' {
+ import { ComponentType } from 'react';
+ import { DocumentNode, OperationDefinitionNode } from 'graphql/language';
+ import { FetcherParams } from '@graphiql/toolkit';
+
+ type TabWithOperations = {
+ id: string,
+ document: DocumentNode,
+ operations: OperationDefinitionNode[],
+ variables: Record,
+ headers: Record
+ }
+
+ export type TabsWithOperations = Record;
+
+ export type FetcherParamsWithDocument = FetcherParams & { document: DocumentNode, operationName?: string };
+
+ export type GraphiQLBatchRequestProps = {
+ url: string,
+ useAllOperations?: boolean
+ }
+
+ const GraphiQLBatchRequest: ComponentType;
+
+ export default GraphiQLBatchRequest;
+}
diff --git a/packages/graphiql-plugin-batch-request/src/index.css b/packages/graphiql-plugin-batch-request/src/index.css
new file mode 100644
index 00000000000..2968db93552
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/src/index.css
@@ -0,0 +1,67 @@
+.graphiql-batch-request-header {
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+}
+
+.graphiql-batch-request-header-content {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.graphiql-batch-request-title {
+ font-weight: var(--font-weight-medium);
+ font-size: var(--font-size-h2);
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.graphiql-batch-request-send {
+ height: 100%;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.graphiql-batch-request-strategy-section {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ padding: var(--px-2);
+}
+
+.graphiql-batch-request-strategy-title {
+ font-size: var(--font-size-h4);
+ font-weight: var(--font-weight-medium);
+}
+
+.graphiql-batch-request-strategy-caption {
+ color: hsla(var(--color-neutral),var(--alpha-secondary));
+}
+
+.graphiql-batch-request-content {
+ margin: var(--px-16) 0 0;
+}
+
+.graphiql-batch-request-content>* {
+ color: hsla(var(--color-neutral),var(--alpha-secondary));
+ margin-top: var(--px-20);
+}
+
+.graphiql-batch-request-content .rct-node-leaf .rct-title {
+ color: hsl(var(--color-tertiary));
+}
+
+.graphiql-execute-button:disabled {
+ background-color: hsl(var(--color-neutral));
+}
+
+.graphiql-execute-button:disabled:hover {
+ background-color: hsl(var(--color-neutral));
+}
+
+.react-checkbox-tree {
+ overflow-y: auto;
+}
diff --git a/packages/graphiql-plugin-batch-request/src/index.tsx b/packages/graphiql-plugin-batch-request/src/index.tsx
new file mode 100644
index 00000000000..500b23283b1
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/src/index.tsx
@@ -0,0 +1,25 @@
+import {
+ GraphiQLPlugin
+} from '@graphiql/react';
+import { GraphiQLBatchRequestProps } from 'graphiql-batch-request';
+import { BatchRequestPlugin } from './batch-request-plugin';
+import { useMemo, useRef } from 'react';
+
+export function useBatchRequestPlugin(props: GraphiQLBatchRequestProps) {
+ const propsRef = useRef(props);
+ propsRef.current = props;
+ return useMemo(
+ () => ({
+ title: 'Batch Request',
+ icon: () => (
+
+ ),
+ content: () => ,
+ }),
+ [],
+ );
+}
\ No newline at end of file
diff --git a/packages/graphiql-plugin-batch-request/src/merge-requests.ts b/packages/graphiql-plugin-batch-request/src/merge-requests.ts
new file mode 100644
index 00000000000..e365631e3f1
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/src/merge-requests.ts
@@ -0,0 +1,300 @@
+// copied from https://github.com/ardatan/graphql-tools/blob/master/packages/batch-execute/src/prefix.ts
+
+import {
+ visit,
+ Kind,
+ OperationDefinitionNode,
+ DocumentNode,
+ FragmentDefinitionNode,
+ VariableDefinitionNode,
+ SelectionNode,
+ FragmentSpreadNode,
+ VariableNode,
+ InlineFragmentNode,
+ FieldNode,
+} from 'graphql';
+import { ExecutionRequest, getOperationASTFromRequest } from '@graphql-tools/utils';
+
+/**
+ * Merge multiple queries into a single query in such a way that query results
+ * can be split and transformed as if they were obtained by running original queries.
+ *
+ * Merging algorithm involves several transformations:
+ * 1. Replace top-level fragment spreads with inline fragments (... on Query {})
+ * 2. Add unique aliases to all top-level query fields (including those on inline fragments)
+ * 3. Prefix all variable definitions and variable usages
+ * 4. Prefix names (and spreads) of fragments
+ *
+ * i.e transform:
+ * [
+ * `query Foo($id: ID!) { foo, bar(id: $id), ...FooQuery }
+ * fragment FooQuery on Query { baz }`,
+ *
+ * `query Bar($id: ID!) { foo: baz, bar(id: $id), ... on Query { baz } }`
+ * ]
+ * to:
+ * query (
+ * $graphqlTools1_id: ID!
+ * $graphqlTools2_id: ID!
+ * ) {
+ * graphqlTools1_foo: foo,
+ * graphqlTools1_bar: bar(id: $graphqlTools1_id)
+ * ... on Query {
+ * graphqlTools1__baz: baz
+ * }
+ * graphqlTools1__foo: baz
+ * graphqlTools1__bar: bar(id: $graphqlTools1__id)
+ * ... on Query {
+ * graphqlTools1__baz: baz
+ * }
+ * }
+ */
+export function mergeRequests(
+ requests: Array
+): ExecutionRequest {
+ const mergedVariables: Record = Object.create(null);
+ const mergedVariableDefinitions: Array = [];
+ const mergedSelections: Array = [];
+ const mergedFragmentDefinitions: Array = [];
+ let mergedExtensions: Record = Object.create(null);
+
+ for (const index in requests) {
+ const request = requests[index];
+ const prefixedRequest = prefixRequest(`_${index}_`, request);
+
+ for (const def of prefixedRequest.document.definitions) {
+ if (def.kind === Kind.OPERATION_DEFINITION) {
+ mergedSelections.push(...def.selectionSet.selections);
+ if (def.variableDefinitions) {
+ mergedVariableDefinitions.push(...def.variableDefinitions);
+ }
+ }
+ if (def.kind === Kind.FRAGMENT_DEFINITION) {
+ mergedFragmentDefinitions.push(def);
+ }
+ }
+ Object.assign(mergedVariables, prefixedRequest.variables);
+ mergedExtensions = {
+ ...mergedExtensions,
+ ...request.extensions
+ };
+ }
+
+ const firstRequest = requests[0];
+ const operationType = firstRequest.operationType ?? getOperationASTFromRequest(firstRequest).operation;
+ const mergedOperationDefinition: OperationDefinitionNode = {
+ kind: Kind.OPERATION_DEFINITION,
+ operation: operationType,
+ variableDefinitions: mergedVariableDefinitions,
+ selectionSet: {
+ kind: Kind.SELECTION_SET,
+ selections: mergedSelections,
+ },
+ };
+ const operationName = firstRequest.operationName ?? firstRequest.info?.operation?.name?.value;
+ if (operationName) {
+ (mergedOperationDefinition as any).name = {
+ kind: Kind.NAME,
+ value: operationName,
+ };
+ }
+
+ return {
+ document: {
+ kind: Kind.DOCUMENT,
+ definitions: [mergedOperationDefinition, ...mergedFragmentDefinitions],
+ },
+ variables: mergedVariables,
+ extensions: mergedExtensions,
+ context: requests[0].context,
+ info: requests[0].info,
+ operationType,
+ };
+}
+
+function prefixRequest(prefix: string, request: ExecutionRequest): ExecutionRequest {
+ const executionVariables = request.variables ?? {};
+
+ const prefixNode = (node: VariableNode | FragmentDefinitionNode | FragmentSpreadNode) => prefixNodeName(node, prefix);
+
+ let prefixedDocument = aliasTopLevelFields(prefix, request.document);
+
+ const executionVariableNames = Object.keys(executionVariables);
+ const hasFragmentDefinitions = request.document.definitions.some(def => def.kind === Kind.FRAGMENT_DEFINITION);
+ const fragmentSpreadImpl: Record = {};
+
+ if (executionVariableNames.length > 0 || hasFragmentDefinitions) {
+ prefixedDocument = visit(prefixedDocument, {
+ [Kind.VARIABLE]: prefixNode,
+ [Kind.FRAGMENT_DEFINITION]: prefixNode,
+ [Kind.FRAGMENT_SPREAD]: node => {
+ node = prefixNodeName(node, prefix);
+ fragmentSpreadImpl[node.name.value] = true;
+ return node;
+ },
+ }) as DocumentNode;
+ }
+
+ const prefixedVariables: Record = {};
+
+ for (const variableName of executionVariableNames) {
+ prefixedVariables[prefix + variableName] = executionVariables[variableName];
+ }
+
+ if (hasFragmentDefinitions) {
+ prefixedDocument = {
+ ...prefixedDocument,
+ definitions: prefixedDocument.definitions.filter(def => {
+ return !(def.kind === Kind.FRAGMENT_DEFINITION) || fragmentSpreadImpl[def.name.value];
+ }),
+ };
+ }
+
+ return {
+ document: prefixedDocument,
+ variables: prefixedVariables
+ };
+}
+
+/**
+ * Adds prefixed aliases to top-level fields of the query.
+ *
+ * @see aliasFieldsInSelection for implementation details
+ */
+function aliasTopLevelFields(prefix: string, document: DocumentNode): DocumentNode {
+ const transformer = {
+ [Kind.OPERATION_DEFINITION]: (def: OperationDefinitionNode) => {
+ const { selections } = def.selectionSet;
+ return {
+ ...def,
+ selectionSet: {
+ ...def.selectionSet,
+ selections: aliasFieldsInSelection(prefix, selections, document),
+ },
+ };
+ },
+ };
+ return visit(document, transformer, {
+ [Kind.DOCUMENT]: [`definitions`],
+ } as any);
+}
+
+/**
+ * Add aliases to fields of the selection, including top-level fields of inline fragments.
+ * Fragment spreads are converted to inline fragments and their top-level fields are also aliased.
+ *
+ * Note that this method is shallow. It adds aliases only to the top-level fields and doesn't
+ * descend to field sub-selections.
+ *
+ * For example, transforms:
+ * {
+ * foo
+ * ... on Query { foo }
+ * ...FragmentWithBarField
+ * }
+ * To:
+ * {
+ * graphqlTools1_foo: foo
+ * ... on Query { graphqlTools1_foo: foo }
+ * ... on Query { graphqlTools1_bar: bar }
+ * }
+ */
+function aliasFieldsInSelection(
+ prefix: string,
+ selections: ReadonlyArray,
+ document: DocumentNode
+): Array {
+ return selections.map(selection => {
+ switch (selection.kind) {
+ case Kind.INLINE_FRAGMENT:
+ return aliasFieldsInInlineFragment(prefix, selection, document);
+ case Kind.FRAGMENT_SPREAD: {
+ const inlineFragment = inlineFragmentSpread(selection, document);
+ return aliasFieldsInInlineFragment(prefix, inlineFragment, document);
+ }
+ case Kind.FIELD:
+ default:
+ return aliasField(selection, prefix);
+ }
+ });
+}
+
+/**
+ * Add aliases to top-level fields of the inline fragment.
+ * Returns new inline fragment node.
+ *
+ * For Example, transforms:
+ * ... on Query { foo, ... on Query { bar: foo } }
+ * To
+ * ... on Query { graphqlTools1_foo: foo, ... on Query { graphqlTools1_bar: foo } }
+ */
+function aliasFieldsInInlineFragment(
+ prefix: string,
+ fragment: InlineFragmentNode,
+ document: DocumentNode
+): InlineFragmentNode {
+ return {
+ ...fragment,
+ selectionSet: {
+ ...fragment.selectionSet,
+ selections: aliasFieldsInSelection(prefix, fragment.selectionSet.selections, document),
+ },
+ };
+}
+
+/**
+ * Replaces fragment spread with inline fragment
+ *
+ * Example:
+ * query { ...Spread }
+ * fragment Spread on Query { bar }
+ *
+ * Transforms to:
+ * query { ... on Query { bar } }
+ */
+function inlineFragmentSpread(spread: FragmentSpreadNode, document: DocumentNode): InlineFragmentNode {
+ const fragment = document.definitions.find(
+ def => def.kind === Kind.FRAGMENT_DEFINITION && def.name.value === spread.name.value
+ ) as FragmentDefinitionNode;
+ if (!fragment) {
+ throw new Error(`Fragment ${spread.name.value} does not exist`);
+ }
+ const { typeCondition, selectionSet } = fragment;
+ return {
+ kind: Kind.INLINE_FRAGMENT,
+ typeCondition,
+ selectionSet,
+ directives: spread.directives,
+ };
+}
+
+function prefixNodeName(
+ node: T,
+ prefix: string
+): T {
+ return {
+ ...node,
+ name: {
+ ...node.name,
+ value: `${prefix}${node.name.value}`,
+ },
+ };
+}
+
+/**
+ * Returns a new FieldNode with prefixed alias
+ *
+ * Example. Given prefix === "graphqlTools1_" transforms:
+ * { foo } -> { graphqlTools1_foo: foo }
+ * { foo: bar } -> { graphqlTools1_foo: bar }
+ */
+function aliasField(field: FieldNode, aliasPrefix: string): FieldNode {
+ const aliasNode = field.alias ?? field.name;
+ return {
+ ...field,
+ alias: {
+ ...aliasNode,
+ value: `${aliasPrefix}${aliasNode.value}`,
+ },
+ };
+}
\ No newline at end of file
diff --git a/packages/graphiql-plugin-batch-request/src/vite-env.d.ts b/packages/graphiql-plugin-batch-request/src/vite-env.d.ts
new file mode 100644
index 00000000000..11f02fe2a00
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/packages/graphiql-plugin-batch-request/test-plugin.tsx b/packages/graphiql-plugin-batch-request/test-plugin.tsx
new file mode 100644
index 00000000000..2c0c50f3901
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/test-plugin.tsx
@@ -0,0 +1,28 @@
+import { render } from 'react-dom';
+import { createGraphiQLFetcher } from '@graphiql/toolkit';
+import { GraphiQL } from 'graphiql';
+import * as React from 'react';
+import { useBatchRequestPlugin } from './src/index';
+
+// const url = 'https://swapi-graphql.netlify.app/.netlify/functions/index';
+// const url = 'https://api.spacex.land/graphql';
+const url = 'https://countries.trevorblades.com/graphql';
+
+const fetcher = createGraphiQLFetcher({ url });
+
+const App = () => {
+ const [query, setQuery] = React.useState('');
+ const batchRequestPlugin = useBatchRequestPlugin({ url });
+ const defaultEditorToolsVisibility = true;
+ return (
+
+ );
+}
+
+render(, document.getElementById('root'));
\ No newline at end of file
diff --git a/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts b/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts
new file mode 100644
index 00000000000..6c59da11fcc
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/test/filter-document-by-operation-name.test.ts
@@ -0,0 +1,135 @@
+import { filterDocumentByOperationName } from '../src/filter-document';
+import { FragmentDefinitionNode, Kind, OperationDefinitionNode, parse } from 'graphql';
+
+describe('filterDocumentByOperationName', () => {
+ it('should filter document with only 1 operation definition', () => {
+ const document = parse(`
+ fragment ItemFragment on Item { id }
+ query GetItem($id: ID!) {
+ item(id: $id) { ...ItemFragment }
+ }
+ `);
+
+ const filteredDocument = filterDocumentByOperationName(document, 'GetItem');
+ expect(filteredDocument.definitions.length).toEqual(2);
+
+ const operationDefinition = filteredDocument.definitions.find(definition =>
+ definition.kind === Kind.OPERATION_DEFINITION
+ ) as OperationDefinitionNode | undefined;
+ expect(operationDefinition?.name?.value).toEqual('GetItem');
+
+ const fragmentDefinition = filteredDocument.definitions.find(definition =>
+ definition.kind === Kind.FRAGMENT_DEFINITION
+ ) as FragmentDefinitionNode | undefined;
+ expect(fragmentDefinition?.name?.value).toEqual('ItemFragment');
+ });
+
+ it('should filter document with multiple operation definitions', () => {
+ const document = parse(`
+ fragment ItemFragment on Item { id }
+ fragment UserFragment on User { id }
+ fragment ReviewsFragment on Review {
+ id
+ user { ...UserFragment }
+ }
+ query GetItem($id: ID!) {
+ item(id: $id) { ...ItemFragment }
+ }
+ query GetItemsAndReviews {
+ reviews { ...ReviewsFragment }
+ items { ...ItemFragment }
+ }
+ `);
+
+ const filteredDocument = filterDocumentByOperationName(document, 'GetItemsAndReviews');
+ expect(filteredDocument.definitions.length).toEqual(4);
+
+ const operationDefinition = filteredDocument.definitions.find(definition =>
+ definition.kind === Kind.OPERATION_DEFINITION
+ ) as OperationDefinitionNode | undefined;
+ expect(operationDefinition?.name?.value).toEqual('GetItemsAndReviews');
+
+ const fragmentDefinitions = filteredDocument.definitions.filter(definition =>
+ definition.kind === Kind.FRAGMENT_DEFINITION
+ ) as FragmentDefinitionNode[] | undefined;
+ expect(
+ fragmentDefinitions?.map(def => def.name.value)
+ ).toEqual(
+ ['ReviewsFragment', 'UserFragment', 'ItemFragment']
+ );
+ });
+
+ it('should filter document with multiple operations and 1 anonymous operation', () => {
+ const document = parse(`
+ fragment ItemFragment on Item { id }
+ fragment UserFragment on User { id }
+ fragment ReviewsFragment on Review {
+ id
+ user { ...UserFragment }
+ }
+ query GetItem($id: ID!) {
+ item(id: $id) { ...ItemFragment }
+ }
+ query GetItemsAndReviews {
+ reviews { ...ReviewsFragment }
+ items { ...ItemFragment }
+ }
+ {
+ reviews(ids: [1,2,3]) { ...ReviewsFragment }
+ }
+ `);
+
+ const filteredDocument = filterDocumentByOperationName(document);
+ expect(filteredDocument.definitions.length).toEqual(3);
+
+ const operationDefinition = filteredDocument.definitions.find(definition =>
+ definition.kind === Kind.OPERATION_DEFINITION
+ ) as OperationDefinitionNode | undefined;
+ expect(operationDefinition?.name?.value).toBeUndefined();
+
+ const fragmentDefinitions = filteredDocument.definitions.filter(definition =>
+ definition.kind === Kind.FRAGMENT_DEFINITION
+ ) as FragmentDefinitionNode[] | undefined;
+ expect(
+ fragmentDefinitions?.map(def => def.name.value)
+ ).toEqual(
+ ['ReviewsFragment', 'UserFragment']
+ );
+ });
+
+ it('should not filter document when no operation defitinion matches provided operation name', () => {
+ const document = parse(`
+ fragment ItemFragment on Item { id }
+ fragment UserFragment on User { id }
+ fragment ReviewsFragment on Review {
+ id
+ user { ...UserFragment }
+ }
+ query GetItem($id: ID!) {
+ item(id: $id) { ...ItemFragment }
+ }
+ query GetItemsAndReviews {
+ reviews { ...ReviewsFragment }
+ items { ...ItemFragment }
+ }
+ `);
+
+ const filteredDocument = filterDocumentByOperationName(document, 'MyAwesomeQuery');
+ expect(filteredDocument.definitions.length).toEqual(0);
+ });
+
+ it('should not filter document without repeated fagment definitions', () => {
+ const document = parse(`
+ fragment ItemFragment on Item { id }
+ query GetItem {
+ item_1: item(id: 1) { ...ItemFragment }
+ item_2: item(id: 2) { ...ItemFragment }
+ item_3: item(id: 3) { ...ItemFragment }
+ item_4: item(id: 4) { ...ItemFragment }
+ }
+ `);
+
+ const filteredDocument = filterDocumentByOperationName(document, 'GetItem');
+ expect(filteredDocument.definitions.length).toEqual(2);
+ });
+});
diff --git a/packages/graphiql-plugin-batch-request/tsconfig.json b/packages/graphiql-plugin-batch-request/tsconfig.json
new file mode 100644
index 00000000000..872ce46c3eb
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "declaration": true,
+ "declarationDir": "types",
+ "jsx": "react-jsx"
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/packages/graphiql-plugin-batch-request/tsconfig.node.json b/packages/graphiql-plugin-batch-request/tsconfig.node.json
new file mode 100644
index 00000000000..9d31e2aed93
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/tsconfig.node.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Node",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/packages/graphiql-plugin-batch-request/vite.config.ts b/packages/graphiql-plugin-batch-request/vite.config.ts
new file mode 100644
index 00000000000..91ec9e3b367
--- /dev/null
+++ b/packages/graphiql-plugin-batch-request/vite.config.ts
@@ -0,0 +1,31 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ lib: {
+ entry: 'src/index.tsx',
+ fileName: 'graphiql-plugin-batch-request',
+ name: 'GraphiQLPluginBatchRequest',
+ formats: ['cjs', 'es', 'umd'],
+ },
+ rollupOptions: {
+ external: ['@graphiql/react', 'graphql', 'react', 'react-dom'],
+ output: {
+ chunkFileNames: '[name].[format].js',
+ globals: {
+ '@graphiql/react': 'GraphiQL.React',
+ graphql: 'GraphiQL.GraphQL',
+ react: 'React',
+ 'react-dom': 'ReactDOM',
+ },
+ },
+ },
+ commonjsOptions: {
+ esmExternals: true,
+ requireReturnsDefault: 'auto',
+ },
+ },
+});
diff --git a/yarn.lock b/yarn.lock
index c132d889026..36c23aa2a8a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2307,6 +2307,30 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
+"@fortawesome/fontawesome-free@6.2.1":
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.1.tgz#344baf6ff9eaad7a73cff067d8c56bfc11ae5304"
+ integrity sha512-viouXhegu/TjkvYQoiRZK3aax69dGXxgEjpvZW81wIJdxm5Fnvp3VVIP4VHKqX4SvFw6qpmkILkD4RJWAdrt7A==
+
+"@graphiql/react@^0.15.0":
+ version "0.15.0"
+ resolved "https://registry.yarnpkg.com/@graphiql/react/-/react-0.15.0.tgz#b10fc40c5a85e7b5b845907d57bfd8be92753037"
+ integrity sha512-kJqkdf6d4Cck05Wt5yCDZXWfs7HZgcpuoWq/v8nOa698qVaNMM3qdG4CpRsZEexku0DSSJzWWuanxd5x+sRcFg==
+ dependencies:
+ "@graphiql/toolkit" "^0.8.0"
+ "@reach/combobox" "^0.17.0"
+ "@reach/dialog" "^0.17.0"
+ "@reach/listbox" "^0.17.0"
+ "@reach/menu-button" "^0.17.0"
+ "@reach/tooltip" "^0.17.0"
+ "@reach/visually-hidden" "^0.17.0"
+ codemirror "^5.65.3"
+ codemirror-graphql "^2.0.2"
+ copy-to-clipboard "^3.2.0"
+ graphql-language-service "^5.1.0"
+ markdown-it "^12.2.0"
+ set-value "^4.1.0"
+
"@graphql-tools/batch-execute@8.5.1":
version "8.5.1"
resolved "https://registry.yarnpkg.com/@graphql-tools/batch-execute/-/batch-execute-8.5.1.tgz#fa3321d58c64041650be44250b1ebc3aab0ba7a9"
@@ -2475,6 +2499,13 @@
dependencies:
tslib "^2.4.0"
+"@graphql-tools/utils@9.1.4":
+ version "9.1.4"
+ resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-9.1.4.tgz#2c9e0aefc9655dd73247667befe3c850ec014f3f"
+ integrity sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==
+ dependencies:
+ tslib "^2.4.0"
+
"@graphql-tools/wrap@8.5.1":
version "8.5.1"
resolved "https://registry.yarnpkg.com/@graphql-tools/wrap/-/wrap-8.5.1.tgz#d4bd1f89850bb1ce0209f35f66d002080b439495"
@@ -5772,6 +5803,11 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
+classnames@^2.2.5:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
+ integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
+
clean-css@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
@@ -12200,7 +12236,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0:
+lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -12881,7 +12917,7 @@ nanoid@3.3.3:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
-nanoid@^3.3.3:
+nanoid@^3.0.0, nanoid@^3.3.3:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
@@ -14708,7 +14744,7 @@ prop-types@15.7.2, prop-types@^15.6.1, prop-types@^15.6.2:
object-assign "^4.1.1"
react-is "^16.8.1"
-prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@^15.5.8, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -14945,6 +14981,16 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+react-checkbox-tree@1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/react-checkbox-tree/-/react-checkbox-tree-1.8.0.tgz#10dc3e86df25d92559ab6857c7ada4fe285abc03"
+ integrity sha512-ufC4aorihOvjLpvY1beab2hjVLGZbDTFRzw62foG0+th+KX7e/sdmWu/nD1ZS/U5Yr0rWGwedGH5GOtR0IkUXw==
+ dependencies:
+ classnames "^2.2.5"
+ lodash "^4.17.10"
+ nanoid "^3.0.0"
+ prop-types "^15.5.8"
+
react-clientside-effect@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"