diff --git a/.nvmrc b/.nvmrc
index c9d82507f..53217dcfa 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v14.17.0
\ No newline at end of file
+v14.18.0
\ No newline at end of file
diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json
index 74fe4ab59..6db522baf 100644
--- a/packages/bruno-app/package.json
+++ b/packages/bruno-app/package.json
@@ -30,6 +30,7 @@
"idb": "^7.0.0",
"immer": "^9.0.15",
"lodash": "^4.17.21",
+ "markdown-it": "^13.0.1",
"mousetrap": "^1.6.5",
"nanoid": "3.3.4",
"next": "12.3.1",
diff --git a/packages/bruno-graphql-docs/package.json b/packages/bruno-graphql-docs/package.json
index a7be20db4..2da58a083 100644
--- a/packages/bruno-graphql-docs/package.json
+++ b/packages/bruno-graphql-docs/package.json
@@ -10,12 +10,23 @@
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
+ "@types/markdown-it": "^12.2.3",
"@types/react": "^18.0.25",
+ "graphql": "^16.6.0",
+ "markdown-it": "^13.0.1",
+ "postcss": "^8.4.18",
"react": "^17.0.2",
+ "react-dom": "^17.0.2",
"rollup": "3.2.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
+ "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
+ },
+ "peerDependencies": {
+ "graphql": "^16.6.0",
+ "markdown-it": "^13.0.1",
+ "react": "^17.0.2"
}
}
diff --git a/packages/bruno-graphql-docs/rollup.config.js b/packages/bruno-graphql-docs/rollup.config.js
index e02818914..dd2424c5f 100644
--- a/packages/bruno-graphql-docs/rollup.config.js
+++ b/packages/bruno-graphql-docs/rollup.config.js
@@ -1,13 +1,14 @@
-import resolve from "@rollup/plugin-node-resolve";
-import commonjs from "@rollup/plugin-commonjs";
-import typescript from "@rollup/plugin-typescript";
-import dts from "rollup-plugin-dts";
-import { terser } from "rollup-plugin-terser";
-import peerDepsExternal from 'rollup-plugin-peer-deps-external';
+const { nodeResolve } = require("@rollup/plugin-node-resolve");
+const commonjs = require("@rollup/plugin-commonjs");
+const typescript = require("@rollup/plugin-typescript");
+const dts = require("rollup-plugin-dts");
+const postcss = require("rollup-plugin-postcss");
+const { terser } = require("rollup-plugin-terser");
+const peerDepsExternal = require('rollup-plugin-peer-deps-external');
const packageJson = require("./package.json");
-export default [
+module.exports = [
{
input: "src/index.ts",
output: [
@@ -23,12 +24,23 @@ export default [
},
],
plugins: [
+ postcss({
+ minimize: true,
+ extensions: ['.css']
+ }),
peerDepsExternal(),
- resolve(),
+ nodeResolve({
+ extensions: ['.css']
+ }),
commonjs(),
typescript({ tsconfig: "./tsconfig.json" }),
- terser(),
+ terser()
],
- external: ["react", "react-dom", "styled-components"]
+ external: ["react", "react-dom", "index.css"]
+ },
+ {
+ input: "dist/esm/index.d.ts",
+ output: [{ file: "dist/index.d.ts", format: "esm" }],
+ plugins: [dts.default()],
}
];
\ No newline at end of file
diff --git a/packages/bruno-graphql-docs/src/GraphDocs.tsx b/packages/bruno-graphql-docs/src/GraphDocs.tsx
deleted file mode 100644
index d398691a9..000000000
--- a/packages/bruno-graphql-docs/src/GraphDocs.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-
-class GraphDocs extends React.Component {
- render() {
- return "Graphql Docs Explorer"
- }
-}
-
-export default GraphDocs;
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer.tsx
new file mode 100644
index 000000000..fcd76a10e
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer.tsx
@@ -0,0 +1,233 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, { ReactNode } from 'react';
+import { GraphQLSchema, isType, GraphQLNamedType, GraphQLError } from 'graphql';
+import { FieldType } from './DocExplorer/types';
+
+import FieldDoc from './DocExplorer/FieldDoc';
+import SchemaDoc from './DocExplorer/SchemaDoc';
+import SearchBox from './DocExplorer/SearchBox';
+import SearchResults from './DocExplorer/SearchResults';
+import TypeDoc from './DocExplorer/TypeDoc';
+
+type NavStackItem = {
+ name: string;
+ title?: string;
+ search?: string;
+ def?: GraphQLNamedType | FieldType;
+};
+
+const initialNav: NavStackItem = {
+ name: 'Schema',
+ title: 'Documentation Explorer',
+};
+
+type DocExplorerProps = {
+ schema?: GraphQLSchema | null;
+ schemaErrors?: readonly GraphQLError[];
+ children?: ReactNode | null;
+};
+
+type DocExplorerState = {
+ navStack: NavStackItem[];
+};
+
+/**
+ * DocExplorer
+ *
+ * Shows documentations for GraphQL definitions from the schema.
+ *
+ * Props:
+ *
+ * - schema: A required GraphQLSchema instance that provides GraphQL document
+ * definitions.
+ *
+ * Children:
+ *
+ * - Any provided children will be positioned in the right-hand-side of the
+ * top bar. Typically this will be a "close" button for temporary explorer.
+ *
+ */
+export class DocExplorer extends React.Component<
+ DocExplorerProps,
+ DocExplorerState
+> {
+ // handleClickTypeOrField: OnClickTypeFunction | OnClickFieldFunction
+ constructor(props: DocExplorerProps) {
+ super(props);
+
+ this.state = { navStack: [initialNav] };
+ }
+
+ shouldComponentUpdate(
+ nextProps: DocExplorerProps,
+ nextState: DocExplorerState,
+ ) {
+ return (
+ this.props.schema !== nextProps.schema ||
+ this.state.navStack !== nextState.navStack ||
+ this.props.schemaErrors !== nextProps.schemaErrors
+ );
+ }
+
+ render() {
+ const { schema, schemaErrors } = this.props;
+ const navStack = this.state.navStack;
+ const navItem = navStack[navStack.length - 1];
+
+ let content;
+ if (schemaErrors) {
+ content = (
+
{'Error fetching schema'}
+ );
+ } else if (schema === undefined) {
+ // Schema is undefined when it is being loaded via introspection.
+ content = (
+
+ );
+ } else if (!schema) {
+ // Schema is null when it explicitly does not exist, typically due to
+ // an error during introspection.
+ content = {'No Schema Available'}
;
+ } else if (navItem.search) {
+ content = (
+
+ );
+ } else if (navStack.length === 1) {
+ content = (
+
+ );
+ } else if (isType(navItem.def)) {
+ content = (
+
+ );
+ } else {
+ content = (
+
+ );
+ }
+
+ const shouldSearchBoxAppear =
+ navStack.length === 1 ||
+ (isType(navItem.def) && 'getFields' in navItem.def);
+
+ let prevName;
+ if (navStack.length > 1) {
+ prevName = navStack[navStack.length - 2].name;
+ }
+
+ return (
+
+
+
+ {prevName && (
+
+ )}
+
+ {navItem.title || navItem.name}
+
+
{this.props.children}
+
+
+ {shouldSearchBoxAppear && (
+
+ )}
+ {content}
+
+
+
+ );
+ }
+
+ // Public API
+ showDoc(typeOrField: GraphQLNamedType | FieldType) {
+ const navStack = this.state.navStack;
+ const topNav = navStack[navStack.length - 1];
+ if (topNav.def !== typeOrField) {
+ this.setState({
+ navStack: navStack.concat([
+ {
+ name: typeOrField.name,
+ def: typeOrField,
+ },
+ ]),
+ });
+ }
+ }
+
+ // Public API
+ showDocForReference(reference: any) {
+ if (reference && reference.kind === 'Type') {
+ this.showDoc(reference.type);
+ } else if (reference.kind === 'Field') {
+ this.showDoc(reference.field);
+ } else if (reference.kind === 'Argument' && reference.field) {
+ this.showDoc(reference.field);
+ } else if (reference.kind === 'EnumValue' && reference.type) {
+ this.showDoc(reference.type);
+ }
+ }
+
+ // Public API
+ showSearch(search: string) {
+ const navStack = this.state.navStack.slice();
+ const topNav = navStack[navStack.length - 1];
+ navStack[navStack.length - 1] = { ...topNav, search };
+ this.setState({ navStack });
+ }
+
+ reset() {
+ this.setState({ navStack: [initialNav] });
+ }
+
+ handleNavBackClick = () => {
+ if (this.state.navStack.length > 1) {
+ this.setState({ navStack: this.state.navStack.slice(0, -1) });
+ }
+ };
+
+ handleClickType = (type: GraphQLNamedType) => {
+ this.showDoc(type);
+ };
+
+ handleClickField = (field: FieldType) => {
+ this.showDoc(field);
+ };
+
+ handleSearch = (value: string) => {
+ this.showSearch(value);
+ };
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/Argument.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/Argument.tsx
new file mode 100644
index 000000000..1ea1b83ea
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/Argument.tsx
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import { GraphQLArgument } from 'graphql';
+import TypeLink from './TypeLink';
+import DefaultValue from './DefaultValue';
+import { OnClickTypeFunction } from './types';
+
+type ArgumentProps = {
+ arg: GraphQLArgument;
+ onClickType: OnClickTypeFunction;
+ showDefaultValue?: boolean;
+};
+
+export default function Argument({
+ arg,
+ onClickType,
+ showDefaultValue,
+}: ArgumentProps) {
+ return (
+
+ {arg.name}
+ {': '}
+
+ {showDefaultValue !== false && }
+
+ );
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/DefaultValue.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/DefaultValue.tsx
new file mode 100644
index 000000000..574dd97b6
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/DefaultValue.tsx
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import { astFromValue, print, ValueNode } from 'graphql';
+import { FieldType } from './types';
+
+const printDefault = (ast?: ValueNode | null): string => {
+ if (!ast) {
+ return '';
+ }
+ return print(ast);
+};
+
+type DefaultValueProps = {
+ field: FieldType;
+};
+
+export default function DefaultValue({ field }: DefaultValueProps) {
+ // field.defaultValue could be null or false, so be careful here!
+ if ('defaultValue' in field && field.defaultValue !== undefined) {
+ return (
+
+ {' = '}
+
+ {printDefault(astFromValue(field.defaultValue, field.type))}
+
+
+ );
+ }
+
+ return null;
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/Directive.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/Directive.tsx
new file mode 100644
index 000000000..ab6ed7ce5
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/Directive.tsx
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import { DirectiveNode } from 'graphql';
+
+type DirectiveProps = {
+ directive: DirectiveNode;
+};
+
+export default function Directive({ directive }: DirectiveProps) {
+ return (
+
+ {'@'}
+ {directive.name.value}
+
+ );
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/FieldDoc.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/FieldDoc.tsx
new file mode 100644
index 000000000..c77892f07
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/FieldDoc.tsx
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import Argument from './Argument';
+import Directive from './Directive';
+import MarkdownContent from './MarkdownContent';
+import TypeLink from './TypeLink';
+import { GraphQLArgument, DirectiveNode } from 'graphql';
+import { OnClickTypeFunction, FieldType } from './types';
+
+type FieldDocProps = {
+ field?: FieldType;
+ onClickType: OnClickTypeFunction;
+};
+
+export default function FieldDoc({ field, onClickType }: FieldDocProps) {
+ const [showDeprecated, handleShowDeprecated] = React.useState(false);
+ let argsDef;
+ let deprecatedArgsDef;
+ if (field && 'args' in field && field.args.length > 0) {
+ argsDef = (
+
+
{'arguments'}
+ {field.args
+ .filter(arg => !arg.deprecationReason)
+ .map((arg: GraphQLArgument) => (
+
+
+
+ {arg && 'deprecationReason' in arg && (
+
+ )}
+
+ ))}
+
+ );
+ const deprecatedArgs = field.args.filter(arg =>
+ Boolean(arg.deprecationReason),
+ );
+ if (deprecatedArgs.length > 0) {
+ deprecatedArgsDef = (
+
+
{'deprecated arguments'}
+ {!showDeprecated ? (
+
+ ) : (
+ deprecatedArgs.map((arg, i) => (
+
+
+
+ {arg && 'deprecationReason' in arg && (
+
+ )}
+
+ ))
+ )}
+
+ );
+ }
+ }
+
+ let directivesDef;
+ if (
+ field &&
+ field.astNode &&
+ field.astNode.directives &&
+ field.astNode.directives.length > 0
+ ) {
+ directivesDef = (
+
+
{'directives'}
+ {field.astNode.directives.map((directive: DirectiveNode) => (
+
+ ))}
+
+ );
+ }
+
+ return (
+
+
+ {field && 'deprecationReason' in field && (
+
+ )}
+
+ {argsDef}
+ {directivesDef}
+ {deprecatedArgsDef}
+
+ );
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/MarkdownContent.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/MarkdownContent.tsx
new file mode 100644
index 000000000..690d2669c
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/MarkdownContent.tsx
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import MD from 'markdown-it';
+
+type Maybe = T | null | undefined;
+
+const md = new MD({
+ // render urls as links, à la github-flavored markdown
+ linkify: true,
+});
+
+type MarkdownContentProps = {
+ markdown?: Maybe;
+ className?: string;
+};
+
+export default function MarkdownContent({
+ markdown,
+ className,
+}: MarkdownContentProps) {
+ if (!markdown) {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/SchemaDoc.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/SchemaDoc.tsx
new file mode 100644
index 000000000..0a873973c
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/SchemaDoc.tsx
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import TypeLink from './TypeLink';
+import MarkdownContent from './MarkdownContent';
+import { GraphQLSchema } from 'graphql';
+import { OnClickTypeFunction } from './types';
+
+type SchemaDocProps = {
+ schema: GraphQLSchema;
+ onClickType: OnClickTypeFunction;
+};
+
+// Render the top level Schema
+export default function SchemaDoc({ schema, onClickType }: SchemaDocProps) {
+ const queryType = schema.getQueryType();
+ const mutationType = schema.getMutationType && schema.getMutationType();
+ const subscriptionType =
+ schema.getSubscriptionType && schema.getSubscriptionType();
+
+ return (
+
+
+
+
{'root types'}
+
+ {'query'}
+ {': '}
+
+
+ {mutationType && (
+
+ {'mutation'}
+ {': '}
+
+
+ )}
+ {subscriptionType && (
+
+ {'subscription'}
+ {': '}
+
+
+ )}
+
+
+ );
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/SearchBox.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/SearchBox.tsx
new file mode 100644
index 000000000..6f45e54ea
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/SearchBox.tsx
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, { ChangeEventHandler } from 'react';
+
+import debounce from '../../utility/debounce';
+
+type OnSearchFn = (value: string) => void;
+
+type SearchBoxProps = {
+ value?: string;
+ placeholder: string;
+ onSearch: OnSearchFn;
+};
+
+type SearchBoxState = {
+ value: string;
+};
+
+export default class SearchBox extends React.Component<
+ SearchBoxProps,
+ SearchBoxState
+> {
+ debouncedOnSearch: OnSearchFn;
+
+ constructor(props: SearchBoxProps) {
+ super(props);
+ this.state = { value: props.value || '' };
+ this.debouncedOnSearch = debounce(200, this.props.onSearch);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+
+ handleChange: ChangeEventHandler = event => {
+ const value = event.currentTarget.value;
+ this.setState({ value });
+ this.debouncedOnSearch(value);
+ };
+
+ handleClear = () => {
+ this.setState({ value: '' });
+ this.props.onSearch('');
+ };
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/SearchResults.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/SearchResults.tsx
new file mode 100644
index 000000000..d8bd30c31
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/SearchResults.tsx
@@ -0,0 +1,164 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, { ReactNode } from 'react';
+import { GraphQLSchema, GraphQLNamedType } from 'graphql';
+
+import Argument from './Argument';
+import TypeLink from './TypeLink';
+import { OnClickFieldFunction, OnClickTypeFunction } from './types';
+
+type SearchResultsProps = {
+ schema: GraphQLSchema;
+ withinType?: GraphQLNamedType;
+ searchValue: string;
+ onClickType: OnClickTypeFunction;
+ onClickField: OnClickFieldFunction;
+};
+
+export default class SearchResults extends React.Component<
+ SearchResultsProps,
+ {}
+> {
+ shouldComponentUpdate(nextProps: SearchResultsProps) {
+ return (
+ this.props.schema !== nextProps.schema ||
+ this.props.searchValue !== nextProps.searchValue
+ );
+ }
+
+ render() {
+ const searchValue = this.props.searchValue;
+ const withinType = this.props.withinType;
+ const schema = this.props.schema;
+ const onClickType = this.props.onClickType;
+ const onClickField = this.props.onClickField;
+
+ const matchedWithin: ReactNode[] = [];
+ const matchedTypes: ReactNode[] = [];
+ const matchedFields: ReactNode[] = [];
+
+ const typeMap = schema.getTypeMap();
+ let typeNames = Object.keys(typeMap);
+
+ // Move the within type name to be the first searched.
+ if (withinType) {
+ typeNames = typeNames.filter(n => n !== withinType.name);
+ typeNames.unshift(withinType.name);
+ }
+
+ for (const typeName of typeNames) {
+ if (
+ matchedWithin.length + matchedTypes.length + matchedFields.length >=
+ 100
+ ) {
+ break;
+ }
+
+ const type = typeMap[typeName];
+ if (withinType !== type && isMatch(typeName, searchValue)) {
+ matchedTypes.push(
+
+
+
,
+ );
+ }
+
+ if (type && 'getFields' in type) {
+ const fields = type.getFields();
+ Object.keys(fields).forEach(fieldName => {
+ const field = fields[fieldName];
+ let matchingArgs;
+
+ if (!isMatch(fieldName, searchValue)) {
+ if ('args' in field && field.args.length) {
+ matchingArgs = field.args.filter(arg =>
+ isMatch(arg.name, searchValue),
+ );
+ if (matchingArgs.length === 0) {
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+
+ const match = (
+
+ );
+
+ if (withinType === type) {
+ matchedWithin.push(match);
+ } else {
+ matchedFields.push(match);
+ }
+ });
+ }
+ }
+
+ if (
+ matchedWithin.length + matchedTypes.length + matchedFields.length ===
+ 0
+ ) {
+ return {'No results found.'};
+ }
+
+ if (withinType && matchedTypes.length + matchedFields.length > 0) {
+ return (
+
+ {matchedWithin}
+
+
{'other results'}
+ {matchedTypes}
+ {matchedFields}
+
+
+ );
+ }
+
+ return (
+
+ {matchedWithin}
+ {matchedTypes}
+ {matchedFields}
+
+ );
+ }
+}
+
+function isMatch(sourceText: string, searchValue: string) {
+ try {
+ const escaped = searchValue.replace(/[^_0-9A-Za-z]/g, ch => '\\' + ch);
+ return sourceText.search(new RegExp(escaped, 'i')) !== -1;
+ } catch (e) {
+ return sourceText.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1;
+ }
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/TypeDoc.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/TypeDoc.tsx
new file mode 100644
index 000000000..1f1468e75
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/TypeDoc.tsx
@@ -0,0 +1,260 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, { ReactNode } from 'react';
+import {
+ GraphQLSchema,
+ GraphQLObjectType,
+ GraphQLInterfaceType,
+ GraphQLUnionType,
+ GraphQLEnumType,
+ GraphQLType,
+ GraphQLEnumValue,
+} from 'graphql';
+
+import Argument from './Argument';
+import MarkdownContent from './MarkdownContent';
+import TypeLink from './TypeLink';
+import DefaultValue from './DefaultValue';
+import { FieldType, OnClickTypeFunction, OnClickFieldFunction } from './types';
+
+type TypeDocProps = {
+ schema: GraphQLSchema;
+ type: GraphQLType;
+ onClickType: OnClickTypeFunction;
+ onClickField: OnClickFieldFunction;
+};
+
+type TypeDocState = {
+ showDeprecated: boolean;
+};
+
+export default class TypeDoc extends React.Component<
+ TypeDocProps,
+ TypeDocState
+> {
+ constructor(props: TypeDocProps) {
+ super(props);
+ this.state = { showDeprecated: false };
+ }
+
+ shouldComponentUpdate(nextProps: TypeDocProps, nextState: TypeDocState) {
+ return (
+ this.props.type !== nextProps.type ||
+ this.props.schema !== nextProps.schema ||
+ this.state.showDeprecated !== nextState.showDeprecated
+ );
+ }
+
+ render() {
+ const schema = this.props.schema;
+ const type = this.props.type;
+ const onClickType = this.props.onClickType;
+ const onClickField = this.props.onClickField;
+
+ let typesTitle: string | null = null;
+ let types: readonly (GraphQLObjectType | GraphQLInterfaceType)[] = [];
+ if (type instanceof GraphQLUnionType) {
+ typesTitle = 'possible types';
+ types = schema.getPossibleTypes(type);
+ } else if (type instanceof GraphQLInterfaceType) {
+ typesTitle = 'implementations';
+ types = schema.getPossibleTypes(type);
+ } else if (type instanceof GraphQLObjectType) {
+ typesTitle = 'implements';
+ types = type.getInterfaces();
+ }
+
+ let typesDef;
+ if (types && types.length > 0) {
+ typesDef = (
+
+
{typesTitle}
+ {types.map(subtype => (
+
+
+
+ ))}
+
+ );
+ }
+
+ // InputObject and Object
+ let fieldsDef;
+ let deprecatedFieldsDef;
+ if (type && 'getFields' in type) {
+ const fieldMap = type.getFields();
+ const fields = Object.keys(fieldMap).map(name => fieldMap[name]);
+ fieldsDef = (
+
+
{'fields'}
+ {fields
+ .filter(field => !field.deprecationReason)
+ .map(field => (
+
+ ))}
+
+ );
+
+ const deprecatedFields = fields.filter(field =>
+ Boolean(field.deprecationReason),
+ );
+ if (deprecatedFields.length > 0) {
+ deprecatedFieldsDef = (
+
+
{'deprecated fields'}
+ {!this.state.showDeprecated ? (
+
+ ) : (
+ deprecatedFields.map(field => (
+
+ ))
+ )}
+
+ );
+ }
+ }
+
+ let valuesDef: ReactNode;
+ let deprecatedValuesDef: ReactNode;
+ if (type instanceof GraphQLEnumType) {
+ const values = type.getValues();
+ valuesDef = (
+
+
{'values'}
+ {values
+ .filter(value => Boolean(!value.deprecationReason))
+ .map(value => (
+
+ ))}
+
+ );
+
+ const deprecatedValues = values.filter(value =>
+ Boolean(value.deprecationReason),
+ );
+ if (deprecatedValues.length > 0) {
+ deprecatedValuesDef = (
+
+
{'deprecated values'}
+ {!this.state.showDeprecated ? (
+
+ ) : (
+ deprecatedValues.map(value => (
+
+ ))
+ )}
+
+ );
+ }
+ }
+
+ return (
+
+
+ {type instanceof GraphQLObjectType && typesDef}
+ {fieldsDef}
+ {deprecatedFieldsDef}
+ {valuesDef}
+ {deprecatedValuesDef}
+ {!(type instanceof GraphQLObjectType) && typesDef}
+
+ );
+ }
+
+ handleShowDeprecated = () => this.setState({ showDeprecated: true });
+}
+
+type FieldProps = {
+ type: GraphQLType;
+ field: FieldType;
+ onClickType: OnClickTypeFunction;
+ onClickField: OnClickFieldFunction;
+};
+
+function Field({ type, field, onClickType, onClickField }: FieldProps) {
+ return (
+
+
onClickField(field, type, event)}>
+ {field.name}
+
+ {'args' in field &&
+ field.args &&
+ field.args.length > 0 && [
+ '(',
+
+ {field.args
+ .filter(arg => !arg.deprecationReason)
+ .map(arg => (
+
+ ))}
+ ,
+ ')',
+ ]}
+ {': '}
+
+
+ {field.description && (
+
+ )}
+ {'deprecationReason' in field && field.deprecationReason && (
+
+ )}
+
+ );
+}
+
+type EnumValue = {
+ value: GraphQLEnumValue;
+};
+
+function EnumValue({ value }: EnumValue) {
+ return (
+
+
{value.name}
+
+ {value.deprecationReason && (
+
+ )}
+
+ );
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/TypeLink.tsx b/packages/bruno-graphql-docs/src/components/DocExplorer/TypeLink.tsx
new file mode 100644
index 000000000..cab065f47
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/TypeLink.tsx
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import {
+ GraphQLList,
+ GraphQLNonNull,
+ GraphQLType,
+ GraphQLNamedType,
+} from 'graphql';
+import { OnClickTypeFunction } from './types';
+
+type Maybe = T | null | undefined;
+
+type TypeLinkProps = {
+ type?: Maybe;
+ onClick?: OnClickTypeFunction;
+};
+
+export default function TypeLink(props: TypeLinkProps) {
+ const onClick = props.onClick ? props.onClick : () => null;
+ return renderType(props.type, onClick);
+}
+
+function renderType(type: Maybe, onClick: OnClickTypeFunction) {
+ if (type instanceof GraphQLNonNull) {
+ return (
+
+ {renderType(type.ofType, onClick)}
+ {'!'}
+
+ );
+ }
+ if (type instanceof GraphQLList) {
+ return (
+
+ {'['}
+ {renderType(type.ofType, onClick)}
+ {']'}
+
+ );
+ }
+ return (
+ {
+ event.preventDefault();
+ onClick(type as GraphQLNamedType, event);
+ }}
+ href="#">
+ {type?.name}
+
+ );
+}
diff --git a/packages/bruno-graphql-docs/src/components/DocExplorer/types.ts b/packages/bruno-graphql-docs/src/components/DocExplorer/types.ts
new file mode 100644
index 000000000..3922663ff
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/components/DocExplorer/types.ts
@@ -0,0 +1,35 @@
+import { MouseEvent } from 'react';
+import {
+ GraphQLField,
+ GraphQLInputField,
+ GraphQLArgument,
+ GraphQLObjectType,
+ GraphQLInterfaceType,
+ GraphQLInputObjectType,
+ GraphQLType,
+ GraphQLNamedType,
+} from 'graphql';
+
+export type FieldType =
+ | GraphQLField<{}, {}, {}>
+ | GraphQLInputField
+ | GraphQLArgument;
+
+export type OnClickFieldFunction = (
+ field: FieldType,
+ type?:
+ | GraphQLObjectType
+ | GraphQLInterfaceType
+ | GraphQLInputObjectType
+ | GraphQLType,
+ event?: MouseEvent,
+) => void;
+
+export type OnClickTypeFunction = (
+ type: GraphQLNamedType,
+ event?: MouseEvent,
+) => void;
+
+export type OnClickFieldOrTypeFunction =
+ | OnClickFieldFunction
+ | OnClickTypeFunction;
diff --git a/packages/bruno-graphql-docs/src/index.css b/packages/bruno-graphql-docs/src/index.css
new file mode 100644
index 000000000..d35627864
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/index.css
@@ -0,0 +1,298 @@
+.graphql-docs-container .doc-explorer {
+ background: white;
+}
+
+.graphql-docs-container .doc-explorer-title-bar,
+.graphql-docs-container .history-title-bar {
+ cursor: default;
+ display: flex;
+ height: 34px;
+ line-height: 14px;
+ padding: 8px 8px 5px;
+ position: relative;
+ user-select: none;
+}
+
+.graphql-docs-container .doc-explorer-title,
+.graphql-docs-container .history-title {
+ flex: 1;
+ font-weight: bold;
+ overflow-x: hidden;
+ padding: 10px 0 10px 10px;
+ text-align: center;
+ text-overflow: ellipsis;
+ user-select: text;
+ white-space: nowrap;
+}
+
+.graphql-docs-container .doc-explorer-back {
+ color: #3B5998;
+ cursor: pointer;
+ margin: -7px 0 -6px -8px;
+ overflow-x: hidden;
+ padding: 17px 12px 16px 16px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ background: 0;
+ border: 0;
+ line-height: 14px;
+}
+
+.doc-explorer-narrow .doc-explorer-back {
+ width: 0;
+}
+
+.graphql-docs-container .doc-explorer-back:before {
+ border-left: 2px solid #3B5998;
+ border-top: 2px solid #3B5998;
+ content: '';
+ display: inline-block;
+ height: 9px;
+ margin: 0 3px -1px 0;
+ position: relative;
+ transform: rotate(-45deg);
+ width: 9px;
+}
+
+.graphql-docs-container .doc-explorer-rhs {
+ position: relative;
+}
+
+.graphql-docs-container .doc-explorer-contents,
+.graphql-docs-container .history-contents {
+ background-color: #ffffff;
+ border-top: 1px solid #d6d6d6;
+ bottom: 0;
+ left: 0;
+ overflow-y: auto;
+ padding: 20px 15px;
+ position: absolute;
+ right: 0;
+ top: 47px;
+}
+
+.graphql-docs-container .doc-explorer-contents {
+ min-width: 300px;
+}
+
+.graphql-docs-container .doc-type-description p:first-child ,
+.graphql-docs-container .doc-type-description blockquote:first-child {
+ margin-top: 0;
+}
+
+.graphql-docs-container .doc-explorer-contents a {
+ cursor: pointer;
+ text-decoration: none;
+}
+
+.graphql-docs-container .doc-explorer-contents a:hover {
+ text-decoration: underline;
+}
+
+.graphql-docs-container .doc-value-description > :first-child {
+ margin-top: 4px;
+}
+
+.graphql-docs-container .doc-value-description > :last-child {
+ margin-bottom: 4px;
+}
+
+.graphql-docs-container .doc-type-description code,
+.graphql-docs-container .doc-type-description pre,
+.graphql-docs-container .doc-category code,
+.graphql-docs-container .doc-category pre {
+ --saf-0: rgba(var(--sk_foreground_low,29,28,29),0.13);
+ font-size: 12px;
+ line-height: 1.50001;
+ font-variant-ligatures: none;
+ white-space: pre;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ word-break: normal;
+ -webkit-tab-size: 4;
+ -moz-tab-size: 4;
+ tab-size: 4;
+}
+
+.graphql-docs-container .doc-type-description code,
+.graphql-docs-container .doc-category code {
+ padding: 2px 3px 1px;
+ border: 1px solid var(--saf-0);
+ border-radius: 3px;
+ background-color: rgba(var(--sk_foreground_min,29,28,29),.04);
+ color: #e01e5a;
+ background-color: white;
+}
+
+.graphql-docs-container .doc-category {
+ margin: 20px 0;
+}
+
+.graphql-docs-container .doc-category-title {
+ border-bottom: 1px solid #e0e0e0;
+ color: #777;
+ cursor: default;
+ font-size: 14px;
+ font-variant: small-caps;
+ font-weight: bold;
+ letter-spacing: 1px;
+ margin: 0 -15px 10px 0;
+ padding: 10px 0;
+ user-select: none;
+}
+
+.graphql-docs-container .doc-category-item {
+ margin: 12px 0;
+ color: #555;
+}
+
+.graphql-docs-container .keyword {
+ color: #B11A04;
+}
+
+.graphql-docs-container .type-name {
+ color: #CA9800;
+}
+
+.graphql-docs-container .field-name {
+ color: #1F61A0;
+}
+
+.graphql-docs-container .field-short-description {
+ color: #666;
+ margin-left: 5px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.graphql-docs-container .enum-value {
+ color: #0B7FC7;
+}
+
+.graphql-docs-container .arg-name {
+ color: #8B2BB9;
+}
+
+.graphql-docs-container .arg {
+ display: block;
+ margin-left: 1em;
+}
+
+.graphql-docs-container .arg:first-child:last-child,
+.graphql-docs-container .arg:first-child:nth-last-child(2),
+.graphql-docs-container .arg:first-child:nth-last-child(2) ~ .arg {
+ display: inherit;
+ margin: inherit;
+}
+
+.graphql-docs-container .arg:first-child:nth-last-child(2):after {
+ content: ', ';
+}
+
+.graphql-docs-container .arg-default-value {
+ color: #43A047;
+}
+
+.graphql-docs-container .doc-deprecation {
+ background: #fffae8;
+ box-shadow: inset 0 0 1px #bfb063;
+ color: #867F70;
+ line-height: 16px;
+ margin: 8px -8px;
+ max-height: 80px;
+ overflow: hidden;
+ padding: 8px;
+ border-radius: 3px;
+}
+
+.graphql-docs-container .doc-deprecation:before {
+ content: 'Deprecated:';
+ color: #c79b2e;
+ cursor: default;
+ display: block;
+ font-size: 9px;
+ font-weight: bold;
+ letter-spacing: 1px;
+ line-height: 1;
+ padding-bottom: 5px;
+ text-transform: uppercase;
+ user-select: none;
+}
+
+.graphql-docs-container .doc-deprecation > :first-child {
+ margin-top: 0;
+}
+
+.graphql-docs-container .doc-deprecation > :last-child {
+ margin-bottom: 0;
+}
+
+.graphql-docs-container .show-btn {
+ -webkit-appearance: initial;
+ display: block;
+ border-radius: 3px;
+ border: solid 1px #ccc;
+ text-align: center;
+ padding: 8px 12px 10px;
+ width: 100%;
+ box-sizing: border-box;
+ background: #fbfcfc;
+ color: #555;
+ cursor: pointer;
+}
+
+.graphql-docs-container .search-box {
+ border-bottom: 1px solid #d3d6db;
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ margin: -15px -15px 12px 0;
+ position: relative;
+}
+
+.graphql-docs-container .search-box-icon {
+ cursor: pointer;
+ display: block;
+ font-size: 24px;
+ transform: rotate(-45deg);
+ user-select: none;
+}
+
+.graphql-docs-container .search-box .search-box-clear {
+ background-color: #d0d0d0;
+ border-radius: 12px;
+ color: #fff;
+ cursor: pointer;
+ font-size: 11px;
+ padding: 1px 5px 2px;
+ position: absolute;
+ right: 3px;
+ user-select: none;
+ border: 0;
+}
+
+.graphql-docs-container .search-box .search-box-clear:hover {
+ background-color: #b9b9b9;
+}
+
+.graphql-docs-container .search-box > input {
+ border: none;
+ box-sizing: border-box;
+ font-size: 14px;
+ outline: none;
+ padding: 6px 24px 8px 20px;
+ width: 100%;
+}
+
+.graphql-docs-container .error-container {
+ font-weight: bold;
+ left: 0;
+ letter-spacing: 1px;
+ opacity: 0.5;
+ position: absolute;
+ right: 0;
+ text-align: center;
+ text-transform: uppercase;
+ top: 50%;
+ transform: translate(0, -50%);
+}
diff --git a/packages/bruno-graphql-docs/src/index.ts b/packages/bruno-graphql-docs/src/index.ts
index 54d83c494..88cc2983e 100644
--- a/packages/bruno-graphql-docs/src/index.ts
+++ b/packages/bruno-graphql-docs/src/index.ts
@@ -1,5 +1,8 @@
-import GraphDocs from "./GraphDocs";
+import { DocExplorer } from "./components/DocExplorer";
+
+// Todo: Rollup throws error
+import './index.css';
export {
- GraphDocs
-}
\ No newline at end of file
+ DocExplorer
+}
diff --git a/packages/bruno-graphql-docs/src/utility/debounce.ts b/packages/bruno-graphql-docs/src/utility/debounce.ts
new file mode 100644
index 000000000..833b8ba15
--- /dev/null
+++ b/packages/bruno-graphql-docs/src/utility/debounce.ts
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2021 GraphQL Contributors.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * Provided a duration and a function, returns a new function which is called
+ * `duration` milliseconds after the last call.
+ */
+export default function debounce any>(
+ duration: number,
+ fn: F,
+) {
+ let timeout: number | null;
+ return function (this: any, ...args: Parameters) {
+ if (timeout) {
+ window.clearTimeout(timeout);
+ }
+ timeout = window.setTimeout(() => {
+ timeout = null;
+ fn.apply(this, args);
+ }, duration);
+ };
+}