From b90d09d26ec07d2ac59b180f2ce0cca6ba498eb4 Mon Sep 17 00:00:00 2001 From: Thomas Heyenbrock Date: Sat, 4 Jun 2022 16:27:31 +0200 Subject: [PATCH 1/6] simplify type link --- .../graphiql/src/components/DocExplorer.tsx | 14 +--- .../src/components/DocExplorer/Argument.tsx | 10 +-- .../src/components/DocExplorer/FieldDoc.tsx | 16 ++-- .../src/components/DocExplorer/SchemaDoc.tsx | 20 ++--- .../components/DocExplorer/SearchResults.tsx | 9 +-- .../src/components/DocExplorer/TypeDoc.tsx | 21 ++--- .../src/components/DocExplorer/TypeLink.tsx | 39 ++++----- .../DocExplorer/__tests__/FieldDoc.spec.tsx | 69 ++++++++-------- .../DocExplorer/__tests__/TypeDoc.spec.tsx | 81 ++++++++++--------- .../DocExplorer/__tests__/TypeLink.spec.tsx | 55 +++++++++---- .../DocExplorer/__tests__/test-utils.ts | 21 +++++ .../src/components/DocExplorer/types.ts | 10 --- 12 files changed, 194 insertions(+), 171 deletions(-) create mode 100644 packages/graphiql/src/components/DocExplorer/__tests__/test-utils.ts diff --git a/packages/graphiql/src/components/DocExplorer.tsx b/packages/graphiql/src/components/DocExplorer.tsx index b5eb85b12a8..a4165feaedb 100644 --- a/packages/graphiql/src/components/DocExplorer.tsx +++ b/packages/graphiql/src/components/DocExplorer.tsx @@ -42,15 +42,11 @@ export function DocExplorer(props: DocExplorerProps) { const navItem = explorerNavStack[explorerNavStack.length - 1]; - function handleClickType(type: GraphQLNamedType) { - push({ name: type.name, def: type }); - } - function handleClickField(field: ExplorerFieldDef) { push({ name: field.name, def: field }); } - let content: ReactNode; + let content: ReactNode = null; if (fetchError) { content =
Error fetching schema
; } else if (validationErrors) { @@ -76,23 +72,21 @@ export function DocExplorer(props: DocExplorerProps) { searchValue={navItem.search} withinType={navItem.def as GraphQLNamedType} schema={schema} - onClickType={handleClickType} onClickField={handleClickField} /> ); } else if (explorerNavStack.length === 1) { - content = ; + content = ; } else if (isType(navItem.def)) { content = ( ); - } else { - content = ; + } else if (navItem.def) { + content = ; } const shouldSearchBoxAppear = diff --git a/packages/graphiql/src/components/DocExplorer/Argument.tsx b/packages/graphiql/src/components/DocExplorer/Argument.tsx index 1ea1b83ea58..15a848086e2 100644 --- a/packages/graphiql/src/components/DocExplorer/Argument.tsx +++ b/packages/graphiql/src/components/DocExplorer/Argument.tsx @@ -9,24 +9,18 @@ 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) { +export default function Argument({ arg, showDefaultValue }: ArgumentProps) { return ( {arg.name} {': '} - + {showDefaultValue !== false && } ); diff --git a/packages/graphiql/src/components/DocExplorer/FieldDoc.tsx b/packages/graphiql/src/components/DocExplorer/FieldDoc.tsx index a3a06966601..fb20d47837e 100644 --- a/packages/graphiql/src/components/DocExplorer/FieldDoc.tsx +++ b/packages/graphiql/src/components/DocExplorer/FieldDoc.tsx @@ -13,14 +13,12 @@ import Argument from './Argument'; import Directive from './Directive'; import MarkdownContent from './MarkdownContent'; import TypeLink from './TypeLink'; -import { OnClickTypeFunction } from './types'; type FieldDocProps = { - field?: ExplorerFieldDef; - onClickType: OnClickTypeFunction; + field: ExplorerFieldDef; }; -export default function FieldDoc({ field, onClickType }: FieldDocProps) { +export default function FieldDoc({ field }: FieldDocProps) { const [showDeprecated, handleShowDeprecated] = React.useState(false); let argsDef; let deprecatedArgsDef; @@ -33,7 +31,7 @@ export default function FieldDoc({ field, onClickType }: FieldDocProps) { .map((arg: GraphQLArgument) => (
- +
(
- +
{field && 'deprecationReason' in field && ( )}
type
- +
{argsDef} {directivesDef} diff --git a/packages/graphiql/src/components/DocExplorer/SchemaDoc.tsx b/packages/graphiql/src/components/DocExplorer/SchemaDoc.tsx index 820d41cb0db..2d1131c80aa 100644 --- a/packages/graphiql/src/components/DocExplorer/SchemaDoc.tsx +++ b/packages/graphiql/src/components/DocExplorer/SchemaDoc.tsx @@ -9,15 +9,13 @@ 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) { +export default function SchemaDoc({ schema }: SchemaDocProps) { const queryType = schema.getQueryType(); const mutationType = schema.getMutationType && schema.getMutationType(); const subscriptionType = @@ -34,23 +32,25 @@ export default function SchemaDoc({ schema, onClickType }: SchemaDocProps) { />
root types
-
- query - {': '} - -
+ {queryType ? ( +
+ query + {': '} + +
+ ) : null} {mutationType && (
mutation {': '} - +
)} {subscriptionType && (
subscription {': '} - +
)}
diff --git a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx b/packages/graphiql/src/components/DocExplorer/SearchResults.tsx index 22b7f5bdcc5..8dc5ef94337 100644 --- a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx +++ b/packages/graphiql/src/components/DocExplorer/SearchResults.tsx @@ -10,13 +10,12 @@ import { GraphQLSchema, GraphQLNamedType } from 'graphql'; import Argument from './Argument'; import TypeLink from './TypeLink'; -import { OnClickFieldFunction, OnClickTypeFunction } from './types'; +import { OnClickFieldFunction } from './types'; type SearchResultsProps = { schema: GraphQLSchema; withinType?: GraphQLNamedType; searchValue: string; - onClickType: OnClickTypeFunction; onClickField: OnClickFieldFunction; }; @@ -35,7 +34,6 @@ export default class SearchResults extends React.Component< 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[] = []; @@ -63,7 +61,7 @@ export default class SearchResults extends React.Component< if (withinType !== type && isMatch(typeName, searchValue)) { matchedTypes.push(
- +
, ); } @@ -90,7 +88,7 @@ export default class SearchResults extends React.Component< const match = (
{withinType !== type && [ - , + , '.', ]} ))} diff --git a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx index ff38a394e41..ce9c69c5105 100644 --- a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx +++ b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx @@ -12,8 +12,8 @@ import { GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, - GraphQLType, GraphQLEnumValue, + GraphQLNamedType, } from 'graphql'; import { ExplorerFieldDef } from '@graphiql/react'; @@ -21,12 +21,11 @@ import Argument from './Argument'; import MarkdownContent from './MarkdownContent'; import TypeLink from './TypeLink'; import DefaultValue from './DefaultValue'; -import { OnClickTypeFunction, OnClickFieldFunction } from './types'; +import { OnClickFieldFunction } from './types'; type TypeDocProps = { schema: GraphQLSchema; - type: GraphQLType; - onClickType: OnClickTypeFunction; + type: GraphQLNamedType; onClickField: OnClickFieldFunction; }; @@ -54,7 +53,6 @@ export default class TypeDoc extends React.Component< 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; @@ -77,7 +75,7 @@ export default class TypeDoc extends React.Component<
{typesTitle}
{types.map(subtype => (
- +
))}
@@ -100,7 +98,6 @@ export default class TypeDoc extends React.Component< key={field.name} type={type} field={field} - onClickType={onClickType} onClickField={onClickField} /> ))} @@ -124,7 +121,6 @@ export default class TypeDoc extends React.Component< key={field.name} type={type} field={field} - onClickType={onClickType} onClickField={onClickField} /> )) @@ -192,13 +188,12 @@ export default class TypeDoc extends React.Component< } type FieldProps = { - type: GraphQLType; + type: GraphQLNamedType; field: ExplorerFieldDef; - onClickType: OnClickTypeFunction; onClickField: OnClickFieldFunction; }; -function Field({ type, field, onClickType, onClickField }: FieldProps) { +function Field({ type, field, onClickField }: FieldProps) { return (
!arg.deprecationReason) .map(arg => ( - + ))} , ')', ]} {': '} - + {field.description && ( ; - onClick?: OnClickTypeFunction; + type: GraphQLType; }; export default function TypeLink(props: TypeLinkProps) { - const onClick = props.onClick ? props.onClick : () => null; - return renderType(props.type, onClick); -} + const { push } = useExplorerContext({ nonNull: true, caller: TypeLink }); + + if (!props.type) { + return null; + } -function renderType(type: Maybe, onClick: OnClickTypeFunction) { + const type = props.type; if (type instanceof GraphQLNonNull) { - return {renderType(type.ofType, onClick)}!; + return ( + <> + ! + + ); } if (type instanceof GraphQLList) { - return [{renderType(type.ofType, onClick)}]; + return ( + <> + [] + + ); } return ( { event.preventDefault(); - onClick(type as GraphQLNamedType, event); + push({ name: type.name, def: type }); }} href="#"> - {type?.name} + {type.name} ); } diff --git a/packages/graphiql/src/components/DocExplorer/__tests__/FieldDoc.spec.tsx b/packages/graphiql/src/components/DocExplorer/__tests__/FieldDoc.spec.tsx index 6fe9af3c93a..84399d61866 100644 --- a/packages/graphiql/src/components/DocExplorer/__tests__/FieldDoc.spec.tsx +++ b/packages/graphiql/src/components/DocExplorer/__tests__/FieldDoc.spec.tsx @@ -5,57 +5,61 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { + ExplorerContext, + ExplorerContextType, + ExplorerNavStackItem, +} from '@graphiql/react'; +import { + // @ts-expect-error + fireEvent, + render, +} from '@testing-library/react'; +import { GraphQLString, GraphQLObjectType, Kind } from 'graphql'; +import React, { ComponentProps } from 'react'; import FieldDoc from '../FieldDoc'; - -import { GraphQLString, GraphQLObjectType } from 'graphql'; +import { mockExplorerContextValue } from './test-utils'; const exampleObject = new GraphQLObjectType({ name: 'Query', fields: { string: { - name: 'simpleStringField', type: GraphQLString, }, stringWithArgs: { - name: 'stringWithArgs', type: GraphQLString, description: 'Example String field with arguments', args: { stringArg: { - name: 'stringArg', type: GraphQLString, }, deprecatedStringArg: { - name: 'deprecatedStringArg', type: GraphQLString, deprecationReason: 'no longer used', }, }, }, stringWithDirective: { - name: 'stringWithDirective', type: GraphQLString, astNode: { - kind: 'FieldDefinition', + kind: Kind.FIELD_DEFINITION, name: { - kind: 'Name', + kind: Kind.NAME, value: 'stringWithDirective', }, type: { - kind: 'NamedType', + kind: Kind.NAMED_TYPE, name: { - kind: 'Name', + kind: Kind.NAME, value: 'GraphQLString', }, }, directives: [ { - kind: 'Directive', + kind: Kind.DIRECTIVE, name: { - kind: 'Name', + kind: Kind.NAME, value: 'development', }, }, @@ -65,13 +69,22 @@ const exampleObject = new GraphQLObjectType({ }, }); +function FieldDocWithContext(props: ComponentProps) { + return ( + + + + ); +} + describe('FieldDoc', () => { it('should render a simple string field', () => { const { container } = render( - , + , ); expect(container.querySelector('.doc-type-description')).toHaveTextContent( 'No Description', @@ -82,10 +95,7 @@ describe('FieldDoc', () => { it('should re-render on field change', () => { const { container, rerender } = render( - , + , ); expect(container.querySelector('.doc-type-description')).toHaveTextContent( 'No Description', @@ -94,10 +104,7 @@ describe('FieldDoc', () => { expect(container.querySelector('.arg')).not.toBeInTheDocument(); rerender( - , + , ); expect(container.querySelector('.type-name')).toHaveTextContent('String'); expect(container.querySelector('.doc-type-description')).toHaveTextContent( @@ -107,10 +114,7 @@ describe('FieldDoc', () => { it('should render a string field with arguments', () => { const { container } = render( - , + , ); expect(container.querySelector('.type-name')).toHaveTextContent('String'); expect(container.querySelector('.doc-type-description')).toHaveTextContent( @@ -131,9 +135,8 @@ describe('FieldDoc', () => { it('should render a string field with directives', () => { const { container } = render( - , ); expect(container.querySelector('.type-name')).toHaveTextContent('String'); diff --git a/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx b/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx index cabd90e85d7..a2a9cbb8535 100644 --- a/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx +++ b/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx @@ -5,12 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; - -import { GraphQLString } from 'graphql'; - -import TypeDoc from '../TypeDoc'; +import { ExplorerContext } from '@graphiql/react'; +import { + // @ts-expect-error + fireEvent, + render, +} from '@testing-library/react'; +import { GraphQLType } from 'graphql'; +import React, { ComponentProps } from 'react'; import { ExampleSchema, @@ -18,15 +20,28 @@ import { ExampleUnion, ExampleEnum, } from '../../__tests__/ExampleSchema'; +import TypeDoc from '../TypeDoc'; +import { mockExplorerContextValue, unwrapType } from './test-utils'; + +function TypeDocWithContext(props: ComponentProps) { + return ( + + + + ); +} describe('TypeDoc', () => { it('renders a top-level query object type', () => { const { container } = render( - // @ts-ignore - , ); const description = container.querySelectorAll('.doc-type-description'); @@ -43,35 +58,12 @@ describe('TypeDoc', () => { ); }); - it('handles onClickField and onClickType', () => { - const onClickType = jest.fn(); - const onClickField = jest.fn(); - const { container } = render( - , - ); - fireEvent.click(container.querySelector('.type-name')!); - expect(onClickType.mock.calls.length).toEqual(1); - expect(onClickType.mock.calls[0][0]).toEqual(GraphQLString); - - fireEvent.click(container.querySelector('.field-name')!); - expect(onClickField.mock.calls.length).toEqual(1); - expect(onClickField.mock.calls[0][0].name).toEqual('string'); - expect(onClickField.mock.calls[0][0].type).toEqual(GraphQLString); - expect(onClickField.mock.calls[0][1]).toEqual(ExampleQuery); - }); - it('renders deprecated fields when you click to see them', () => { const { container } = render( - // @ts-ignore - , ); let cats = container.querySelectorAll('.doc-category-item'); @@ -91,8 +83,11 @@ describe('TypeDoc', () => { it('renders a Union type', () => { const { container } = render( - // @ts-ignore - , + , ); expect(container.querySelector('.doc-category-title')).toHaveTextContent( 'possible types', @@ -101,8 +96,11 @@ describe('TypeDoc', () => { it('renders an Enum type', () => { const { container } = render( - // @ts-ignore - , + , ); expect(container.querySelector('.doc-category-title')).toHaveTextContent( 'values', @@ -114,8 +112,11 @@ describe('TypeDoc', () => { it('shows deprecated enum values on click', () => { const { getByText, container } = render( - // @ts-ignore - , + , ); const showBtn = getByText('Show deprecated values...'); expect(showBtn).toBeInTheDocument(); diff --git a/packages/graphiql/src/components/DocExplorer/__tests__/TypeLink.spec.tsx b/packages/graphiql/src/components/DocExplorer/__tests__/TypeLink.spec.tsx index df8c8ff630b..c60be4489c7 100644 --- a/packages/graphiql/src/components/DocExplorer/__tests__/TypeLink.spec.tsx +++ b/packages/graphiql/src/components/DocExplorer/__tests__/TypeLink.spec.tsx @@ -5,44 +5,71 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { ExplorerContext } from '@graphiql/react'; +import { + // @ts-expect-error + fireEvent, + render, +} from '@testing-library/react'; +import { GraphQLNonNull, GraphQLList, GraphQLString } from 'graphql'; +import React, { ComponentProps } from 'react'; import TypeLink from '../TypeLink'; - -import { GraphQLNonNull, GraphQLList, GraphQLString } from 'graphql'; +import { mockExplorerContextValue, unwrapType } from './test-utils'; const nonNullType = new GraphQLNonNull(GraphQLString); const listType = new GraphQLList(GraphQLString); +function TypeLinkWithContext(props: ComponentProps) { + return ( + + + {/* Print the top of the current nav stack for test assertions */} + + {({ explorerNavStack }) => ( + + {JSON.stringify(explorerNavStack[explorerNavStack.length + 1])} + + )} + + + ); +} + describe('TypeLink', () => { it('should render a string', () => { - const { container } = render(); + const { container } = render(); expect(container).toHaveTextContent('String'); expect(container.querySelectorAll('a')).toHaveLength(1); expect(container.querySelector('a')).toHaveClass('type-name'); }); it('should render a nonnull type', () => { - const { container } = render(); + const { container } = render(); expect(container).toHaveTextContent('String!'); expect(container.querySelectorAll('span')).toHaveLength(1); }); it('should render a list type', () => { - const { container } = render(); + const { container } = render(); expect(container).toHaveTextContent('[String]'); expect(container.querySelectorAll('span')).toHaveLength(1); }); - it('should handle a click event', () => { - const op = jest.fn(); - const { container } = render(); + it('should push to the nav stack on click', () => { + const { container, getByTestId } = render( + , + ); fireEvent.click(container.querySelector('a')!); - expect(op.mock.calls.length).toEqual(1); - expect(op.mock.calls[0][0]).toEqual(GraphQLString); + expect(getByTestId('nav-stack')).toHaveTextContent(''); }); it('should re-render on type change', () => { - const { container, rerender } = render(); + const { container, rerender } = render( + , + ); expect(container).toHaveTextContent('[String]'); - rerender(); + rerender(); expect(container).toHaveTextContent('String'); }); }); diff --git a/packages/graphiql/src/components/DocExplorer/__tests__/test-utils.ts b/packages/graphiql/src/components/DocExplorer/__tests__/test-utils.ts new file mode 100644 index 00000000000..2657315225d --- /dev/null +++ b/packages/graphiql/src/components/DocExplorer/__tests__/test-utils.ts @@ -0,0 +1,21 @@ +import { ExplorerContextType, ExplorerNavStackItem } from '@graphiql/react'; +import { GraphQLNamedType, GraphQLType } from 'graphql'; + +export function mockExplorerContextValue( + navStackItem: ExplorerNavStackItem, +): ExplorerContextType { + return { + explorerNavStack: [navStackItem], + hide() {}, + isVisible: true, + pop() {}, + push() {}, + reset() {}, + show() {}, + showSearch() {}, + }; +} + +export function unwrapType(type: GraphQLType): GraphQLNamedType { + return 'ofType' in type ? unwrapType(type.ofType) : type; +} diff --git a/packages/graphiql/src/components/DocExplorer/types.ts b/packages/graphiql/src/components/DocExplorer/types.ts index cddb36494f7..fa4dd6148f2 100644 --- a/packages/graphiql/src/components/DocExplorer/types.ts +++ b/packages/graphiql/src/components/DocExplorer/types.ts @@ -4,7 +4,6 @@ import { GraphQLInterfaceType, GraphQLInputObjectType, GraphQLType, - GraphQLNamedType, } from 'graphql'; import { ExplorerFieldDef } from '@graphiql/react'; @@ -17,12 +16,3 @@ export type OnClickFieldFunction = ( | GraphQLType, event?: MouseEvent, ) => void; - -export type OnClickTypeFunction = ( - type: GraphQLNamedType, - event?: MouseEvent, -) => void; - -export type OnClickFieldOrTypeFunction = - | OnClickFieldFunction - | OnClickTypeFunction; From 925f0266bab1e1304161c56ea4ee0a51484e4d3b Mon Sep 17 00:00:00 2001 From: Thomas Heyenbrock Date: Sat, 4 Jun 2022 16:48:15 +0200 Subject: [PATCH 2/6] refactor class components to function components --- .../components/DocExplorer/SearchResults.tsx | 31 ++++------ .../src/components/DocExplorer/TypeDoc.tsx | 56 ++++++++----------- 2 files changed, 35 insertions(+), 52 deletions(-) diff --git a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx b/packages/graphiql/src/components/DocExplorer/SearchResults.tsx index 8dc5ef94337..e4dd7953332 100644 --- a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx +++ b/packages/graphiql/src/components/DocExplorer/SearchResults.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { ReactNode } from 'react'; +import React, { memo, ReactNode } from 'react'; import { GraphQLSchema, GraphQLNamedType } from 'graphql'; import Argument from './Argument'; @@ -19,22 +19,12 @@ type SearchResultsProps = { 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 onClickField = this.props.onClickField; +export default memo( + function SearchResults(props: SearchResultsProps) { + const searchValue = props.searchValue; + const withinType = props.withinType; + const schema = props.schema; + const onClickField = props.onClickField; const matchedWithin: ReactNode[] = []; const matchedTypes: ReactNode[] = []; @@ -148,8 +138,11 @@ export default class SearchResults extends React.Component< {matchedFields}
); - } -} + }, + (prevProps, nextProps) => + prevProps.schema === nextProps.schema && + prevProps.searchValue === nextProps.searchValue, +); function isMatch(sourceText: string, searchValue: string) { try { diff --git a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx index ce9c69c5105..6c858651aa1 100644 --- a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx +++ b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React, { ReactNode } from 'react'; +import React, { memo, ReactNode, useState } from 'react'; import { GraphQLSchema, GraphQLObjectType, @@ -29,31 +29,13 @@ type TypeDocProps = { onClickField: OnClickFieldFunction; }; -type TypeDocState = { - showDeprecated: boolean; -}; - -export default class TypeDoc extends React.Component< - TypeDocProps, - TypeDocState -> { - constructor(props: TypeDocProps) { - super(props); - this.state = { showDeprecated: false }; - } +export default memo( + function TypeDoc(props: TypeDocProps) { + const [showDeprecated, setShowDeprecated] = useState(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 onClickField = this.props.onClickField; + const schema = props.schema; + const type = props.type; + const onClickField = props.onClickField; let typesTitle: string | null = null; let types: readonly (GraphQLObjectType | GraphQLInterfaceType)[] = []; @@ -111,8 +93,12 @@ export default class TypeDoc extends React.Component< deprecatedFieldsDef = (
deprecated fields
- {!this.state.showDeprecated ? ( - ) : ( @@ -152,8 +138,12 @@ export default class TypeDoc extends React.Component< deprecatedValuesDef = (
deprecated values
- {!this.state.showDeprecated ? ( - ) : ( @@ -182,10 +172,10 @@ export default class TypeDoc extends React.Component< {!(type instanceof GraphQLObjectType) && typesDef}
); - } - - handleShowDeprecated = () => this.setState({ showDeprecated: true }); -} + }, + (prevProps, nextProps) => + prevProps.type === nextProps.type && prevProps.schema === nextProps.schema, +); type FieldProps = { type: GraphQLNamedType; From 85f28142ff0aad4ffdd84ddb6ec1e4b00053daca Mon Sep 17 00:00:00 2001 From: Thomas Heyenbrock Date: Sat, 4 Jun 2022 16:58:37 +0200 Subject: [PATCH 3/6] remove props and consume contexts --- .../graphiql/src/components/DocExplorer.tsx | 23 +- .../src/components/DocExplorer/FieldDoc.tsx | 18 +- .../src/components/DocExplorer/SchemaDoc.tsx | 14 +- .../components/DocExplorer/SearchResults.tsx | 214 +++++++------- .../src/components/DocExplorer/TypeDoc.tsx | 273 +++++++++--------- .../DocExplorer/__tests__/FieldDoc.spec.tsx | 12 +- .../DocExplorer/__tests__/TypeDoc.spec.tsx | 62 ++-- 7 files changed, 296 insertions(+), 320 deletions(-) diff --git a/packages/graphiql/src/components/DocExplorer.tsx b/packages/graphiql/src/components/DocExplorer.tsx index a4165feaedb..2f0f66c68ec 100644 --- a/packages/graphiql/src/components/DocExplorer.tsx +++ b/packages/graphiql/src/components/DocExplorer.tsx @@ -6,7 +6,7 @@ */ import React, { ReactNode } from 'react'; -import { isType, GraphQLNamedType } from 'graphql'; +import { isType } from 'graphql'; import { ExplorerFieldDef, useExplorerContext, @@ -67,26 +67,13 @@ export function DocExplorer(props: DocExplorerProps) { // an error during introspection. content =
No Schema Available
; } else if (navItem.search) { - content = ( - - ); + content = ; } else if (explorerNavStack.length === 1) { - content = ; + content = ; } else if (isType(navItem.def)) { - content = ( - - ); + content = ; } else if (navItem.def) { - content = ; + content = ; } const shouldSearchBoxAppear = diff --git a/packages/graphiql/src/components/DocExplorer/FieldDoc.tsx b/packages/graphiql/src/components/DocExplorer/FieldDoc.tsx index fb20d47837e..61c29076e24 100644 --- a/packages/graphiql/src/components/DocExplorer/FieldDoc.tsx +++ b/packages/graphiql/src/components/DocExplorer/FieldDoc.tsx @@ -6,20 +6,24 @@ */ import React from 'react'; -import { GraphQLArgument, DirectiveNode } from 'graphql'; -import { ExplorerFieldDef } from '@graphiql/react'; +import { GraphQLArgument, DirectiveNode, isType } from 'graphql'; +import { useExplorerContext } from '@graphiql/react'; import Argument from './Argument'; import Directive from './Directive'; import MarkdownContent from './MarkdownContent'; import TypeLink from './TypeLink'; -type FieldDocProps = { - field: ExplorerFieldDef; -}; - -export default function FieldDoc({ field }: FieldDocProps) { +export default function FieldDoc() { + const { explorerNavStack } = useExplorerContext({ nonNull: true }); const [showDeprecated, handleShowDeprecated] = React.useState(false); + + const navItem = explorerNavStack[explorerNavStack.length - 1]; + const field = navItem.def; + if (!field || isType(field)) { + return null; + } + let argsDef; let deprecatedArgsDef; if (field && 'args' in field && field.args.length > 0) { diff --git a/packages/graphiql/src/components/DocExplorer/SchemaDoc.tsx b/packages/graphiql/src/components/DocExplorer/SchemaDoc.tsx index 2d1131c80aa..ae20d10bfe5 100644 --- a/packages/graphiql/src/components/DocExplorer/SchemaDoc.tsx +++ b/packages/graphiql/src/components/DocExplorer/SchemaDoc.tsx @@ -8,14 +8,16 @@ import React from 'react'; import TypeLink from './TypeLink'; import MarkdownContent from './MarkdownContent'; -import { GraphQLSchema } from 'graphql'; - -type SchemaDocProps = { - schema: GraphQLSchema; -}; +import { useSchemaContext } from '@graphiql/react'; // Render the top level Schema -export default function SchemaDoc({ schema }: SchemaDocProps) { +export default function SchemaDoc() { + const { schema } = useSchemaContext({ nonNull: true }); + + if (!schema) { + return null; + } + const queryType = schema.getQueryType(); const mutationType = schema.getMutationType && schema.getMutationType(); const subscriptionType = diff --git a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx b/packages/graphiql/src/components/DocExplorer/SearchResults.tsx index e4dd7953332..c059cbbf705 100644 --- a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx +++ b/packages/graphiql/src/components/DocExplorer/SearchResults.tsx @@ -5,144 +5,134 @@ * LICENSE file in the root directory of this source tree. */ -import React, { memo, ReactNode } from 'react'; -import { GraphQLSchema, GraphQLNamedType } from 'graphql'; +import React, { ReactNode } from 'react'; import Argument from './Argument'; import TypeLink from './TypeLink'; import { OnClickFieldFunction } from './types'; +import { useExplorerContext, useSchemaContext } from '@graphiql/react'; type SearchResultsProps = { - schema: GraphQLSchema; - withinType?: GraphQLNamedType; - searchValue: string; onClickField: OnClickFieldFunction; }; -export default memo( - function SearchResults(props: SearchResultsProps) { - const searchValue = props.searchValue; - const withinType = props.withinType; - const schema = props.schema; - const onClickField = props.onClickField; +export default function SearchResults(props: SearchResultsProps) { + const { explorerNavStack } = useExplorerContext({ nonNull: true }); + const { schema } = useSchemaContext({ nonNull: true }); - const matchedWithin: ReactNode[] = []; - const matchedTypes: ReactNode[] = []; - const matchedFields: ReactNode[] = []; + const navItem = explorerNavStack[explorerNavStack.length - 1]; - const typeMap = schema.getTypeMap(); - let typeNames = Object.keys(typeMap); + if (!schema || !navItem.search) { + return null; + } - // Move the within type name to be the first searched. - if (withinType) { - typeNames = typeNames.filter(n => n !== withinType.name); - typeNames.unshift(withinType.name); - } + const searchValue = navItem.search; + const withinType = navItem.def; + const onClickField = props.onClickField; - 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 matchedWithin: ReactNode[] = []; + const matchedTypes: ReactNode[] = []; + const matchedFields: ReactNode[] = []; - const match = ( -
- {withinType !== type && [ - , - '.', - ]} - onClickField(field, type, event)}> - {field.name} - - {matchingArgs && [ - '(', - - {matchingArgs.map(arg => ( - - ))} - , - ')', - ]} -
- ); - - if (withinType === type) { - matchedWithin.push(match); - } else { - matchedFields.push(match); - } - }); - } - } + 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 === - 0 + matchedWithin.length + matchedTypes.length + matchedFields.length >= + 100 ) { - return No results found.; + break; } - if (withinType && matchedTypes.length + matchedFields.length > 0) { - return ( -
- {matchedWithin} -
-
other results
- {matchedTypes} - {matchedFields} -
-
+ 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 = ( +
+ {withinType !== type && [, '.']} + onClickField(field, type, event)}> + {field.name} + + {matchingArgs && [ + '(', + + {matchingArgs.map(arg => ( + + ))} + , + ')', + ]} +
+ ); + + 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} - {matchedTypes} - {matchedFields} +
+
other results
+ {matchedTypes} + {matchedFields} +
); - }, - (prevProps, nextProps) => - prevProps.schema === nextProps.schema && - prevProps.searchValue === nextProps.searchValue, -); + } + + return ( +
+ {matchedWithin} + {matchedTypes} + {matchedFields} +
+ ); +} function isMatch(sourceText: string, searchValue: string) { try { diff --git a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx index 6c858651aa1..60aeaa2a2e7 100644 --- a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx +++ b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx @@ -5,17 +5,21 @@ * LICENSE file in the root directory of this source tree. */ -import React, { memo, ReactNode, useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import { - GraphQLSchema, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLEnumValue, GraphQLNamedType, + isType, } from 'graphql'; -import { ExplorerFieldDef } from '@graphiql/react'; +import { + ExplorerFieldDef, + useExplorerContext, + useSchemaContext, +} from '@graphiql/react'; import Argument from './Argument'; import MarkdownContent from './MarkdownContent'; @@ -24,158 +28,159 @@ import DefaultValue from './DefaultValue'; import { OnClickFieldFunction } from './types'; type TypeDocProps = { - schema: GraphQLSchema; - type: GraphQLNamedType; onClickField: OnClickFieldFunction; }; -export default memo( - function TypeDoc(props: TypeDocProps) { - const [showDeprecated, setShowDeprecated] = useState(false); - - const schema = props.schema; - const type = props.type; - const onClickField = 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 => ( -
- -
+export default function TypeDoc(props: TypeDocProps) { + const { schema } = useSchemaContext({ nonNull: true }); + const { explorerNavStack } = useExplorerContext({ nonNull: true }); + const [showDeprecated, setShowDeprecated] = useState(false); + + const navItem = explorerNavStack[explorerNavStack.length - 1]; + const type = navItem.def; + + if (!schema || !isType(type)) { + return null; + } + + const onClickField = 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 => ( + ))} -
- ); - } +
+ ); - // 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
+ {!showDeprecated ? ( + + ) : ( + deprecatedFields.map(field => ( - ))} + )) + )}
); - - const deprecatedFields = fields.filter(field => - Boolean(field.deprecationReason), - ); - if (deprecatedFields.length > 0) { - deprecatedFieldsDef = ( -
-
deprecated fields
- {!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 => ( + + ))} +
+ ); - let valuesDef: ReactNode; - let deprecatedValuesDef: ReactNode; - if (type instanceof GraphQLEnumType) { - const values = type.getValues(); - valuesDef = ( + const deprecatedValues = values.filter(value => + Boolean(value.deprecationReason), + ); + if (deprecatedValues.length > 0) { + deprecatedValuesDef = (
-
values
- {values - .filter(value => Boolean(!value.deprecationReason)) - .map(value => ( +
deprecated values
+ {!showDeprecated ? ( + + ) : ( + deprecatedValues.map(value => ( - ))} + )) + )}
); - - const deprecatedValues = values.filter(value => - Boolean(value.deprecationReason), - ); - if (deprecatedValues.length > 0) { - deprecatedValuesDef = ( -
-
deprecated values
- {!showDeprecated ? ( - - ) : ( - deprecatedValues.map(value => ( - - )) - )} -
- ); - } } + } - return ( -
- - {type instanceof GraphQLObjectType && typesDef} - {fieldsDef} - {deprecatedFieldsDef} - {valuesDef} - {deprecatedValuesDef} - {!(type instanceof GraphQLObjectType) && typesDef} -
- ); - }, - (prevProps, nextProps) => - prevProps.type === nextProps.type && prevProps.schema === nextProps.schema, -); + return ( +
+ + {type instanceof GraphQLObjectType && typesDef} + {fieldsDef} + {deprecatedFieldsDef} + {valuesDef} + {deprecatedValuesDef} + {!(type instanceof GraphQLObjectType) && typesDef} +
+ ); +} type FieldProps = { type: GraphQLNamedType; diff --git a/packages/graphiql/src/components/DocExplorer/__tests__/FieldDoc.spec.tsx b/packages/graphiql/src/components/DocExplorer/__tests__/FieldDoc.spec.tsx index 84399d61866..74de7581b54 100644 --- a/packages/graphiql/src/components/DocExplorer/__tests__/FieldDoc.spec.tsx +++ b/packages/graphiql/src/components/DocExplorer/__tests__/FieldDoc.spec.tsx @@ -5,18 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import { - ExplorerContext, - ExplorerContextType, - ExplorerNavStackItem, -} from '@graphiql/react'; +import { ExplorerContext, ExplorerFieldDef } from '@graphiql/react'; import { // @ts-expect-error fireEvent, render, } from '@testing-library/react'; import { GraphQLString, GraphQLObjectType, Kind } from 'graphql'; -import React, { ComponentProps } from 'react'; +import React from 'react'; import FieldDoc from '../FieldDoc'; import { mockExplorerContextValue } from './test-utils'; @@ -69,14 +65,14 @@ const exampleObject = new GraphQLObjectType({ }, }); -function FieldDocWithContext(props: ComponentProps) { +function FieldDocWithContext(props: { field: ExplorerFieldDef }) { return ( - + ); } diff --git a/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx b/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx index a2a9cbb8535..c9bbd011ae1 100644 --- a/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx +++ b/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx @@ -5,13 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import { ExplorerContext } from '@graphiql/react'; +import { ExplorerContext, SchemaContext } from '@graphiql/react'; import { // @ts-expect-error fireEvent, render, } from '@testing-library/react'; -import { GraphQLType } from 'graphql'; +import { GraphQLNamedType } from 'graphql'; import React, { ComponentProps } from 'react'; import { @@ -23,26 +23,34 @@ import { import TypeDoc from '../TypeDoc'; import { mockExplorerContextValue, unwrapType } from './test-utils'; -function TypeDocWithContext(props: ComponentProps) { +function TypeDocWithContext( + props: ComponentProps & { type: GraphQLNamedType }, +) { return ( - - - + + + + + ); } describe('TypeDoc', () => { it('renders a top-level query object type', () => { const { container } = render( - , + , ); const description = container.querySelectorAll('.doc-type-description'); expect(description).toHaveLength(1); @@ -60,11 +68,7 @@ describe('TypeDoc', () => { it('renders deprecated fields when you click to see them', () => { const { container } = render( - , + , ); let cats = container.querySelectorAll('.doc-category-item'); expect(cats).toHaveLength(3); @@ -83,11 +87,7 @@ describe('TypeDoc', () => { it('renders a Union type', () => { const { container } = render( - , + , ); expect(container.querySelector('.doc-category-title')).toHaveTextContent( 'possible types', @@ -96,11 +96,7 @@ describe('TypeDoc', () => { it('renders an Enum type', () => { const { container } = render( - , + , ); expect(container.querySelector('.doc-category-title')).toHaveTextContent( 'values', @@ -112,11 +108,7 @@ describe('TypeDoc', () => { it('shows deprecated enum values on click', () => { const { getByText, container } = render( - , + , ); const showBtn = getByText('Show deprecated values...'); expect(showBtn).toBeInTheDocument(); From 8dc4de5ae4fa4d065dce2cec90927c7cecaa8c6d Mon Sep 17 00:00:00 2001 From: Thomas Heyenbrock Date: Sat, 4 Jun 2022 17:06:12 +0200 Subject: [PATCH 4/6] simplify field link --- .../graphiql/src/components/DocExplorer.tsx | 16 +++------ .../src/components/DocExplorer/FieldLink.tsx | 30 +++++++++++++++++ .../components/DocExplorer/SearchResults.tsx | 15 ++------- .../src/components/DocExplorer/TypeDoc.tsx | 33 ++++--------------- .../DocExplorer/__tests__/TypeDoc.spec.tsx | 26 +++++---------- .../src/components/DocExplorer/types.ts | 18 ---------- 6 files changed, 51 insertions(+), 87 deletions(-) create mode 100644 packages/graphiql/src/components/DocExplorer/FieldLink.tsx delete mode 100644 packages/graphiql/src/components/DocExplorer/types.ts diff --git a/packages/graphiql/src/components/DocExplorer.tsx b/packages/graphiql/src/components/DocExplorer.tsx index 2f0f66c68ec..f66c485018f 100644 --- a/packages/graphiql/src/components/DocExplorer.tsx +++ b/packages/graphiql/src/components/DocExplorer.tsx @@ -7,11 +7,7 @@ import React, { ReactNode } from 'react'; import { isType } from 'graphql'; -import { - ExplorerFieldDef, - useExplorerContext, - useSchemaContext, -} from '@graphiql/react'; +import { useExplorerContext, useSchemaContext } from '@graphiql/react'; import FieldDoc from './DocExplorer/FieldDoc'; import SchemaDoc from './DocExplorer/SchemaDoc'; @@ -36,16 +32,12 @@ export function DocExplorer(props: DocExplorerProps) { schema, validationErrors, } = useSchemaContext({ nonNull: true }); - const { explorerNavStack, hide, pop, push, showSearch } = useExplorerContext({ + const { explorerNavStack, hide, pop, showSearch } = useExplorerContext({ nonNull: true, }); const navItem = explorerNavStack[explorerNavStack.length - 1]; - function handleClickField(field: ExplorerFieldDef) { - push({ name: field.name, def: field }); - } - let content: ReactNode = null; if (fetchError) { content =
Error fetching schema
; @@ -67,11 +59,11 @@ export function DocExplorer(props: DocExplorerProps) { // an error during introspection. content =
No Schema Available
; } else if (navItem.search) { - content = ; + content = ; } else if (explorerNavStack.length === 1) { content = ; } else if (isType(navItem.def)) { - content = ; + content = ; } else if (navItem.def) { content = ; } diff --git a/packages/graphiql/src/components/DocExplorer/FieldLink.tsx b/packages/graphiql/src/components/DocExplorer/FieldLink.tsx new file mode 100644 index 00000000000..a5d9d5caaa8 --- /dev/null +++ b/packages/graphiql/src/components/DocExplorer/FieldLink.tsx @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 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 { ExplorerFieldDef, useExplorerContext } from '@graphiql/react'; + +type FieldLinkProps = { + field: ExplorerFieldDef; +}; + +export default function FieldLink(props: FieldLinkProps) { + const { push } = useExplorerContext({ nonNull: true }); + + return ( + { + event.preventDefault(); + push({ name: props.field.name, def: props.field }); + }} + href="#"> + {props.field.name} + + ); +} diff --git a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx b/packages/graphiql/src/components/DocExplorer/SearchResults.tsx index c059cbbf705..1e282e0dac4 100644 --- a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx +++ b/packages/graphiql/src/components/DocExplorer/SearchResults.tsx @@ -9,14 +9,10 @@ import React, { ReactNode } from 'react'; import Argument from './Argument'; import TypeLink from './TypeLink'; -import { OnClickFieldFunction } from './types'; import { useExplorerContext, useSchemaContext } from '@graphiql/react'; +import FieldLink from './FieldLink'; -type SearchResultsProps = { - onClickField: OnClickFieldFunction; -}; - -export default function SearchResults(props: SearchResultsProps) { +export default function SearchResults() { const { explorerNavStack } = useExplorerContext({ nonNull: true }); const { schema } = useSchemaContext({ nonNull: true }); @@ -28,7 +24,6 @@ export default function SearchResults(props: SearchResultsProps) { const searchValue = navItem.search; const withinType = navItem.def; - const onClickField = props.onClickField; const matchedWithin: ReactNode[] = []; const matchedTypes: ReactNode[] = []; @@ -82,11 +77,7 @@ export default function SearchResults(props: SearchResultsProps) { const match = (
{withinType !== type && [, '.']} - onClickField(field, type, event)}> - {field.name} - + {matchingArgs && [ '(', diff --git a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx index 60aeaa2a2e7..1397981c375 100644 --- a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx +++ b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx @@ -25,13 +25,9 @@ import Argument from './Argument'; import MarkdownContent from './MarkdownContent'; import TypeLink from './TypeLink'; import DefaultValue from './DefaultValue'; -import { OnClickFieldFunction } from './types'; +import FieldLink from './FieldLink'; -type TypeDocProps = { - onClickField: OnClickFieldFunction; -}; - -export default function TypeDoc(props: TypeDocProps) { +export default function TypeDoc() { const { schema } = useSchemaContext({ nonNull: true }); const { explorerNavStack } = useExplorerContext({ nonNull: true }); const [showDeprecated, setShowDeprecated] = useState(false); @@ -43,8 +39,6 @@ export default function TypeDoc(props: TypeDocProps) { return null; } - const onClickField = props.onClickField; - let typesTitle: string | null = null; let types: readonly (GraphQLObjectType | GraphQLInterfaceType)[] = []; if (type instanceof GraphQLUnionType) { @@ -84,12 +78,7 @@ export default function TypeDoc(props: TypeDocProps) { {fields .filter(field => !field.deprecationReason) .map(field => ( - + ))}
); @@ -111,12 +100,7 @@ export default function TypeDoc(props: TypeDocProps) { ) : ( deprecatedFields.map(field => ( - + )) )}
@@ -185,17 +169,12 @@ export default function TypeDoc(props: TypeDocProps) { type FieldProps = { type: GraphQLNamedType; field: ExplorerFieldDef; - onClickField: OnClickFieldFunction; }; -function Field({ type, field, onClickField }: FieldProps) { +function Field({ field }: FieldProps) { return (
- onClickField(field, type, event)}> - {field.name} - + {'args' in field && field.args && field.args.length > 0 && [ diff --git a/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx b/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx index c9bbd011ae1..0c410b3e3a9 100644 --- a/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx +++ b/packages/graphiql/src/components/DocExplorer/__tests__/TypeDoc.spec.tsx @@ -12,7 +12,7 @@ import { render, } from '@testing-library/react'; import { GraphQLNamedType } from 'graphql'; -import React, { ComponentProps } from 'react'; +import React from 'react'; import { ExampleSchema, @@ -23,9 +23,7 @@ import { import TypeDoc from '../TypeDoc'; import { mockExplorerContextValue, unwrapType } from './test-utils'; -function TypeDocWithContext( - props: ComponentProps & { type: GraphQLNamedType }, -) { +function TypeDocWithContext(props: { type: GraphQLNamedType }) { return ( - + ); @@ -49,9 +47,7 @@ function TypeDocWithContext( describe('TypeDoc', () => { it('renders a top-level query object type', () => { - const { container } = render( - , - ); + const { container } = render(); const description = container.querySelectorAll('.doc-type-description'); expect(description).toHaveLength(1); expect(description[0]).toHaveTextContent('Query description\nSecond line', { @@ -67,9 +63,7 @@ describe('TypeDoc', () => { }); it('renders deprecated fields when you click to see them', () => { - const { container } = render( - , - ); + const { container } = render(); let cats = container.querySelectorAll('.doc-category-item'); expect(cats).toHaveLength(3); @@ -86,18 +80,14 @@ describe('TypeDoc', () => { }); it('renders a Union type', () => { - const { container } = render( - , - ); + const { container } = render(); expect(container.querySelector('.doc-category-title')).toHaveTextContent( 'possible types', ); }); it('renders an Enum type', () => { - const { container } = render( - , - ); + const { container } = render(); expect(container.querySelector('.doc-category-title')).toHaveTextContent( 'values', ); @@ -108,7 +98,7 @@ describe('TypeDoc', () => { it('shows deprecated enum values on click', () => { const { getByText, container } = render( - , + , ); const showBtn = getByText('Show deprecated values...'); expect(showBtn).toBeInTheDocument(); diff --git a/packages/graphiql/src/components/DocExplorer/types.ts b/packages/graphiql/src/components/DocExplorer/types.ts deleted file mode 100644 index fa4dd6148f2..00000000000 --- a/packages/graphiql/src/components/DocExplorer/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { MouseEvent } from 'react'; -import { - GraphQLObjectType, - GraphQLInterfaceType, - GraphQLInputObjectType, - GraphQLType, -} from 'graphql'; -import { ExplorerFieldDef } from '@graphiql/react'; - -export type OnClickFieldFunction = ( - field: ExplorerFieldDef, - type?: - | GraphQLObjectType - | GraphQLInterfaceType - | GraphQLInputObjectType - | GraphQLType, - event?: MouseEvent, -) => void; From 06e4f0a2df1ad0b5f509b93f5daf04a6b26dd7a6 Mon Sep 17 00:00:00 2001 From: Thomas Heyenbrock Date: Sat, 4 Jun 2022 15:53:26 -0400 Subject: [PATCH 5/6] use type assertion functions from graphql-js --- .../graphiql-react/src/editor/completion.ts | 11 ++--- .../src/components/DocExplorer/TypeDoc.tsx | 40 ++++++++++--------- .../src/components/DocExplorer/TypeLink.tsx | 9 ++--- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/packages/graphiql-react/src/editor/completion.ts b/packages/graphiql-react/src/editor/completion.ts index caac87ca805..b8d00e0b88b 100644 --- a/packages/graphiql-react/src/editor/completion.ts +++ b/packages/graphiql-react/src/editor/completion.ts @@ -1,11 +1,6 @@ import type { Editor, EditorChange } from 'codemirror'; import escapeHTML from 'escape-html'; -import { - GraphQLList, - GraphQLNonNull, - GraphQLSchema, - GraphQLType, -} from 'graphql'; +import { GraphQLSchema, GraphQLType, isListType, isNonNullType } from 'graphql'; import { ExplorerContextType } from '../explorer'; import { markdown } from '../markdown'; @@ -117,10 +112,10 @@ export function onHasCompletion( } function renderType(type: GraphQLType): string { - if (type instanceof GraphQLNonNull) { + if (isNonNullType(type)) { return `${renderType(type.ofType)}!`; } - if (type instanceof GraphQLList) { + if (isListType(type)) { return `[${renderType(type.ofType)}]`; } return `${escapeHTML(type.name)}`; diff --git a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx index 1397981c375..8be2fcf533a 100644 --- a/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx +++ b/packages/graphiql/src/components/DocExplorer/TypeDoc.tsx @@ -5,27 +5,29 @@ * LICENSE file in the root directory of this source tree. */ -import React, { ReactNode, useState } from 'react'; -import { - GraphQLObjectType, - GraphQLInterfaceType, - GraphQLUnionType, - GraphQLEnumType, - GraphQLEnumValue, - GraphQLNamedType, - isType, -} from 'graphql'; import { ExplorerFieldDef, useExplorerContext, useSchemaContext, } from '@graphiql/react'; +import { + GraphQLEnumValue, + GraphQLInterfaceType, + GraphQLNamedType, + GraphQLObjectType, + isEnumType, + isInterfaceType, + isNamedType, + isObjectType, + isUnionType, +} from 'graphql'; +import React, { ReactNode, useState } from 'react'; import Argument from './Argument'; -import MarkdownContent from './MarkdownContent'; -import TypeLink from './TypeLink'; import DefaultValue from './DefaultValue'; import FieldLink from './FieldLink'; +import MarkdownContent from './MarkdownContent'; +import TypeLink from './TypeLink'; export default function TypeDoc() { const { schema } = useSchemaContext({ nonNull: true }); @@ -35,19 +37,19 @@ export default function TypeDoc() { const navItem = explorerNavStack[explorerNavStack.length - 1]; const type = navItem.def; - if (!schema || !isType(type)) { + if (!schema || !isNamedType(type)) { return null; } let typesTitle: string | null = null; let types: readonly (GraphQLObjectType | GraphQLInterfaceType)[] = []; - if (type instanceof GraphQLUnionType) { + if (isUnionType(type)) { typesTitle = 'possible types'; types = schema.getPossibleTypes(type); - } else if (type instanceof GraphQLInterfaceType) { + } else if (isInterfaceType(type)) { typesTitle = 'implementations'; types = schema.getPossibleTypes(type); - } else if (type instanceof GraphQLObjectType) { + } else if (isObjectType(type)) { typesTitle = 'implements'; types = type.getInterfaces(); } @@ -110,7 +112,7 @@ export default function TypeDoc() { let valuesDef: ReactNode; let deprecatedValuesDef: ReactNode; - if (type instanceof GraphQLEnumType) { + if (isEnumType(type)) { const values = type.getValues(); valuesDef = (
@@ -156,12 +158,12 @@ export default function TypeDoc() { ('description' in type && type.description) || 'No Description' } /> - {type instanceof GraphQLObjectType && typesDef} + {isObjectType(type) && typesDef} {fieldsDef} {deprecatedFieldsDef} {valuesDef} {deprecatedValuesDef} - {!(type instanceof GraphQLObjectType) && typesDef} + {!isObjectType(type) && typesDef}
); } diff --git a/packages/graphiql/src/components/DocExplorer/TypeLink.tsx b/packages/graphiql/src/components/DocExplorer/TypeLink.tsx index d512ab1dbdb..f87c768c366 100644 --- a/packages/graphiql/src/components/DocExplorer/TypeLink.tsx +++ b/packages/graphiql/src/components/DocExplorer/TypeLink.tsx @@ -5,10 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import { GraphQLList, GraphQLNonNull, GraphQLType } from 'graphql'; - import { useExplorerContext } from '@graphiql/react'; +import { GraphQLType, isListType, isNonNullType } from 'graphql'; +import React from 'react'; type TypeLinkProps = { type: GraphQLType; @@ -22,14 +21,14 @@ export default function TypeLink(props: TypeLinkProps) { } const type = props.type; - if (type instanceof GraphQLNonNull) { + if (isNonNullType(type)) { return ( <> ! ); } - if (type instanceof GraphQLList) { + if (isListType(type)) { return ( <> [] From 19a271a695c026d5575dd066699fcebcb678474a Mon Sep 17 00:00:00 2001 From: Thomas Heyenbrock Date: Tue, 14 Jun 2022 10:20:58 +0200 Subject: [PATCH 6/6] add back schema prop to DocExplorer --- packages/graphiql/src/components/DocExplorer.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/graphiql/src/components/DocExplorer.tsx b/packages/graphiql/src/components/DocExplorer.tsx index f66c485018f..18c39123933 100644 --- a/packages/graphiql/src/components/DocExplorer.tsx +++ b/packages/graphiql/src/components/DocExplorer.tsx @@ -6,7 +6,7 @@ */ import React, { ReactNode } from 'react'; -import { isType } from 'graphql'; +import { GraphQLSchema, isType } from 'graphql'; import { useExplorerContext, useSchemaContext } from '@graphiql/react'; import FieldDoc from './DocExplorer/FieldDoc'; @@ -17,6 +17,14 @@ import TypeDoc from './DocExplorer/TypeDoc'; type DocExplorerProps = { onClose?(): void; + /** + * @deprecated Passing a schema prop directly to this component will be + * removed in the next major version. Instead you need to wrap this component + * with the `SchemaContextProvider` from `@graphiql/react`. This context + * provider accepts a `schema` prop that you can use to skip fetching the + * schema with an introspection request. + */ + schema?: GraphQLSchema | null; }; /** @@ -29,7 +37,7 @@ export function DocExplorer(props: DocExplorerProps) { const { fetchError, isFetching, - schema, + schema: schemaFromContext, validationErrors, } = useSchemaContext({ nonNull: true }); const { explorerNavStack, hide, pop, showSearch } = useExplorerContext({ @@ -38,6 +46,9 @@ export function DocExplorer(props: DocExplorerProps) { const navItem = explorerNavStack[explorerNavStack.length - 1]; + // The schema passed via props takes precedence until we remove the prop + const schema = props.schema === undefined ? schemaFromContext : props.schema; + let content: ReactNode = null; if (fetchError) { content =
Error fetching schema
;