From b4aa4d89132d576ae9d0d7b468f19018e88988f5 Mon Sep 17 00:00:00 2001 From: Thomas Heyenbrock Date: Mon, 30 May 2022 09:23:33 +0200 Subject: [PATCH] always use the latest header values for introspection --- .changeset/friendly-cougars-happen.md | 6 ++ .../graphiql-react/src/editor/context.tsx | 72 ++++++++++++++++++- .../src/editor/header-editor.tsx | 16 ++--- .../src/editor/query-editor.tsx | 57 +++------------ .../src/editor/variable-editor.tsx | 20 +++--- packages/graphiql-react/src/schema.tsx | 20 ++++-- .../graphiql/__mocks__/@graphiql/react.ts | 30 +++++--- packages/graphiql/src/components/GraphiQL.tsx | 56 ++++++--------- 8 files changed, 155 insertions(+), 122 deletions(-) create mode 100644 .changeset/friendly-cougars-happen.md diff --git a/.changeset/friendly-cougars-happen.md b/.changeset/friendly-cougars-happen.md new file mode 100644 index 00000000000..1ba61bd2317 --- /dev/null +++ b/.changeset/friendly-cougars-happen.md @@ -0,0 +1,6 @@ +--- +'graphiql': patch +'@graphiql/react': patch +--- + +Always use the current value of the headers for the introspection request diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 1a8a78bdfb1..4998b248c79 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -2,8 +2,13 @@ import { DocumentNode, OperationDefinitionNode } from 'graphql'; import { VariableToType } from 'graphql-language-service'; import { ReactNode, useMemo, useState } from 'react'; +import { useStorageContext } from '../storage'; import { createContextHook, createNullableContext } from '../utility/context'; +import { STORAGE_KEY as STORAGE_KEY_HEADERS } from './header-editor'; +import { useSynchronizeValue } from './hooks'; +import { STORAGE_KEY_QUERY } from './query-editor'; import { CodeMirrorEditor } from './types'; +import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from './variable-editor'; export type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { documentAST: DocumentNode | null; @@ -21,13 +26,25 @@ export type EditorContextType = { setQueryEditor(newEditor: CodeMirrorEditorWithOperationFacts): void; setResponseEditor(newEditor: CodeMirrorEditor): void; setVariableEditor(newEditor: CodeMirrorEditor): void; + initialHeaders: string; + initialQuery: string; + initialVariables: string; }; export const EditorContext = createNullableContext( 'EditorContext', ); -export function EditorContextProvider(props: { children: ReactNode }) { +type EditorContextProviderProps = { + children: ReactNode; + defaultQuery?: string; + headers?: string; + query?: string; + variables?: string; +}; + +export function EditorContextProvider(props: EditorContextProviderProps) { + const storage = useStorageContext(); const [headerEditor, setHeaderEditor] = useState( null, ); @@ -42,6 +59,23 @@ export function EditorContextProvider(props: { children: ReactNode }) { null, ); + useSynchronizeValue(headerEditor, props.headers); + useSynchronizeValue(queryEditor, props.query); + useSynchronizeValue(variableEditor, props.variables); + + // We store this in state but never update it. By passing a function we only + // need to compute it lazily during the initial render. + const [initialValues] = useState(() => ({ + initialHeaders: props.headers ?? storage?.get(STORAGE_KEY_HEADERS) ?? '', + initialQuery: + props.query ?? + storage?.get(STORAGE_KEY_QUERY) ?? + props.defaultQuery ?? + DEFAULT_QUERY, + initialVariables: + props.variables ?? storage?.get(STORAGE_KEY_VARIABLES) ?? '', + })); + const value = useMemo( () => ({ headerEditor, @@ -52,8 +86,9 @@ export function EditorContextProvider(props: { children: ReactNode }) { setQueryEditor, setResponseEditor, setVariableEditor, + ...initialValues, }), - [headerEditor, queryEditor, responseEditor, variableEditor], + [headerEditor, initialValues, queryEditor, responseEditor, variableEditor], ); return ( @@ -64,3 +99,36 @@ export function EditorContextProvider(props: { children: ReactNode }) { } export const useEditorContext = createContextHook(EditorContext); + +const DEFAULT_QUERY = `# Welcome to GraphiQL +# +# GraphiQL is an in-browser tool for writing, validating, and +# testing GraphQL queries. +# +# Type queries into this side of the screen, and you will see intelligent +# typeaheads aware of the current GraphQL type schema and live syntax and +# validation errors highlighted within the text. +# +# GraphQL queries typically start with a "{" character. Lines that start +# with a # are ignored. +# +# An example GraphQL query might look like: +# +# { +# field(arg: "value") { +# subField +# } +# } +# +# Keyboard shortcuts: +# +# Prettify Query: Shift-Ctrl-P (or press the prettify button above) +# +# Merge Query: Shift-Ctrl-M (or press the merge button above) +# +# Run Query: Ctrl-Enter (or press the play button above) +# +# Auto Complete: Ctrl-Space (or just start typing) +# + +`; diff --git a/packages/graphiql-react/src/editor/header-editor.tsx b/packages/graphiql-react/src/editor/header-editor.tsx index af94b258047..989cfaca4a7 100644 --- a/packages/graphiql-react/src/editor/header-editor.tsx +++ b/packages/graphiql-react/src/editor/header-editor.tsx @@ -1,6 +1,5 @@ import { useEffect, useRef } from 'react'; -import { useStorageContext } from '../storage'; import { commonKeys, importCodeMirror } from './common'; import { useEditorContext } from './context'; import { @@ -12,7 +11,6 @@ import { useMergeQuery, usePrettifyEditors, useResizeEditor, - useSynchronizeValue, } from './hooks'; export type UseHeaderEditorArgs = { @@ -21,7 +19,6 @@ export type UseHeaderEditorArgs = { onRunQuery?: EmptyCallback; readOnly?: boolean; shouldPersistHeaders?: boolean; - value?: string; }; export function useHeaderEditor({ @@ -30,17 +27,14 @@ export function useHeaderEditor({ onRunQuery, readOnly = false, shouldPersistHeaders = false, - value, }: UseHeaderEditorArgs = {}) { - const { headerEditor, setHeaderEditor } = useEditorContext({ + const { initialHeaders, headerEditor, setHeaderEditor } = useEditorContext({ nonNull: true, caller: useHeaderEditor, }); - const storage = useStorageContext(); const merge = useMergeQuery({ caller: useHeaderEditor }); const prettify = usePrettifyEditors({ caller: useHeaderEditor }); const ref = useRef(null); - const initialValue = useRef(value ?? storage?.get(STORAGE_KEY) ?? ''); useEffect(() => { let isActive = true; @@ -60,7 +54,7 @@ export function useHeaderEditor({ } const newEditor = CodeMirror(container, { - value: initialValue.current || '', + value: initialHeaders, lineNumbers: true, tabSize: 2, mode: { name: 'javascript', json: true }, @@ -108,9 +102,7 @@ export function useHeaderEditor({ return () => { isActive = false; }; - }, [editorTheme, readOnly, setHeaderEditor]); - - useSynchronizeValue(headerEditor, value); + }, [editorTheme, initialHeaders, readOnly, setHeaderEditor]); useChangeHandler( headerEditor, @@ -129,4 +121,4 @@ export function useHeaderEditor({ return ref; } -const STORAGE_KEY = 'headers'; +export const STORAGE_KEY = 'headers'; diff --git a/packages/graphiql-react/src/editor/query-editor.tsx b/packages/graphiql-react/src/editor/query-editor.tsx index 6c6b0709c7e..d07581e5ba7 100644 --- a/packages/graphiql-react/src/editor/query-editor.tsx +++ b/packages/graphiql-react/src/editor/query-editor.tsx @@ -28,7 +28,6 @@ import { useMergeQuery, usePrettifyEditors, useResizeEditor, - useSynchronizeValue, } from './hooks'; import { CodeMirrorEditor, CodeMirrorType } from './types'; import { normalizeWhitespace } from './whitespace'; @@ -36,7 +35,6 @@ import { normalizeWhitespace } from './whitespace'; type OnClickReference = (reference: SchemaReference) => void; export type UseQueryEditorArgs = { - defaultValue?: string; editorTheme?: string; externalFragments?: string | FragmentDefinitionNode[]; onEdit?: EditCallback; @@ -45,11 +43,9 @@ export type UseQueryEditorArgs = { onRunQuery?: EmptyCallback; readOnly?: boolean; validationRules?: ValidationRule[]; - value?: string; }; export function useQueryEditor({ - defaultValue = DEFAULT_VALUE, editorTheme = 'graphiql', externalFragments, onEdit, @@ -58,13 +54,17 @@ export function useQueryEditor({ onRunQuery, readOnly = false, validationRules, - value, }: UseQueryEditorArgs = {}) { const { schema } = useSchemaContext({ nonNull: true, caller: useQueryEditor, }); - const { queryEditor, setQueryEditor, variableEditor } = useEditorContext({ + const { + initialQuery, + queryEditor, + setQueryEditor, + variableEditor, + } = useEditorContext({ nonNull: true, caller: useQueryEditor, }); @@ -95,10 +95,6 @@ export function useQueryEditor({ }; }, [explorer]); - const initialValue = useRef( - value ?? storage?.get(STORAGE_KEY_QUERY) ?? defaultValue, - ); - useEffect(() => { let isActive = true; @@ -124,7 +120,7 @@ export function useQueryEditor({ } const newEditor = CodeMirror(container, { - value: initialValue.current || '', + value: initialQuery, lineNumbers: true, tabSize: 2, foldGutter: true, @@ -218,7 +214,7 @@ export function useQueryEditor({ return () => { isActive = false; }; - }, [editorTheme, readOnly, setQueryEditor]); + }, [editorTheme, initialQuery, readOnly, setQueryEditor]); /** * We don't use the generic `useChangeHandler` hook here because we want to @@ -320,8 +316,6 @@ export function useQueryEditor({ codeMirrorRef, ); - useSynchronizeValue(queryEditor, value); - useCompletion(queryEditor); useKeyMap(queryEditor, ['Cmd-Enter', 'Ctrl-Enter'], onRunQuery); @@ -412,39 +406,6 @@ function useSynchronizeExternalFragments( const AUTO_COMPLETE_AFTER_KEY = /^[a-zA-Z0-9_@(]$/; -const DEFAULT_VALUE = `# Welcome to GraphiQL -# -# GraphiQL is an in-browser tool for writing, validating, and -# testing GraphQL queries. -# -# Type queries into this side of the screen, and you will see intelligent -# typeaheads aware of the current GraphQL type schema and live syntax and -# validation errors highlighted within the text. -# -# GraphQL queries typically start with a "{" character. Lines that start -# with a # are ignored. -# -# An example GraphQL query might look like: -# -# { -# field(arg: "value") { -# subField -# } -# } -# -# Keyboard shortcuts: -# -# Prettify Query: Shift-Ctrl-P (or press the prettify button above) -# -# Merge Query: Shift-Ctrl-M (or press the merge button above) -# -# Run Query: Ctrl-Enter (or press the play button above) -# -# Auto Complete: Ctrl-Space (or just start typing) -# - -`; - -const STORAGE_KEY_QUERY = 'query'; +export const STORAGE_KEY_QUERY = 'query'; const STORAGE_KEY_OPERATION_NAME = 'operationName'; diff --git a/packages/graphiql-react/src/editor/variable-editor.tsx b/packages/graphiql-react/src/editor/variable-editor.tsx index 37f62dbd97d..5ae980193f7 100644 --- a/packages/graphiql-react/src/editor/variable-editor.tsx +++ b/packages/graphiql-react/src/editor/variable-editor.tsx @@ -1,6 +1,5 @@ import { useEffect, useRef } from 'react'; -import { useStorageContext } from '../storage'; import { commonKeys, importCodeMirror } from './common'; import { useEditorContext } from './context'; import { @@ -12,7 +11,6 @@ import { useMergeQuery, usePrettifyEditors, useResizeEditor, - useSynchronizeValue, } from './hooks'; import { CodeMirrorType } from './types'; @@ -21,7 +19,6 @@ export type UseVariableEditorArgs = { onEdit?: EditCallback; onRunQuery?: EmptyCallback; readOnly?: boolean; - value?: string; }; export function useVariableEditor({ @@ -29,18 +26,19 @@ export function useVariableEditor({ onEdit, onRunQuery, readOnly = false, - value, }: UseVariableEditorArgs = {}) { - const { variableEditor, setVariableEditor } = useEditorContext({ + const { + initialVariables, + variableEditor, + setVariableEditor, + } = useEditorContext({ nonNull: true, caller: useVariableEditor, }); - const storage = useStorageContext(); const merge = useMergeQuery({ caller: useVariableEditor }); const prettify = usePrettifyEditors({ caller: useVariableEditor }); const ref = useRef(null); const codeMirrorRef = useRef(); - const initialValue = useRef(value ?? storage?.get(STORAGE_KEY) ?? ''); useEffect(() => { let isActive = true; @@ -63,7 +61,7 @@ export function useVariableEditor({ } const newEditor = CodeMirror(container, { - value: initialValue.current || '', + value: initialVariables, lineNumbers: true, tabSize: 2, mode: 'graphql-variables', @@ -122,9 +120,7 @@ export function useVariableEditor({ return () => { isActive = false; }; - }, [editorTheme, readOnly, setVariableEditor]); - - useSynchronizeValue(variableEditor, value); + }, [editorTheme, initialVariables, readOnly, setVariableEditor]); useChangeHandler(variableEditor, onEdit, STORAGE_KEY); @@ -139,4 +135,4 @@ export function useVariableEditor({ return ref; } -const STORAGE_KEY = 'variables'; +export const STORAGE_KEY = 'variables'; diff --git a/packages/graphiql-react/src/schema.tsx b/packages/graphiql-react/src/schema.tsx index 48c5d81afde..7fa4197670e 100644 --- a/packages/graphiql-react/src/schema.tsx +++ b/packages/graphiql-react/src/schema.tsx @@ -24,6 +24,7 @@ import { useState, } from 'react'; +import { useEditorContext } from './editor'; import { createContextHook, createNullableContext } from './utility/context'; /** @@ -53,11 +54,14 @@ type SchemaContextProviderProps = { children: ReactNode; dangerouslyAssumeSchemaIsValid?: boolean; fetcher: Fetcher; - initialHeaders?: string; schema?: GraphQLSchema | null; } & IntrospectionArgs; export function SchemaContextProvider(props: SchemaContextProviderProps) { + const { initialHeaders, headerEditor } = useEditorContext({ + nonNull: true, + caller: SchemaContextProvider, + }); const [schema, setSchema] = useState( props.schema || null, ); @@ -74,7 +78,12 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { /** * Keep a ref to the current headers */ - const headersRef = useRef(parseHeaderString(props.initialHeaders)); + const headersRef = useRef(initialHeaders); + useEffect(() => { + if (headerEditor) { + headersRef.current = headerEditor.getValue(); + } + }); /** * Get introspection query for settings given via props @@ -101,13 +110,14 @@ export function SchemaContextProvider(props: SchemaContextProviderProps) { let isActive = true; - if (!headersRef.current.isValidJSON) { + const parsedHeaders = parseHeaderString(headersRef.current); + if (!parsedHeaders.isValidJSON) { setFetchError('Introspection failed as headers are invalid.'); return; } - const fetcherOpts: FetcherOpts = headersRef.current.headers - ? { headers: headersRef.current.headers } + const fetcherOpts: FetcherOpts = parsedHeaders.headers + ? { headers: parsedHeaders.headers } : {}; const fetch = fetcherReturnToPromise( diff --git a/packages/graphiql/__mocks__/@graphiql/react.ts b/packages/graphiql/__mocks__/@graphiql/react.ts index e03ce1038e3..f626850524d 100644 --- a/packages/graphiql/__mocks__/@graphiql/react.ts +++ b/packages/graphiql/__mocks__/@graphiql/react.ts @@ -82,13 +82,27 @@ export type { UseVariableEditorArgs, }; +type Name = 'query' | 'variable' | 'header' | 'response'; + +const NAME_TO_INITIAL_VALUE: Record< + Name, + 'initialQuery' | 'initialVariables' | 'initialHeaders' | undefined +> = { + query: 'initialQuery', + variable: 'initialVariables', + header: 'initialHeaders', + response: undefined, +}; + function useMockedEditor( - name: string, + name: Name, value?: string, onEdit?: (newValue: string) => void, - defaultValue?: string, ) { - const [code, setCode] = useState(value ?? defaultValue); + const editorContext = useEditorContext({ nonNull: true }); + const [code, setCode] = useState( + value ?? editorContext[NAME_TO_INITIAL_VALUE[name]], + ); const ref = useRef(null); const context = useEditorContext({ nonNull: true }); @@ -175,17 +189,14 @@ function useMockedEditor( export const useHeaderEditor: typeof _useHeaderEditor = function useHeaderEditor({ onEdit, - value, }) { - return useMockedEditor('header', value, onEdit); + return useMockedEditor('header', undefined, onEdit); }; export const useQueryEditor: typeof _useQueryEditor = function useQueryEditor({ - defaultValue = '# Welcome to GraphiQL', onEdit, - value, }) { - return useMockedEditor('query', value, onEdit, defaultValue); + return useMockedEditor('query', undefined, onEdit); }; export const useResponseEditor: typeof _useResponseEditor = function useResponseEditor({ @@ -196,7 +207,6 @@ export const useResponseEditor: typeof _useResponseEditor = function useResponse export const useVariableEditor: typeof _useVariableEditor = function useVariableEditor({ onEdit, - value, }) { - return useMockedEditor('variable', value, onEdit); + return useMockedEditor('variable', undefined, onEdit); }; diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index cb3ddd8528a..17069cc0986 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -27,7 +27,6 @@ import { ExplorerContextProvider, HistoryContextProvider, SchemaContextProvider, - StorageContext, StorageContextProvider, useAutoCompleteLeafs, useCopyQuery, @@ -366,34 +365,29 @@ export function GraphiQL({ }: GraphiQLProps) { return ( - - {storageContext => ( - - - - - - - - - - )} - + + + + + + + + + ); } @@ -865,7 +859,6 @@ class GraphiQLWithContext extends React.Component< onMouseDown={this.handleResizeStart}>
)}