From b6539d9c2ed0cd0c2787cdb3eeb2556a50ab8609 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 21 May 2020 17:59:05 -0600 Subject: [PATCH 01/28] Add a header editor to the GraphiQL Interface. It doesn't actually send the headers, and has some weird behavior right now. --- packages/graphiql/src/components/GraphiQL.tsx | 144 +++++++++++ .../graphiql/src/components/HeaderEditor.tsx | 234 ++++++++++++++++++ packages/graphiql/src/css/app.css | 6 +- 3 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 packages/graphiql/src/components/HeaderEditor.tsx diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 566bb350cf7..eaddea96082 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -30,6 +30,7 @@ import { ToolbarGroup } from './ToolbarGroup'; import { ToolbarMenu, ToolbarMenuItem } from './ToolbarMenu'; import { QueryEditor } from './QueryEditor'; import { VariableEditor } from './VariableEditor'; +import { HeaderEditor } from './HeaderEditor'; import { ResultViewer } from './ResultViewer'; import { DocExplorer } from './DocExplorer'; import { QueryHistory } from './QueryHistory'; @@ -72,6 +73,7 @@ export type FetcherParams = { query: string; operationName: string; variables?: string; + headers?: string; }; export type FetcherResult = | { @@ -94,14 +96,17 @@ export type GraphiQLProps = { schema?: GraphQLSchema; query?: string; variables?: string; + headers?: string; operationName?: string; response?: string; storage?: Storage; defaultQuery?: string; defaultVariableEditorOpen?: boolean; + defaultHeaderEditorOpen?: boolean; onCopyQuery?: (query?: string) => void; onEditQuery?: (query?: string) => void; onEditVariables?: (value: string) => void; + onEditHeaders?: (value: string) => void; onEditOperationName?: (operationName: string) => void; onToggleDocs?: (docExplorerOpen: boolean) => void; getDefaultFieldNames?: GetDefaultFieldNamesFn; @@ -116,12 +121,15 @@ export type GraphiQLState = { schema?: GraphQLSchema; query?: string; variables?: string; + headers?: string; operationName?: string; docExplorerOpen: boolean; response?: string; editorFlex: number; variableEditorOpen: boolean; variableEditorHeight: number; + headerEditorOpen: boolean; + headerEditorHeight: number; historyPaneOpen: boolean; docExplorerWidth: number; isWaitingForResponse: boolean; @@ -162,6 +170,7 @@ export class GraphiQL extends React.Component { graphiqlContainer: Maybe; resultComponent: Maybe; variableEditorComponent: Maybe; + headerEditorComponent: Maybe; _queryHistory: Maybe; editorBarComponent: Maybe; queryEditorComponent: Maybe; @@ -197,6 +206,12 @@ export class GraphiQL extends React.Component { ? props.variables : this._storage.get('variables'); + // Determine the initial headers to display. + const headers = + props.headers !== undefined + ? props.headers + : this._storage.get('headers'); + // Determine the initial operationName to use. const operationName = props.operationName !== undefined @@ -221,6 +236,12 @@ export class GraphiQL extends React.Component { ? props.defaultVariableEditorOpen : Boolean(variables); + // initial header editor pane open + const headerEditorOpen = + props.defaultHeaderEditorOpen !== undefined + ? props.defaultHeaderEditorOpen + : Boolean(headers); + // Initialize state this.state = { schema: props.schema, @@ -233,6 +254,9 @@ export class GraphiQL extends React.Component { variableEditorOpen, variableEditorHeight: Number(this._storage.get('variableEditorHeight')) || 200, + headerEditorOpen, + headerEditorHeight: + Number(this._storage.get('headerEditorHeight')) || 200, historyPaneOpen: this._storage.get('historyPaneOpen') === 'true' || false, docExplorerWidth: Number(this._storage.get('docExplorerWidth')) || @@ -268,6 +292,7 @@ export class GraphiQL extends React.Component { let nextSchema = this.state.schema; let nextQuery = this.state.query; let nextVariables = this.state.variables; + let nextHeaders = this.state.headers; let nextOperationName = this.state.operationName; let nextResponse = this.state.response; @@ -280,6 +305,9 @@ export class GraphiQL extends React.Component { if (nextProps.variables !== undefined) { nextVariables = nextProps.variables; } + if (nextProps.headers !== undefined) { + nextHeaders = nextProps.headers; + } if (nextProps.operationName !== undefined) { nextOperationName = nextProps.operationName; } @@ -321,6 +349,7 @@ export class GraphiQL extends React.Component { schema: nextSchema, query: nextQuery, variables: nextVariables, + headers: nextHeaders, operationName: nextOperationName, response: nextResponse, }, @@ -342,6 +371,7 @@ export class GraphiQL extends React.Component { this.codeMirrorSizer.updateSizes([ this.queryEditorComponent, this.variableEditorComponent, + this.headerEditorComponent, this.resultComponent, ]); } @@ -355,6 +385,9 @@ export class GraphiQL extends React.Component { if (this.state.variables) { this._storage.set('variables', this.state.variables); } + if (this.state.headers) { + this._storage.set('headers', this.state.headers); + } if (this.state.operationName) { this._storage.set('operationName', this.state.operationName); } @@ -363,6 +396,10 @@ export class GraphiQL extends React.Component { 'variableEditorHeight', JSON.stringify(this.state.variableEditorHeight), ); + this._storage.set( + 'headerEditorHeight', + JSON.stringify(this.state.headerEditorHeight), + ); this._storage.set( 'docExplorerWidth', JSON.stringify(this.state.docExplorerWidth), @@ -439,6 +476,11 @@ export class GraphiQL extends React.Component { height: variableOpen ? this.state.variableEditorHeight : undefined, }; + const headerOpen = this.state.headerEditorOpen; + const headerStyle = { + height: headerOpen ? this.state.headerEditorHeight : undefined, + }; + return (
{ @@ -537,6 +579,33 @@ export class GraphiQL extends React.Component { readOnly={this.props.readOnly} /> +
+
+ {'Query Headers'} +
+ { + this.headerEditorComponent = n; + }} + value={this.state.headers} + onEdit={this.handleEditHeaders} + onHintInformationRender={this.handleHintInformationRender} + onPrettifyQuery={this.handlePrettifyQuery} + onMergeQuery={this.handleMergeQuery} + onRunQuery={this.handleEditorRunQuery} + editorTheme={this.props.editorTheme} + readOnly={this.props.readOnly} + /> +
{this.state.isWaitingForResponse && ( @@ -591,6 +660,7 @@ export class GraphiQL extends React.Component { static Footer = GraphiQLFooter; static QueryEditor = QueryEditor; static VariableEditor = VariableEditor; + static HeaderEditor = HeaderEditor; static ResultViewer = ResultViewer; // Add a button to the Toolbar. @@ -644,6 +714,9 @@ export class GraphiQL extends React.Component { if (this.variableEditorComponent) { this.variableEditorComponent.getCodeMirror().refresh(); } + if (this.headerEditorComponent) { + this.headerEditorComponent.getCodeMirror().refresh(); + } if (this.resultComponent) { this.resultComponent.getCodeMirror().refresh(); } @@ -767,11 +840,13 @@ export class GraphiQL extends React.Component { private _fetchQuery( query: string, variables: string, + headers: string, operationName: string, cb: (value: FetcherResult) => any, ) { const fetcher = this.props.fetcher; let jsonVariables = null; + let jsonHeaders = null; try { jsonVariables = @@ -784,9 +859,21 @@ export class GraphiQL extends React.Component { throw new Error('Variables are not a JSON object.'); } + try { + jsonHeaders = + headers && headers.trim() !== '' ? JSON.parse(headers) : null; + } catch (error) { + throw new Error(`Headers are invalid JSON: ${error.message}.`); + } + + if (typeof jsonHeaders !== 'object') { + throw new Error('Headers are not a JSON object.'); + } + const fetch = fetcher({ query, variables: jsonVariables, + headers: jsonHeaders, operationName, }); @@ -843,6 +930,7 @@ export class GraphiQL extends React.Component { // the current query from the editor. const editedQuery = this.autoCompleteLeafs() || this.state.query; const variables = this.state.variables; + const headers = this.state.headers; let operationName = this.state.operationName; // If an operation was explicitly provided, different from the current @@ -867,6 +955,7 @@ export class GraphiQL extends React.Component { const subscription = this._fetchQuery( editedQuery as string, variables as string, + headers as string, operationName as string, (result: FetcherResult) => { if (queryID === this._editorQueryID) { @@ -1038,6 +1127,13 @@ export class GraphiQL extends React.Component { } }; + handleEditHeaders = (value: string) => { + this.setState({ headers: value }); + if (this.props.onEditHeaders) { + this.props.onEditHeaders(value); + } + }; + handleEditOperationName = (operationName: string) => { const onEditOperationName = this.props.onEditOperationName; if (onEditOperationName) { @@ -1266,6 +1362,54 @@ export class GraphiQL extends React.Component { document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; + + private handleHeaderResizeStart: MouseEventHandler< + HTMLDivElement + > = downEvent => { + downEvent.preventDefault(); + + let didMove = false; + const wasOpen = this.state.headerEditorOpen; + const hadHeight = this.state.headerEditorHeight; + const offset = downEvent.clientY - getTop(downEvent.target as HTMLElement); + + let onMouseMove: OnMouseMoveFn = moveEvent => { + if (moveEvent.buttons === 0) { + return onMouseUp!(); + } + + didMove = true; + + const editorBar = this.editorBarComponent as HTMLElement; + const topSize = moveEvent.clientY - getTop(editorBar) - offset; + const bottomSize = editorBar.clientHeight - topSize; + if (bottomSize < 60) { + this.setState({ + headerEditorOpen: false, + headerEditorHeight: hadHeight, + }); + } else { + this.setState({ + headerEditorOpen: true, + headerEditorHeight: bottomSize, + }); + } + }; + + let onMouseUp: OnMouseUpFn = () => { + if (!didMove) { + this.setState({ headerEditorOpen: !wasOpen }); + } + + document.removeEventListener('mousemove', onMouseMove!); + document.removeEventListener('mouseup', onMouseUp!); + onMouseMove = null; + onMouseUp = null; + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }; } // // Configure the UI by providing this Component as a child of GraphiQL. diff --git a/packages/graphiql/src/components/HeaderEditor.tsx b/packages/graphiql/src/components/HeaderEditor.tsx new file mode 100644 index 00000000000..3a3a7594b99 --- /dev/null +++ b/packages/graphiql/src/components/HeaderEditor.tsx @@ -0,0 +1,234 @@ +/** + * Copyright (c) 2019 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 * as CM from 'codemirror'; +import 'codemirror/addon/hint/show-hint'; +import React from 'react'; + +import onHasCompletion from '../utility/onHasCompletion'; +import commonKeys from '../utility/commonKeys'; + +declare module CodeMirror { + export interface Editor extends CM.Editor {} + export interface ShowHintOptions { + completeSingle: boolean; + hint: CM.HintFunction | CM.AsyncHintFunction; + container: HTMLElement | null; + } +} + +type HeaderEditorProps = { + value?: string; + onEdit: (value: string) => void; + readOnly?: boolean; + onHintInformationRender: (value: HTMLDivElement) => void; + onPrettifyQuery: (value?: string) => void; + onMergeQuery: (value?: string) => void; + onRunQuery: (value?: string) => void; + editorTheme?: string; +}; + +/** + * HeaderEditor + * + * An instance of CodeMirror for editing headers to be passed with the GraphQL request. + * + * Props: + * + * - value: The text of the editor. + * - onEdit: A function called when the editor changes, given the edited text. + * - readOnly: Turns the editor to read-only mode. + * + */ +export class HeaderEditor extends React.Component { + CodeMirror: any; + editor: (CM.Editor & { options: any }) | null = null; + cachedValue: string; + private _node: HTMLElement | null = null; + ignoreChangeEvent: boolean; + constructor(props: HeaderEditorProps) { + super(props); + + // Keep a cached version of the value, this cache will be updated when the + // editor is updated, which can later be used to protect the editor from + // unnecessary updates during the update lifecycle. + this.cachedValue = props.value || ''; + this.ignoreChangeEvent = true; + } + + componentDidMount() { + // Lazily require to ensure requiring GraphiQL outside of a Browser context + // does not produce an error. + this.CodeMirror = require('codemirror'); + require('codemirror/addon/hint/show-hint'); + require('codemirror/addon/edit/matchbrackets'); + require('codemirror/addon/edit/closebrackets'); + require('codemirror/addon/fold/brace-fold'); + require('codemirror/addon/fold/foldgutter'); + require('codemirror/addon/lint/lint'); + require('codemirror/addon/search/searchcursor'); + require('codemirror/addon/search/jump-to-line'); + require('codemirror/addon/dialog/dialog'); + require('codemirror/keymap/sublime'); + + const editor = (this.editor = this.CodeMirror(this._node, { + value: this.props.value || '', + lineNumbers: true, + tabSize: 2, + mode: 'graphql-headers', + theme: this.props.editorTheme || 'graphiql', + keyMap: 'sublime', + autoCloseBrackets: true, + matchBrackets: true, + showCursorWhenSelecting: true, + readOnly: this.props.readOnly ? 'nocursor' : false, + foldGutter: { + minFoldSize: 4, + }, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + extraKeys: { + 'Cmd-Space': () => + this.editor!.showHint({ + completeSingle: false, + container: this._node, + } as CodeMirror.ShowHintOptions), + 'Ctrl-Space': () => + this.editor!.showHint({ + completeSingle: false, + container: this._node, + } as CodeMirror.ShowHintOptions), + 'Alt-Space': () => + this.editor!.showHint({ + completeSingle: false, + container: this._node, + } as CodeMirror.ShowHintOptions), + 'Shift-Space': () => + this.editor!.showHint({ + completeSingle: false, + container: this._node, + } as CodeMirror.ShowHintOptions), + 'Cmd-Enter': () => { + if (this.props.onRunQuery) { + this.props.onRunQuery(); + } + }, + 'Ctrl-Enter': () => { + if (this.props.onRunQuery) { + this.props.onRunQuery(); + } + }, + 'Shift-Ctrl-P': () => { + if (this.props.onPrettifyQuery) { + this.props.onPrettifyQuery(); + } + }, + + 'Shift-Ctrl-M': () => { + if (this.props.onMergeQuery) { + this.props.onMergeQuery(); + } + }, + + ...commonKeys, + }, + })); + + editor.on('change', this._onEdit); + editor.on('keyup', this._onKeyUp); + editor.on('hasCompletion', this._onHasCompletion); + } + + componentDidUpdate(prevProps: HeaderEditorProps) { + this.CodeMirror = require('codemirror'); + if (!this.editor) { + return; + } + + // Ensure the changes caused by this update are not interpretted as + // user-input changes which could otherwise result in an infinite + // event loop. + this.ignoreChangeEvent = true; + if ( + this.props.value !== prevProps.value && + this.props.value !== this.cachedValue + ) { + const thisValue = this.props.value || ''; + this.cachedValue = thisValue; + this.editor.setValue(thisValue); + } + this.ignoreChangeEvent = false; + } + + componentWillUnmount() { + if (!this.editor) { + return; + } + this.editor.off('change', this._onEdit); + this.editor.off('keyup', this._onKeyUp); + this.editor.off('hasCompletion', this._onHasCompletion); + this.editor = null; + } + + render() { + return ( +
{ + this._node = node as HTMLDivElement; + }} + /> + ); + } + + /** + * Public API for retrieving the CodeMirror instance from this + * React component. + */ + getCodeMirror() { + return this.editor as CM.Editor; + } + + /** + * Public API for retrieving the DOM client height for this component. + */ + getClientHeight() { + return this._node && this._node.clientHeight; + } + + private _onKeyUp = (_cm: CodeMirror.Editor, event: KeyboardEvent) => { + const code = event.keyCode; + if (!this.editor) { + return; + } + if ( + (code >= 65 && code <= 90) || // letters + (!event.shiftKey && code >= 48 && code <= 57) || // numbers + (event.shiftKey && code === 189) || // underscore + (event.shiftKey && code === 222) // " + ) { + this.editor.execCommand('autocomplete'); + } + }; + + private _onEdit = () => { + if (!this.editor) { + return; + } + if (!this.ignoreChangeEvent) { + this.cachedValue = this.editor.getValue(); + if (this.props.onEdit) { + this.props.onEdit(this.cachedValue); + } + } + }; + + private _onHasCompletion = ( + instance: CM.Editor, + changeObj?: CM.EditorChangeLinkedList, + ) => { + onHasCompletion(instance, changeObj, this.props.onHintInformationRender); + }; +} diff --git a/packages/graphiql/src/css/app.css b/packages/graphiql/src/css/app.css index f4294a52ac8..00e97af7a21 100644 --- a/packages/graphiql/src/css/app.css +++ b/packages/graphiql/src/css/app.css @@ -161,14 +161,16 @@ position: relative; } -.graphiql-container .variable-editor { +.graphiql-container .variable-editor, +.graphiql-container .header-editor { display: flex; flex-direction: column; height: 30px; position: relative; } -.graphiql-container .variable-editor-title { +.graphiql-container .variable-editor-title, +.graphiql-container .header-editor-title { background: #eeeeee; border-bottom: 1px solid #d6d6d6; border-top: 1px solid #e0e0e0; From d8acb7d1bfaf2b94ac9554940d9c0d84837e7fd3 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 21 May 2020 18:42:43 -0600 Subject: [PATCH 02/28] Move the header and variable editors into tabs. --- packages/graphiql/src/components/GraphiQL.tsx | 252 +++++++++--------- packages/graphiql/src/css/app.css | 6 +- 2 files changed, 123 insertions(+), 135 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index eaddea96082..2c17967a858 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -101,13 +101,14 @@ export type GraphiQLProps = { response?: string; storage?: Storage; defaultQuery?: string; - defaultVariableEditorOpen?: boolean; - defaultHeaderEditorOpen?: boolean; + defaultExtraEditorOpen?: boolean; onCopyQuery?: (query?: string) => void; onEditQuery?: (query?: string) => void; onEditVariables?: (value: string) => void; onEditHeaders?: (value: string) => void; onEditOperationName?: (operationName: string) => void; + onToggleVariableEditor?: (variableEditorActive: boolean) => void; + onToggleHeaderEditor?: (headerEditorActive: boolean) => void; onToggleDocs?: (docExplorerOpen: boolean) => void; getDefaultFieldNames?: GetDefaultFieldNamesFn; editorTheme?: string; @@ -126,10 +127,10 @@ export type GraphiQLState = { docExplorerOpen: boolean; response?: string; editorFlex: number; - variableEditorOpen: boolean; - variableEditorHeight: number; - headerEditorOpen: boolean; - headerEditorHeight: number; + extraEditorOpen: boolean; + extraEditorHeight: number; + variableEditorActive: boolean; + headerEditorActive: boolean; historyPaneOpen: boolean; docExplorerWidth: number; isWaitingForResponse: boolean; @@ -230,17 +231,11 @@ export class GraphiQL extends React.Component { docExplorerOpen = this._storage.get('docExplorerOpen') === 'true'; } - // initial variable editor pane open - const variableEditorOpen = - props.defaultVariableEditorOpen !== undefined - ? props.defaultVariableEditorOpen - : Boolean(variables); - - // initial header editor pane open - const headerEditorOpen = - props.defaultHeaderEditorOpen !== undefined - ? props.defaultHeaderEditorOpen - : Boolean(headers); + // initial extra editor pane open + const extraEditorOpen = + props.defaultExtraEditorOpen !== undefined + ? props.defaultExtraEditorOpen + : Boolean(variables || headers); // Initialize state this.state = { @@ -251,12 +246,11 @@ export class GraphiQL extends React.Component { docExplorerOpen, response: props.response, editorFlex: Number(this._storage.get('editorFlex')) || 1, - variableEditorOpen, - variableEditorHeight: - Number(this._storage.get('variableEditorHeight')) || 200, - headerEditorOpen, - headerEditorHeight: - Number(this._storage.get('headerEditorHeight')) || 200, + extraEditorOpen, + extraEditorHeight: Number(this._storage.get('extraEditorHeight')) || 200, + variableEditorActive: + this._storage.get('variableEditorActive') === 'true', + headerEditorActive: this._storage.get('headerEditorActive') === 'true', historyPaneOpen: this._storage.get('historyPaneOpen') === 'true' || false, docExplorerWidth: Number(this._storage.get('docExplorerWidth')) || @@ -393,12 +387,16 @@ export class GraphiQL extends React.Component { } this._storage.set('editorFlex', JSON.stringify(this.state.editorFlex)); this._storage.set( - 'variableEditorHeight', - JSON.stringify(this.state.variableEditorHeight), + 'extraEditorHeight', + JSON.stringify(this.state.extraEditorHeight), + ); + this._storage.set( + 'variableEditorActive', + JSON.stringify(this.state.variableEditorActive), ); this._storage.set( - 'headerEditorHeight', - JSON.stringify(this.state.headerEditorHeight), + 'headerEditorActive', + JSON.stringify(this.state.headerEditorActive), ); this._storage.set( 'docExplorerWidth', @@ -471,15 +469,13 @@ export class GraphiQL extends React.Component { zIndex: 7, }; - const variableOpen = this.state.variableEditorOpen; - const variableStyle = { - height: variableOpen ? this.state.variableEditorHeight : undefined, + const extraEditorOpen = this.state.extraEditorOpen; + const extraEditorStyle = { + height: extraEditorOpen ? this.state.extraEditorHeight : undefined, }; - const headerOpen = this.state.headerEditorOpen; - const headerStyle = { - height: headerOpen ? this.state.headerEditorHeight : undefined, - }; + const variableEditorActive = this.state.variableEditorActive; + const headerEditorActive = this.state.headerEditorActive; return (
{ readOnly={this.props.readOnly} />
- {'Query Variables'} -
- { - this.variableEditorComponent = n; - }} - value={this.state.variables} - variableToType={this.state.variableToType} - onEdit={this.handleEditVariables} - onHintInformationRender={this.handleHintInformationRender} - onPrettifyQuery={this.handlePrettifyQuery} - onMergeQuery={this.handleMergeQuery} - onRunQuery={this.handleEditorRunQuery} - editorTheme={this.props.editorTheme} - readOnly={this.props.readOnly} - /> -
-
-
- {'Query Headers'} + onMouseDown={this.handleExtraEditorResizeStart}> +
+ {'Query Variables'} +
+
+ {'Query Headers'} +
- { - this.headerEditorComponent = n; - }} - value={this.state.headers} - onEdit={this.handleEditHeaders} - onHintInformationRender={this.handleHintInformationRender} - onPrettifyQuery={this.handlePrettifyQuery} - onMergeQuery={this.handleMergeQuery} - onRunQuery={this.handleEditorRunQuery} - editorTheme={this.props.editorTheme} - readOnly={this.props.readOnly} - /> + {variableEditorActive && ( + { + this.variableEditorComponent = n; + }} + value={this.state.variables} + variableToType={this.state.variableToType} + onEdit={this.handleEditVariables} + onHintInformationRender={this.handleHintInformationRender} + onPrettifyQuery={this.handlePrettifyQuery} + onMergeQuery={this.handleMergeQuery} + onRunQuery={this.handleEditorRunQuery} + editorTheme={this.props.editorTheme} + readOnly={this.props.readOnly} + /> + )} + {headerEditorActive && ( + { + this.headerEditorComponent = n; + }} + value={this.state.headers} + onEdit={this.handleEditHeaders} + onHintInformationRender={this.handleHintInformationRender} + onPrettifyQuery={this.handlePrettifyQuery} + onMergeQuery={this.handleMergeQuery} + onRunQuery={this.handleEditorRunQuery} + editorTheme={this.props.editorTheme} + readOnly={this.props.readOnly} + /> + )}
@@ -1195,6 +1201,13 @@ export class GraphiQL extends React.Component { this.setState({ historyPaneOpen: !this.state.historyPaneOpen }); }; + handleToggleVariableEditor = () => { + if (typeof this.props.onToggleVariableEditor === 'function') { + this.props.onToggleVariableEditor(!this.state.variableEditorActive); + } + this.setState({ variableEditorActive: !this.state.variableEditorActive }); + }; + handleSelectHistoryQuery = ( query?: string, variables?: string, @@ -1315,62 +1328,39 @@ export class GraphiQL extends React.Component { }); }; - private handleVariableResizeStart: MouseEventHandler< + private handleTabClickPropogation: MouseEventHandler< HTMLDivElement > = downEvent => { downEvent.preventDefault(); + downEvent.stopPropagation(); + }; - let didMove = false; - const wasOpen = this.state.variableEditorOpen; - const hadHeight = this.state.variableEditorHeight; - const offset = downEvent.clientY - getTop(downEvent.target as HTMLElement); - - let onMouseMove: OnMouseMoveFn = moveEvent => { - if (moveEvent.buttons === 0) { - return onMouseUp!(); - } - - didMove = true; - - const editorBar = this.editorBarComponent as HTMLElement; - const topSize = moveEvent.clientY - getTop(editorBar) - offset; - const bottomSize = editorBar.clientHeight - topSize; - if (bottomSize < 60) { - this.setState({ - variableEditorOpen: false, - variableEditorHeight: hadHeight, - }); - } else { - this.setState({ - variableEditorOpen: true, - variableEditorHeight: bottomSize, - }); - } - }; - - let onMouseUp: OnMouseUpFn = () => { - if (!didMove) { - this.setState({ variableEditorOpen: !wasOpen }); - } - - document.removeEventListener('mousemove', onMouseMove!); - document.removeEventListener('mouseup', onMouseUp!); - onMouseMove = null; - onMouseUp = null; - }; + private handleOpenHeaderEditorTab: MouseEventHandler< + HTMLDivElement + > = _clickEvent => { + this.setState({ + headerEditorActive: true, + variableEditorActive: false, + }); + }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); + private handleOpenVariableEditorTab: MouseEventHandler< + HTMLDivElement + > = _clickEvent => { + this.setState({ + headerEditorActive: false, + variableEditorActive: true, + }); }; - private handleHeaderResizeStart: MouseEventHandler< + private handleExtraEditorResizeStart: MouseEventHandler< HTMLDivElement > = downEvent => { downEvent.preventDefault(); let didMove = false; - const wasOpen = this.state.headerEditorOpen; - const hadHeight = this.state.headerEditorHeight; + const wasOpen = this.state.extraEditorOpen; + const hadHeight = this.state.extraEditorHeight; const offset = downEvent.clientY - getTop(downEvent.target as HTMLElement); let onMouseMove: OnMouseMoveFn = moveEvent => { @@ -1385,20 +1375,20 @@ export class GraphiQL extends React.Component { const bottomSize = editorBar.clientHeight - topSize; if (bottomSize < 60) { this.setState({ - headerEditorOpen: false, - headerEditorHeight: hadHeight, + extraEditorOpen: false, + extraEditorHeight: hadHeight, }); } else { this.setState({ - headerEditorOpen: true, - headerEditorHeight: bottomSize, + extraEditorOpen: true, + extraEditorHeight: bottomSize, }); } }; let onMouseUp: OnMouseUpFn = () => { if (!didMove) { - this.setState({ headerEditorOpen: !wasOpen }); + this.setState({ extraEditorOpen: !wasOpen }); } document.removeEventListener('mousemove', onMouseMove!); diff --git a/packages/graphiql/src/css/app.css b/packages/graphiql/src/css/app.css index 00e97af7a21..fda511ab570 100644 --- a/packages/graphiql/src/css/app.css +++ b/packages/graphiql/src/css/app.css @@ -161,16 +161,14 @@ position: relative; } -.graphiql-container .variable-editor, -.graphiql-container .header-editor { +.graphiql-container .extra-editor { display: flex; flex-direction: column; height: 30px; position: relative; } -.graphiql-container .variable-editor-title, -.graphiql-container .header-editor-title { +.graphiql-container .extra-editor-title { background: #eeeeee; border-bottom: 1px solid #d6d6d6; border-top: 1px solid #e0e0e0; From 75211851d1cbb2be03484b1c6fe142cecf3ece91 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 21 May 2020 18:58:50 -0600 Subject: [PATCH 03/28] Include header contents in prettification and history. --- packages/graphiql/src/components/GraphiQL.tsx | 41 +++++++++++++++---- .../graphiql/src/components/HistoryQuery.tsx | 7 ++++ .../graphiql/src/components/QueryHistory.tsx | 18 +++++++- packages/graphiql/src/utility/QueryStore.ts | 4 ++ 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 2c17967a858..45868e9bfe5 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -708,6 +708,18 @@ export class GraphiQL extends React.Component { return null; } + /** + * Get the header editor CodeMirror instance. + * + * @public + */ + public getHeaderEditor() { + if (this.headerEditorComponent) { + return this.headerEditorComponent.getCodeMirror(); + } + return null; + } + /** * Refresh all CodeMirror instances. * @@ -954,7 +966,7 @@ export class GraphiQL extends React.Component { }); if (this._queryHistory) { - this._queryHistory.updateHistory(editedQuery, variables, operationName); + this._queryHistory.updateHistory(editedQuery, variables, headers, operationName); } // _fetchQuery may return a subscription. @@ -1049,6 +1061,22 @@ export class GraphiQL extends React.Component { } catch { /* Parsing JSON failed, skip prettification */ } + + const headerEditor = this.getHeaderEditor(); + const headerEditorContent = headerEditor?.getValue() ?? ''; + + try { + const prettifiedHeaderEditorContent = JSON.stringify( + JSON.parse(headerEditorContent), + null, + 2, + ); + if (prettifiedHeaderEditorContent !== headerEditorContent) { + headerEditor?.setValue(prettifiedHeaderEditorContent); + } + } catch { + /* Parsing JSON failed, skip prettification */ + } }; handleMergeQuery = () => { @@ -1201,16 +1229,10 @@ export class GraphiQL extends React.Component { this.setState({ historyPaneOpen: !this.state.historyPaneOpen }); }; - handleToggleVariableEditor = () => { - if (typeof this.props.onToggleVariableEditor === 'function') { - this.props.onToggleVariableEditor(!this.state.variableEditorActive); - } - this.setState({ variableEditorActive: !this.state.variableEditorActive }); - }; - handleSelectHistoryQuery = ( query?: string, variables?: string, + headers?: string, operationName?: string, ) => { if (query) { @@ -1219,6 +1241,9 @@ export class GraphiQL extends React.Component { if (variables) { this.handleEditVariables(variables); } + if (headers) { + this.handleEditHeaders(headers); + } if (operationName) { this.handleEditOperationName(operationName); } diff --git a/packages/graphiql/src/components/HistoryQuery.tsx b/packages/graphiql/src/components/HistoryQuery.tsx index cd29a252958..8b286003e5f 100644 --- a/packages/graphiql/src/components/HistoryQuery.tsx +++ b/packages/graphiql/src/components/HistoryQuery.tsx @@ -11,6 +11,7 @@ import { QueryStoreItem } from '../utility/QueryStore'; export type HandleEditLabelFn = ( query?: string, variables?: string, + headers?: string, operationName?: string, label?: string, favorite?: boolean, @@ -19,6 +20,7 @@ export type HandleEditLabelFn = ( export type HandleToggleFavoriteFn = ( query?: string, variables?: string, + headers?: string, operationName?: string, label?: string, favorite?: boolean, @@ -27,6 +29,7 @@ export type HandleToggleFavoriteFn = ( export type HandleSelectQueryFn = ( query?: string, variables?: string, + headers?: string, operationName?: string, label?: string, ) => void; @@ -101,6 +104,7 @@ export default class HistoryQuery extends React.Component< this.props.onSelect( this.props.query, this.props.variables, + this.props.headers, this.props.operationName, this.props.label, ); @@ -111,6 +115,7 @@ export default class HistoryQuery extends React.Component< this.props.handleToggleFavorite( this.props.query, this.props.variables, + this.props.headers, this.props.operationName, this.props.label, this.props.favorite, @@ -123,6 +128,7 @@ export default class HistoryQuery extends React.Component< this.props.handleEditLabel( this.props.query, this.props.variables, + this.props.headers, this.props.operationName, e.target.value, this.props.favorite, @@ -136,6 +142,7 @@ export default class HistoryQuery extends React.Component< this.props.handleEditLabel( this.props.query, this.props.variables, + this.props.headers, this.props.operationName, e.currentTarget.value, this.props.favorite, diff --git a/packages/graphiql/src/components/QueryHistory.tsx b/packages/graphiql/src/components/QueryHistory.tsx index 0fd948df678..908fceeca27 100644 --- a/packages/graphiql/src/components/QueryHistory.tsx +++ b/packages/graphiql/src/components/QueryHistory.tsx @@ -21,6 +21,7 @@ const MAX_HISTORY_LENGTH = 20; const shouldSaveQuery = ( query?: string, variables?: string, + headers?: string, lastQuerySaved?: QueryStoreItem, ) => { if (!query) { @@ -49,6 +50,14 @@ const shouldSaveQuery = ( if (variables && !lastQuerySaved.variables) { return false; } + if ( + JSON.stringify(headers) === JSON.stringify(lastQuerySaved.headers) + ) { + return false; + } + if (headers && !lastQuerySaved.headers) { + return false; + } } return true; }; @@ -56,6 +65,7 @@ const shouldSaveQuery = ( type QueryHistoryProps = { query?: string; variables?: string; + headers?: string; operationName?: string; queryID?: number; onSelectQuery: HandleSelectQueryFn; @@ -116,12 +126,14 @@ export class QueryHistory extends React.Component< updateHistory = ( query?: string, variables?: string, + headers?: string, operationName?: string, ) => { - if (shouldSaveQuery(query, variables, this.historyStore.fetchRecent())) { + if (shouldSaveQuery(query, variables, headers, this.historyStore.fetchRecent())) { this.historyStore.push({ query, variables, + headers, operationName, }); const historyQueries = this.historyStore.items; @@ -137,6 +149,7 @@ export class QueryHistory extends React.Component< toggleFavorite: HandleToggleFavoriteFn = ( query, variables, + headers, operationName, label, favorite, @@ -144,6 +157,7 @@ export class QueryHistory extends React.Component< const item: QueryStoreItem = { query, variables, + headers, operationName, label, }; @@ -163,6 +177,7 @@ export class QueryHistory extends React.Component< editLabel: HandleEditLabelFn = ( query, variables, + headers, operationName, label, favorite, @@ -170,6 +185,7 @@ export class QueryHistory extends React.Component< const item = { query, variables, + headers, operationName, label, }; diff --git a/packages/graphiql/src/utility/QueryStore.ts b/packages/graphiql/src/utility/QueryStore.ts index e2eceb3d096..70cae0c1698 100644 --- a/packages/graphiql/src/utility/QueryStore.ts +++ b/packages/graphiql/src/utility/QueryStore.ts @@ -9,6 +9,7 @@ import StorageAPI from './StorageAPI'; export type QueryStoreItem = { query?: string; variables?: string; + headers?: string; operationName?: string; label?: string; favorite?: boolean; @@ -34,6 +35,7 @@ export default class QueryStore { x => x.query === item.query && x.variables === item.variables && + x.headers === item.headers && x.operationName === item.operationName, ); } @@ -43,6 +45,7 @@ export default class QueryStore { x => x.query === item.query && x.variables === item.variables && + x.headers === item.headers && x.operationName === item.operationName, ); if (itemIndex !== -1) { @@ -56,6 +59,7 @@ export default class QueryStore { x => x.query === item.query && x.variables === item.variables && + x.headers === item.headers && x.operationName === item.operationName, ); if (itemIndex !== -1) { From 6a256785495850734fbd04b26150318c89cb2108 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 21 May 2020 19:16:26 -0600 Subject: [PATCH 04/28] Pass headers as a parameter to the fetcher. --- packages/graphiql/src/components/GraphiQL.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 45868e9bfe5..8b21aadc88f 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -84,6 +84,7 @@ export type FetcherResult = export type Fetcher = ( graphQLParams: FetcherParams, + headers?: Object, ) => Promise | Observable; type OnMouseMoveFn = Maybe< @@ -888,12 +889,14 @@ export class GraphiQL extends React.Component { throw new Error('Headers are not a JSON object.'); } - const fetch = fetcher({ - query, - variables: jsonVariables, - headers: jsonHeaders, - operationName, - }); + const fetch = fetcher( + { + query, + variables: jsonVariables, + operationName, + }, + jsonHeaders, + ); if (isPromise(fetch)) { // If fetcher returned a Promise, then call the callback when the promise @@ -966,7 +969,12 @@ export class GraphiQL extends React.Component { }); if (this._queryHistory) { - this._queryHistory.updateHistory(editedQuery, variables, headers, operationName); + this._queryHistory.updateHistory( + editedQuery, + variables, + headers, + operationName, + ); } // _fetchQuery may return a subscription. From c0e68da6901d365cf5baed5e3ef7385477f7d04f Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Thu, 21 May 2020 19:30:25 -0600 Subject: [PATCH 05/28] Fix a linting error. --- packages/graphiql/src/components/QueryHistory.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/graphiql/src/components/QueryHistory.tsx b/packages/graphiql/src/components/QueryHistory.tsx index 908fceeca27..dfae7ac9d5f 100644 --- a/packages/graphiql/src/components/QueryHistory.tsx +++ b/packages/graphiql/src/components/QueryHistory.tsx @@ -50,9 +50,7 @@ const shouldSaveQuery = ( if (variables && !lastQuerySaved.variables) { return false; } - if ( - JSON.stringify(headers) === JSON.stringify(lastQuerySaved.headers) - ) { + if (JSON.stringify(headers) === JSON.stringify(lastQuerySaved.headers)) { return false; } if (headers && !lastQuerySaved.headers) { @@ -129,7 +127,14 @@ export class QueryHistory extends React.Component< headers?: string, operationName?: string, ) => { - if (shouldSaveQuery(query, variables, headers, this.historyStore.fetchRecent())) { + if ( + shouldSaveQuery( + query, + variables, + headers, + this.historyStore.fetchRecent(), + ) + ) { this.historyStore.push({ query, variables, From 2dcd46d540dea2cf133eb02b6d39f21c3d9488bc Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 22 May 2020 17:28:35 -0600 Subject: [PATCH 06/28] Rename "Query Headers" tab to "Request Headers". --- packages/graphiql/src/components/GraphiQL.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 8b21aadc88f..829e975a020 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -551,7 +551,9 @@ export class GraphiQL extends React.Component {
+ aria-label={ + variableEditorActive ? 'Query Variables' : 'Request Headers' + }>
{ }} onClick={this.handleOpenHeaderEditorTab} onMouseDown={this.handleTabClickPropogation}> - {'Query Headers'} + {'Request Headers'}
{variableEditorActive && ( From 9f29e6a8cf498c6f1998ca3839f1d4a0e9cfba77 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 22 May 2020 17:31:10 -0600 Subject: [PATCH 07/28] Add the old variable editor CSS class again. --- packages/graphiql/src/css/app.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/graphiql/src/css/app.css b/packages/graphiql/src/css/app.css index fda511ab570..3c7a8ca86be 100644 --- a/packages/graphiql/src/css/app.css +++ b/packages/graphiql/src/css/app.css @@ -161,6 +161,7 @@ position: relative; } +.graphiql-container .variable-editor, .graphiql-container .extra-editor { display: flex; flex-direction: column; @@ -168,6 +169,7 @@ position: relative; } +.graphiql-container .variable-editor-title, .graphiql-container .extra-editor-title { background: #eeeeee; border-bottom: 1px solid #d6d6d6; From e7cfcabaef92eb15e2e5e00f18657ce44c8df3b7 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 22 May 2020 17:34:33 -0600 Subject: [PATCH 08/28] Rename extra editor to secondary editor. --- packages/graphiql/src/components/GraphiQL.tsx | 56 +++++++++---------- packages/graphiql/src/css/app.css | 4 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 829e975a020..7b90c6413ba 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -102,7 +102,7 @@ export type GraphiQLProps = { response?: string; storage?: Storage; defaultQuery?: string; - defaultExtraEditorOpen?: boolean; + defaultSecondaryEditorOpen?: boolean; onCopyQuery?: (query?: string) => void; onEditQuery?: (query?: string) => void; onEditVariables?: (value: string) => void; @@ -128,8 +128,8 @@ export type GraphiQLState = { docExplorerOpen: boolean; response?: string; editorFlex: number; - extraEditorOpen: boolean; - extraEditorHeight: number; + secondaryEditorOpen: boolean; + secondaryEditorHeight: number; variableEditorActive: boolean; headerEditorActive: boolean; historyPaneOpen: boolean; @@ -232,10 +232,10 @@ export class GraphiQL extends React.Component { docExplorerOpen = this._storage.get('docExplorerOpen') === 'true'; } - // initial extra editor pane open - const extraEditorOpen = - props.defaultExtraEditorOpen !== undefined - ? props.defaultExtraEditorOpen + // initial secondary editor pane open + const secondaryEditorOpen = + props.defaultSecondaryEditorOpen !== undefined + ? props.defaultSecondaryEditorOpen : Boolean(variables || headers); // Initialize state @@ -247,8 +247,8 @@ export class GraphiQL extends React.Component { docExplorerOpen, response: props.response, editorFlex: Number(this._storage.get('editorFlex')) || 1, - extraEditorOpen, - extraEditorHeight: Number(this._storage.get('extraEditorHeight')) || 200, + secondaryEditorOpen, + secondaryEditorHeight: Number(this._storage.get('secondaryEditorHeight')) || 200, variableEditorActive: this._storage.get('variableEditorActive') === 'true', headerEditorActive: this._storage.get('headerEditorActive') === 'true', @@ -388,8 +388,8 @@ export class GraphiQL extends React.Component { } this._storage.set('editorFlex', JSON.stringify(this.state.editorFlex)); this._storage.set( - 'extraEditorHeight', - JSON.stringify(this.state.extraEditorHeight), + 'secondaryEditorHeight', + JSON.stringify(this.state.secondaryEditorHeight), ); this._storage.set( 'variableEditorActive', @@ -470,9 +470,9 @@ export class GraphiQL extends React.Component { zIndex: 7, }; - const extraEditorOpen = this.state.extraEditorOpen; - const extraEditorStyle = { - height: extraEditorOpen ? this.state.extraEditorHeight : undefined, + const secondaryEditorOpen = this.state.secondaryEditorOpen; + const secondaryEditorStyle = { + height: secondaryEditorOpen ? this.state.secondaryEditorHeight : undefined, }; const variableEditorActive = this.state.variableEditorActive; @@ -549,18 +549,18 @@ export class GraphiQL extends React.Component { readOnly={this.props.readOnly} />
+ onMouseDown={this.handleSecondaryEditorResizeStart}>
{ }); }; - private handleExtraEditorResizeStart: MouseEventHandler< + private handleSecondaryEditorResizeStart: MouseEventHandler< HTMLDivElement > = downEvent => { downEvent.preventDefault(); let didMove = false; - const wasOpen = this.state.extraEditorOpen; - const hadHeight = this.state.extraEditorHeight; + const wasOpen = this.state.secondaryEditorOpen; + const hadHeight = this.state.secondaryEditorHeight; const offset = downEvent.clientY - getTop(downEvent.target as HTMLElement); let onMouseMove: OnMouseMoveFn = moveEvent => { @@ -1410,20 +1410,20 @@ export class GraphiQL extends React.Component { const bottomSize = editorBar.clientHeight - topSize; if (bottomSize < 60) { this.setState({ - extraEditorOpen: false, - extraEditorHeight: hadHeight, + secondaryEditorOpen: false, + secondaryEditorHeight: hadHeight, }); } else { this.setState({ - extraEditorOpen: true, - extraEditorHeight: bottomSize, + secondaryEditorOpen: true, + secondaryEditorHeight: bottomSize, }); } }; let onMouseUp: OnMouseUpFn = () => { if (!didMove) { - this.setState({ extraEditorOpen: !wasOpen }); + this.setState({ secondaryEditorOpen: !wasOpen }); } document.removeEventListener('mousemove', onMouseMove!); diff --git a/packages/graphiql/src/css/app.css b/packages/graphiql/src/css/app.css index 3c7a8ca86be..a53638294f8 100644 --- a/packages/graphiql/src/css/app.css +++ b/packages/graphiql/src/css/app.css @@ -162,7 +162,7 @@ } .graphiql-container .variable-editor, -.graphiql-container .extra-editor { +.graphiql-container .secondary-editor { display: flex; flex-direction: column; height: 30px; @@ -170,7 +170,7 @@ } .graphiql-container .variable-editor-title, -.graphiql-container .extra-editor-title { +.graphiql-container .secondary-editor-title { background: #eeeeee; border-bottom: 1px solid #d6d6d6; border-top: 1px solid #e0e0e0; From 7679ddfab6cb2b6b37ade5c5edce536f3eab79cc Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 22 May 2020 17:50:06 -0600 Subject: [PATCH 09/28] Bring back defaultVariableEditorOpen prop, bring back variable classes. --- packages/graphiql/src/components/GraphiQL.tsx | 19 ++++++++++++------- packages/graphiql/src/css/app.css | 2 -- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 7b90c6413ba..769db513145 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -102,6 +102,7 @@ export type GraphiQLProps = { response?: string; storage?: Storage; defaultQuery?: string; + defaultVariableEditorOpen?: boolean; defaultSecondaryEditorOpen?: boolean; onCopyQuery?: (query?: string) => void; onEditQuery?: (query?: string) => void; @@ -233,10 +234,14 @@ export class GraphiQL extends React.Component { } // initial secondary editor pane open - const secondaryEditorOpen = - props.defaultSecondaryEditorOpen !== undefined - ? props.defaultSecondaryEditorOpen - : Boolean(variables || headers); + let secondaryEditorOpen; + if (props.defaultVariableEditorOpen !== undefined) { + secondaryEditorOpen = props.defaultVariableEditorOpen; + } else if (props.defaultSecondaryEditorOpen !== undefined) { + secondaryEditorOpen = props.defaultSecondaryEditorOpen; + } else { + secondaryEditorOpen = Boolean(variables || headers); + } // Initialize state this.state = { @@ -549,14 +554,14 @@ export class GraphiQL extends React.Component { readOnly={this.props.readOnly} />
Date: Sat, 23 May 2020 14:36:19 -0600 Subject: [PATCH 10/28] Trigger edit headers on first render. --- packages/graphiql/src/components/HeaderEditor.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/graphiql/src/components/HeaderEditor.tsx b/packages/graphiql/src/components/HeaderEditor.tsx index 3a3a7594b99..9cee3aaa17b 100644 --- a/packages/graphiql/src/components/HeaderEditor.tsx +++ b/packages/graphiql/src/components/HeaderEditor.tsx @@ -48,7 +48,7 @@ export class HeaderEditor extends React.Component { editor: (CM.Editor & { options: any }) | null = null; cachedValue: string; private _node: HTMLElement | null = null; - ignoreChangeEvent: boolean; + ignoreChangeEvent: boolean = false; constructor(props: HeaderEditorProps) { super(props); @@ -56,7 +56,6 @@ export class HeaderEditor extends React.Component { // editor is updated, which can later be used to protect the editor from // unnecessary updates during the update lifecycle. this.cachedValue = props.value || ''; - this.ignoreChangeEvent = true; } componentDidMount() { From 6c987643f3d721890d14300863ffe2908f806896 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 14:42:00 -0600 Subject: [PATCH 11/28] Toggle the display of the editors rather than removing them entirely. This way the editors are persisted when switching tabs. --- packages/graphiql/src/components/GraphiQL.tsx | 69 +++++++++---------- .../graphiql/src/components/HeaderEditor.tsx | 2 + .../src/components/VariableEditor.tsx | 2 + 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 769db513145..032d5a0c44b 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -480,9 +480,6 @@ export class GraphiQL extends React.Component { height: secondaryEditorOpen ? this.state.secondaryEditorHeight : undefined, }; - const variableEditorActive = this.state.variableEditorActive; - const headerEditorActive = this.state.headerEditorActive; - return (
{ @@ -557,7 +554,7 @@ export class GraphiQL extends React.Component { className="variable-editor secondary-editor" style={secondaryEditorStyle} aria-label={ - variableEditorActive ? 'Query Variables' : 'Request Headers' + this.state.variableEditorActive ? 'Query Variables' : 'Request Headers' }>
{
{
{ {'Request Headers'}
- {variableEditorActive && ( - { - this.variableEditorComponent = n; - }} - value={this.state.variables} - variableToType={this.state.variableToType} - onEdit={this.handleEditVariables} - onHintInformationRender={this.handleHintInformationRender} - onPrettifyQuery={this.handlePrettifyQuery} - onMergeQuery={this.handleMergeQuery} - onRunQuery={this.handleEditorRunQuery} - editorTheme={this.props.editorTheme} - readOnly={this.props.readOnly} - /> - )} - {headerEditorActive && ( - { - this.headerEditorComponent = n; - }} - value={this.state.headers} - onEdit={this.handleEditHeaders} - onHintInformationRender={this.handleHintInformationRender} - onPrettifyQuery={this.handlePrettifyQuery} - onMergeQuery={this.handleMergeQuery} - onRunQuery={this.handleEditorRunQuery} - editorTheme={this.props.editorTheme} - readOnly={this.props.readOnly} - /> - )} + { + this.variableEditorComponent = n; + }} + value={this.state.variables} + variableToType={this.state.variableToType} + onEdit={this.handleEditVariables} + onHintInformationRender={this.handleHintInformationRender} + onPrettifyQuery={this.handlePrettifyQuery} + onMergeQuery={this.handleMergeQuery} + onRunQuery={this.handleEditorRunQuery} + editorTheme={this.props.editorTheme} + readOnly={this.props.readOnly} + active={this.state.variableEditorActive} + /> + { + this.headerEditorComponent = n; + }} + value={this.state.headers} + onEdit={this.handleEditHeaders} + onHintInformationRender={this.handleHintInformationRender} + onPrettifyQuery={this.handlePrettifyQuery} + onMergeQuery={this.handleMergeQuery} + onRunQuery={this.handleEditorRunQuery} + editorTheme={this.props.editorTheme} + readOnly={this.props.readOnly} + active={this.state.headerEditorActive} + />
diff --git a/packages/graphiql/src/components/HeaderEditor.tsx b/packages/graphiql/src/components/HeaderEditor.tsx index 9cee3aaa17b..09f6bfa946a 100644 --- a/packages/graphiql/src/components/HeaderEditor.tsx +++ b/packages/graphiql/src/components/HeaderEditor.tsx @@ -29,6 +29,7 @@ type HeaderEditorProps = { onMergeQuery: (value?: string) => void; onRunQuery: (value?: string) => void; editorTheme?: string; + active?: boolean; }; /** @@ -174,6 +175,7 @@ export class HeaderEditor extends React.Component { render() { return (
{ this._node = node as HTMLDivElement; diff --git a/packages/graphiql/src/components/VariableEditor.tsx b/packages/graphiql/src/components/VariableEditor.tsx index 45cd2d6dd41..e40ca264e20 100644 --- a/packages/graphiql/src/components/VariableEditor.tsx +++ b/packages/graphiql/src/components/VariableEditor.tsx @@ -31,6 +31,7 @@ type VariableEditorProps = { onMergeQuery: (value?: string) => void; onRunQuery: (value?: string) => void; editorTheme?: string; + active?: boolean; }; /** @@ -195,6 +196,7 @@ export class VariableEditor extends React.Component { render() { return (
{ this._node = node as HTMLDivElement; From 40c29b5d06f0ad1003dfc4178eba848a7302c643 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 15:24:26 -0600 Subject: [PATCH 12/28] Cursed fix for the gutter getting squashed when both editors were side-by-side. Despite the other editor being rendered as display: none, the gutter for some reason would get squished by CodeMirror. This is the only thing I've figured out that will fix it. I _think_ this works because setting position: absolute will take it out of the normal document flow, but I'm not entirely sure. --- packages/graphiql/src/components/HeaderEditor.tsx | 7 ++++++- packages/graphiql/src/components/VariableEditor.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/graphiql/src/components/HeaderEditor.tsx b/packages/graphiql/src/components/HeaderEditor.tsx index 09f6bfa946a..91c739021b1 100644 --- a/packages/graphiql/src/components/HeaderEditor.tsx +++ b/packages/graphiql/src/components/HeaderEditor.tsx @@ -175,8 +175,13 @@ export class HeaderEditor extends React.Component { render() { return (
{ this._node = node as HTMLDivElement; }} diff --git a/packages/graphiql/src/components/VariableEditor.tsx b/packages/graphiql/src/components/VariableEditor.tsx index e40ca264e20..65240f9a5bb 100644 --- a/packages/graphiql/src/components/VariableEditor.tsx +++ b/packages/graphiql/src/components/VariableEditor.tsx @@ -196,8 +196,13 @@ export class VariableEditor extends React.Component { render() { return (
{ this._node = node as HTMLDivElement; }} From de470ae7051562303d45d6bdb3dcfcb4a2c6bef0 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 15:42:43 -0600 Subject: [PATCH 13/28] Open the secondary editor when clicking on either of the editor tab names. --- packages/graphiql/src/components/GraphiQL.tsx | 2 ++ packages/graphiql/src/components/HeaderEditor.tsx | 2 +- packages/graphiql/src/components/VariableEditor.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 032d5a0c44b..8fd0246379a 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -1376,6 +1376,7 @@ export class GraphiQL extends React.Component { this.setState({ headerEditorActive: true, variableEditorActive: false, + secondaryEditorOpen: true, }); }; @@ -1385,6 +1386,7 @@ export class GraphiQL extends React.Component { this.setState({ headerEditorActive: false, variableEditorActive: true, + secondaryEditorOpen: true, }); }; diff --git a/packages/graphiql/src/components/HeaderEditor.tsx b/packages/graphiql/src/components/HeaderEditor.tsx index 91c739021b1..fd8bec3fb1a 100644 --- a/packages/graphiql/src/components/HeaderEditor.tsx +++ b/packages/graphiql/src/components/HeaderEditor.tsx @@ -177,7 +177,7 @@ export class HeaderEditor extends React.Component {
{
Date: Sat, 23 May 2020 16:04:26 -0600 Subject: [PATCH 14/28] Add headerEditorEnabled prop for enabling/disabling the header editor. The prop will default to true. Also fixed headers not being initialized as part of the initial component state, this way default headers can be passed into the component. --- packages/graphiql/src/components/GraphiQL.tsx | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 8fd0246379a..87630551802 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -104,6 +104,7 @@ export type GraphiQLProps = { defaultQuery?: string; defaultVariableEditorOpen?: boolean; defaultSecondaryEditorOpen?: boolean; + headerEditorEnabled?: boolean; onCopyQuery?: (query?: string) => void; onEditQuery?: (query?: string) => void; onEditVariables?: (value: string) => void; @@ -133,6 +134,7 @@ export type GraphiQLState = { secondaryEditorHeight: number; variableEditorActive: boolean; headerEditorActive: boolean; + headerEditorEnabled: boolean; historyPaneOpen: boolean; docExplorerWidth: number; isWaitingForResponse: boolean; @@ -243,11 +245,14 @@ export class GraphiQL extends React.Component { secondaryEditorOpen = Boolean(variables || headers); } + const headerEditorEnabled = props.headerEditorEnabled ?? true; + // Initialize state this.state = { schema: props.schema, query, variables: variables as string, + headers: headers as string, operationName, docExplorerOpen, response: props.response, @@ -257,6 +262,7 @@ export class GraphiQL extends React.Component { variableEditorActive: this._storage.get('variableEditorActive') === 'true', headerEditorActive: this._storage.get('headerEditorActive') === 'true', + headerEditorEnabled, historyPaneOpen: this._storage.get('historyPaneOpen') === 'true' || false, docExplorerWidth: Number(this._storage.get('docExplorerWidth')) || @@ -573,17 +579,19 @@ export class GraphiQL extends React.Component { onMouseDown={this.handleTabClickPropogation}> {'Query Variables'}
-
- {'Request Headers'} -
+ {this.state.headerEditorEnabled && ( +
+ {'Request Headers'} +
+ )}
{ @@ -600,20 +608,22 @@ export class GraphiQL extends React.Component { readOnly={this.props.readOnly} active={this.state.variableEditorActive} /> - { - this.headerEditorComponent = n; - }} - value={this.state.headers} - onEdit={this.handleEditHeaders} - onHintInformationRender={this.handleHintInformationRender} - onPrettifyQuery={this.handlePrettifyQuery} - onMergeQuery={this.handleMergeQuery} - onRunQuery={this.handleEditorRunQuery} - editorTheme={this.props.editorTheme} - readOnly={this.props.readOnly} - active={this.state.headerEditorActive} - /> + {this.state.headerEditorEnabled && ( + { + this.headerEditorComponent = n; + }} + value={this.state.headers} + onEdit={this.handleEditHeaders} + onHintInformationRender={this.handleHintInformationRender} + onPrettifyQuery={this.handlePrettifyQuery} + onMergeQuery={this.handleMergeQuery} + onRunQuery={this.handleEditorRunQuery} + editorTheme={this.props.editorTheme} + readOnly={this.props.readOnly} + active={this.state.headerEditorActive} + /> + )}
@@ -1363,6 +1373,7 @@ export class GraphiQL extends React.Component { }); }; + // Prevent clicking on the tab button from propagating to the resizer. private handleTabClickPropogation: MouseEventHandler< HTMLDivElement > = downEvent => { From ea990e1e458c507731bc823ba81e82c1aa8eee12 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 16:26:25 -0600 Subject: [PATCH 15/28] Remove unnecessary props from GraphiQLProps. --- packages/graphiql/src/components/GraphiQL.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 87630551802..075508efb4f 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -110,8 +110,6 @@ export type GraphiQLProps = { onEditVariables?: (value: string) => void; onEditHeaders?: (value: string) => void; onEditOperationName?: (operationName: string) => void; - onToggleVariableEditor?: (variableEditorActive: boolean) => void; - onToggleHeaderEditor?: (headerEditorActive: boolean) => void; onToggleDocs?: (docExplorerOpen: boolean) => void; getDefaultFieldNames?: GetDefaultFieldNamesFn; editorTheme?: string; From dd0cbca5d2791494322acb33dcaa7512b3786de0 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 16:26:58 -0600 Subject: [PATCH 16/28] Update GraphiQL README with new props for the header editor. --- packages/graphiql/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/graphiql/README.md b/packages/graphiql/README.md index 47258f7dd7e..b6b7564d1b8 100644 --- a/packages/graphiql/README.md +++ b/packages/graphiql/README.md @@ -163,6 +163,8 @@ GraphiQL supports customization in UI and behavior by accepting React props and - `variables`: an optional GraphQL string to use as the initial displayed query variables, if `undefined` is provided, the stored variables will be used. +- `headers`: an optional GraphQL string to use as the initial displayed request headers, if `undefined` is provided, the stored headers will be used. + - `operationName`: an optional name of which GraphQL operation should be executed. - `response`: an optional JSON string to use as the initial displayed response. If not provided, no response will be initially shown. You might provide this if illustrating the result of the initial query. @@ -171,12 +173,16 @@ GraphiQL supports customization in UI and behavior by accepting React props and - `defaultQuery`: an optional GraphQL string to use when no query is provided and no stored query exists from a previous session. If `undefined` is provided, GraphiQL will use its own default query. -- `defaultVariableEditorOpen`: an optional boolean that sets whether or not to show the variables pane on startup. If not defined, it will be based off whether or not variables are present. +- `defaultVariableEditorOpen`: an optional boolean that sets whether or not to show the variables pane on startup. If not defined, it will be based off whether or not variables are present. (**deprecated** in favor of `defaultSecondaryEditorOpen`) + +- `defaultSecondaryEditorOpen`: an optional boolean that sets whether or not to show the variables/headers pane on startup. If not defined, it will be based off whether or not variables and/or headers are present. - `onEditQuery`: an optional function which will be called when the Query editor changes. The argument to the function will be the query string. - `onEditVariables`: an optional function which will be called when the Query variable editor changes. The argument to the function will be the variables string. +- `onEditHeaders`: an optional function which will be called when the request headers editor changes. The argument to the function will be the headers string. + - `onEditOperationName`: an optional function which will be called when the operation name to be executed changes. - `onToggleDocs`: an optional function which will be called when the docs will be toggled. The argument to the function will be a boolean whether the docs are now open or closed. @@ -232,6 +238,7 @@ class CustomGraphiQL extends React.Component { // GraphQL artifacts query: '', variables: '', + headers: '', response: '', // GraphQL Schema From 403e9d0fea78b0dc56f1c8c46c94d78a1cc7c991 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 16:33:28 -0600 Subject: [PATCH 17/28] Add header submission to renderExample.js. --- packages/graphiql/resources/renderExample.js | 33 ++++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index fe19c100789..91bdfbdd114 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -36,6 +36,20 @@ if (parameters.variables) { } } +// If headers was provided, try to format it. +if (parameters.headers) { + try { + parameters.headers = JSON.stringify( + JSON.parse(parameters.headers), + null, + 2, + ); + } catch (e) { + // Do nothing, we want to display the invalid JSON as a string, rather + // than present an error. + } +} + // When the query and variables string is edited, update the URL bar so // that it can be easily shared. function onEditQuery(newQuery) { @@ -48,6 +62,11 @@ function onEditVariables(newVariables) { updateURL(); } +function onEditHeaders(newHeaders) { + parameters.headers = newHeaders; + updateURL(); +} + function onEditOperationName(newOperationName) { parameters.operationName = newOperationName; updateURL(); @@ -72,7 +91,7 @@ function updateURL() { // Defines a GraphQL fetcher using the fetch API. You're not required to // use fetch, and could instead implement graphQLFetcher however you like, // as long as it returns a Promise or Observable. -function graphQLFetcher(graphQLParams) { +function graphQLFetcher(graphQLParams, headers = {}) { // When working locally, the example expects a GraphQL server at the path /graphql. // In a PR preview, it connects to the Star Wars API externally. // Change this to point wherever you host your GraphQL server. @@ -80,12 +99,18 @@ function graphQLFetcher(graphQLParams) { const api = isDev ? '/graphql' : 'https://swapi-graphql.netlify.app/.netlify/functions/index'; + + // Convert headers to an object. + if (typeof headers === 'string') { + headers = JSON.parse(headers); + } + return fetch(api, { method: 'post', - headers: { + headers: Object.assign({ Accept: 'application/json', 'Content-Type': 'application/json', - }, + }, headers), body: JSON.stringify(graphQLParams), credentials: 'omit', }) @@ -110,9 +135,11 @@ ReactDOM.render( fetcher: graphQLFetcher, query: parameters.query, variables: parameters.variables, + headers: parameters.headers, operationName: parameters.operationName, onEditQuery: onEditQuery, onEditVariables: onEditVariables, + onEditHeaders: onEditHeaders, defaultVariableEditorOpen: true, onEditOperationName: onEditOperationName, }), From 1ef963f8a716da7d487d1f59b50ad6f3af53f2a1 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 16:36:12 -0600 Subject: [PATCH 18/28] Enable variable editor by default if neither tab is set as active. --- packages/graphiql/src/components/GraphiQL.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 075508efb4f..b376d8cca1c 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -258,7 +258,7 @@ export class GraphiQL extends React.Component { secondaryEditorOpen, secondaryEditorHeight: Number(this._storage.get('secondaryEditorHeight')) || 200, variableEditorActive: - this._storage.get('variableEditorActive') === 'true', + this._storage.get('variableEditorActive') === 'true' || (this._storage.get('headerEditorActive') !== 'true'), headerEditorActive: this._storage.get('headerEditorActive') === 'true', headerEditorEnabled, historyPaneOpen: this._storage.get('historyPaneOpen') === 'true' || false, From a01dc1a7aca12178f5c5c30e69e43e06a5e936ea Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 16:38:52 -0600 Subject: [PATCH 19/28] Appease the linters. --- packages/graphiql/src/components/GraphiQL.tsx | 14 ++++++++++---- packages/graphiql/src/components/HeaderEditor.tsx | 2 +- .../graphiql/src/components/VariableEditor.tsx | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index b376d8cca1c..52e4b0e95f7 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -256,9 +256,11 @@ export class GraphiQL extends React.Component { response: props.response, editorFlex: Number(this._storage.get('editorFlex')) || 1, secondaryEditorOpen, - secondaryEditorHeight: Number(this._storage.get('secondaryEditorHeight')) || 200, + secondaryEditorHeight: + Number(this._storage.get('secondaryEditorHeight')) || 200, variableEditorActive: - this._storage.get('variableEditorActive') === 'true' || (this._storage.get('headerEditorActive') !== 'true'), + this._storage.get('variableEditorActive') === 'true' || + this._storage.get('headerEditorActive') !== 'true', headerEditorActive: this._storage.get('headerEditorActive') === 'true', headerEditorEnabled, historyPaneOpen: this._storage.get('historyPaneOpen') === 'true' || false, @@ -481,7 +483,9 @@ export class GraphiQL extends React.Component { const secondaryEditorOpen = this.state.secondaryEditorOpen; const secondaryEditorStyle = { - height: secondaryEditorOpen ? this.state.secondaryEditorHeight : undefined, + height: secondaryEditorOpen + ? this.state.secondaryEditorHeight + : undefined, }; return ( @@ -558,7 +562,9 @@ export class GraphiQL extends React.Component { className="variable-editor secondary-editor" style={secondaryEditorStyle} aria-label={ - this.state.variableEditorActive ? 'Query Variables' : 'Request Headers' + this.state.variableEditorActive + ? 'Query Variables' + : 'Request Headers' }>
{ // causes one of the editors' gutters to break otherwise. style={{ position: this.props.active ? 'relative' : 'absolute', - visibility: this.props.active ? 'visible' : 'hidden' + visibility: this.props.active ? 'visible' : 'hidden', }} ref={node => { this._node = node as HTMLDivElement; diff --git a/packages/graphiql/src/components/VariableEditor.tsx b/packages/graphiql/src/components/VariableEditor.tsx index 395be7fed58..a8e32352f17 100644 --- a/packages/graphiql/src/components/VariableEditor.tsx +++ b/packages/graphiql/src/components/VariableEditor.tsx @@ -201,7 +201,7 @@ export class VariableEditor extends React.Component { // causes one of the editors' gutters to break otherwise. style={{ position: this.props.active ? 'relative' : 'absolute', - visibility: this.props.active ? 'visible' : 'hidden' + visibility: this.props.active ? 'visible' : 'hidden', }} ref={node => { this._node = node as HTMLDivElement; From 3a67fa6cede1c195a6cfe9e051f7d834dd290375 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 17:32:04 -0600 Subject: [PATCH 20/28] Update GraphiQL tests with mock headers and fix a bug in history. It wasn't saving the history changes even if the variables were changed, it should save a new history entry whenever either the headers or variables change. --- packages/graphiql/src/components/QueryHistory.tsx | 13 ++++++------- .../src/components/__tests__/GraphiQL.spec.tsx | 6 ++++++ .../src/components/__tests__/HistoryQuery.spec.tsx | 4 +++- .../graphiql/src/components/__tests__/fixtures.ts | 2 ++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/graphiql/src/components/QueryHistory.tsx b/packages/graphiql/src/components/QueryHistory.tsx index dfae7ac9d5f..e55766156e3 100644 --- a/packages/graphiql/src/components/QueryHistory.tsx +++ b/packages/graphiql/src/components/QueryHistory.tsx @@ -45,17 +45,16 @@ const shouldSaveQuery = ( if ( JSON.stringify(variables) === JSON.stringify(lastQuerySaved.variables) ) { - return false; + if (JSON.stringify(headers) === JSON.stringify(lastQuerySaved.headers)) { + return false; + } + if (headers && !lastQuerySaved.headers) { + return false; + } } if (variables && !lastQuerySaved.variables) { return false; } - if (JSON.stringify(headers) === JSON.stringify(lastQuerySaved.headers)) { - return false; - } - if (headers && !lastQuerySaved.headers) { - return false; - } } return true; }; diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index 6dbe2fd9caf..a6c2a7e4dea 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -18,6 +18,7 @@ import { mockBadQuery, mockQuery2, mockVariables2, + mockHeaders1, } from './fixtures'; codeMirrorModules.forEach(m => jest.mock(m, () => {})); @@ -177,6 +178,7 @@ describe('GraphiQL', () => { , @@ -200,6 +202,7 @@ describe('GraphiQL', () => { operationName={mockOperationName1} query={mockQuery1} variables={mockVariables1} + headers={mockHeaders1} />, ); fireEvent.click(getByTitle('Execute Query (Ctrl-Enter)')); @@ -213,6 +216,7 @@ describe('GraphiQL', () => { operationName={mockOperationName1} query={mockQuery1} variables={mockVariables1} + headers={mockHeaders1} />, ); fireEvent.click(getByTitle('Execute Query (Ctrl-Enter)')); @@ -228,6 +232,7 @@ describe('GraphiQL', () => { operationName={mockOperationName1} query={mockQuery1} variables={mockVariables1} + headers={mockHeaders1} />, ); const executeQueryButton = getByTitle('Execute Query (Ctrl-Enter)'); @@ -255,6 +260,7 @@ describe('GraphiQL', () => { operationName={mockOperationName1} query={mockQuery1} variables={mockVariables1} + headers={mockHeaders1} />, ); const executeQueryButton = getByTitle('Execute Query (Ctrl-Enter)'); diff --git a/packages/graphiql/src/components/__tests__/HistoryQuery.spec.tsx b/packages/graphiql/src/components/__tests__/HistoryQuery.spec.tsx index 256de8765f6..aa455077ecc 100644 --- a/packages/graphiql/src/components/__tests__/HistoryQuery.spec.tsx +++ b/packages/graphiql/src/components/__tests__/HistoryQuery.spec.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import HistoryQuery, { HistoryQueryProps } from '../HistoryQuery'; -import { mockOperationName1, mockQuery1, mockVariables1 } from './fixtures'; +import { mockOperationName1, mockQuery1, mockVariables1, mockHeaders1 } from './fixtures'; const noOp = () => {}; @@ -19,6 +19,7 @@ const baseMockProps = { onSelect: noOp, query: mockQuery1, variables: mockVariables1, + headers: mockHeaders1 }; function getMockProps( @@ -62,6 +63,7 @@ describe('HistoryQuery', () => { expect(onSelectSpy).toHaveBeenCalledWith( mockQuery1, mockVariables1, + mockHeaders1, mockOperationName1, undefined, ); diff --git a/packages/graphiql/src/components/__tests__/fixtures.ts b/packages/graphiql/src/components/__tests__/fixtures.ts index 137ec90fd4a..5fbe8f4895c 100644 --- a/packages/graphiql/src/components/__tests__/fixtures.ts +++ b/packages/graphiql/src/components/__tests__/fixtures.ts @@ -19,6 +19,8 @@ export const mockQuery2 = /* GraphQL */ ` export const mockVariables1 = JSON.stringify({ string: 'string' }); export const mockVariables2 = JSON.stringify({ string: 'string2' }); +export const mockHeaders1 = JSON.stringify({ foo: 'bar' }); + export const mockOperationName1 = 'Test'; export const mockOperationName2 = 'Test2'; From 0fe587aae31d310323c0617316ca9459e016c6bd Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 17:46:08 -0600 Subject: [PATCH 21/28] Add a test for the history queue saving when changing headers. --- .../components/__tests__/GraphiQL.spec.tsx | 28 +++++++++++++++++++ .../src/components/__tests__/fixtures.ts | 1 + 2 files changed, 29 insertions(+) diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index a6c2a7e4dea..131d991f3e0 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -19,6 +19,7 @@ import { mockQuery2, mockVariables2, mockHeaders1, + mockHeaders2, } from './fixtures'; codeMirrorModules.forEach(m => jest.mock(m, () => {})); @@ -278,6 +279,33 @@ describe('GraphiQL', () => { expect(container.querySelectorAll('.history-label')).toHaveLength(2); }); + it('will save query if headers are different ', () => { + const { getByTitle, getByText, container } = render( + , + ); + const executeQueryButton = getByTitle('Execute Query (Ctrl-Enter)'); + fireEvent.click(executeQueryButton); + expect(container.querySelectorAll('.history-label')).toHaveLength(1); + + fireEvent.click(getByText("Request Headers")); + + fireEvent.change( + container.querySelector('[aria-label="Request Headers"] .mockCodeMirror'), + { + target: { value: mockHeaders2 }, + }, + ); + + fireEvent.click(executeQueryButton); + expect(container.querySelectorAll('.history-label')).toHaveLength(2); + }); + describe('children overrides', () => { const MyFunctionalComponent = () => { return null; diff --git a/packages/graphiql/src/components/__tests__/fixtures.ts b/packages/graphiql/src/components/__tests__/fixtures.ts index 5fbe8f4895c..10dd4fbed3d 100644 --- a/packages/graphiql/src/components/__tests__/fixtures.ts +++ b/packages/graphiql/src/components/__tests__/fixtures.ts @@ -20,6 +20,7 @@ export const mockVariables1 = JSON.stringify({ string: 'string' }); export const mockVariables2 = JSON.stringify({ string: 'string2' }); export const mockHeaders1 = JSON.stringify({ foo: 'bar' }); +export const mockHeaders2 = JSON.stringify({ foo: 'baz' }); export const mockOperationName1 = 'Test'; export const mockOperationName2 = 'Test2'; From 5e5ee111aeec19d0ba32008451ddb8dbe9d41803 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 17:53:39 -0600 Subject: [PATCH 22/28] Fix a test that depended on a specific ID. You can only have one ID on a given HTML element, so I chose to use secondary-editor-title as the ID, rather than the old variable-editor-title. --- packages/graphiql/src/components/GraphiQL.tsx | 2 +- .../graphiql/src/components/__tests__/GraphiQL.spec.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 52e4b0e95f7..472a767fafb 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -568,7 +568,7 @@ export class GraphiQL extends React.Component { }>
{ expect(queryVariables.style.height).toEqual(''); - const variableEditorTitle = container1.querySelector( - '#variable-editor-title', + const secondaryEditorTitle = container1.querySelector( + '#secondary-editor-title', ); - fireEvent.mouseDown(variableEditorTitle); - fireEvent.mouseMove(variableEditorTitle); + fireEvent.mouseDown(secondaryEditorTitle); + fireEvent.mouseMove(secondaryEditorTitle); expect(queryVariables.style.height).toEqual('200px'); const { container: container2 } = render( From d9a511830198bd907bac2eb7ff918910c806f6ec Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Sat, 23 May 2020 17:56:55 -0600 Subject: [PATCH 23/28] chore: fix linter errors --- .../graphiql/src/components/__tests__/GraphiQL.spec.tsx | 2 +- .../src/components/__tests__/HistoryQuery.spec.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index e827bae691c..6b165888ced 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -293,7 +293,7 @@ describe('GraphiQL', () => { fireEvent.click(executeQueryButton); expect(container.querySelectorAll('.history-label')).toHaveLength(1); - fireEvent.click(getByText("Request Headers")); + fireEvent.click(getByText('Request Headers')); fireEvent.change( container.querySelector('[aria-label="Request Headers"] .mockCodeMirror'), diff --git a/packages/graphiql/src/components/__tests__/HistoryQuery.spec.tsx b/packages/graphiql/src/components/__tests__/HistoryQuery.spec.tsx index aa455077ecc..23642273b33 100644 --- a/packages/graphiql/src/components/__tests__/HistoryQuery.spec.tsx +++ b/packages/graphiql/src/components/__tests__/HistoryQuery.spec.tsx @@ -8,7 +8,12 @@ import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import HistoryQuery, { HistoryQueryProps } from '../HistoryQuery'; -import { mockOperationName1, mockQuery1, mockVariables1, mockHeaders1 } from './fixtures'; +import { + mockOperationName1, + mockQuery1, + mockVariables1, + mockHeaders1, +} from './fixtures'; const noOp = () => {}; @@ -19,7 +24,7 @@ const baseMockProps = { onSelect: noOp, query: mockQuery1, variables: mockVariables1, - headers: mockHeaders1 + headers: mockHeaders1, }; function getMockProps( From 4c1c74742a44714acd968d0e65445a98a2d15484 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 27 May 2020 17:25:58 -0600 Subject: [PATCH 24/28] fix: change the second argument to opts so it can be expanded later --- packages/graphiql/resources/renderExample.js | 17 ++++++++++------- packages/graphiql/src/components/GraphiQL.tsx | 8 ++++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index 91bdfbdd114..ab037f4624a 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -91,7 +91,7 @@ function updateURL() { // Defines a GraphQL fetcher using the fetch API. You're not required to // use fetch, and could instead implement graphQLFetcher however you like, // as long as it returns a Promise or Observable. -function graphQLFetcher(graphQLParams, headers = {}) { +function graphQLFetcher(graphQLParams, opts = { headers: {} }) { // When working locally, the example expects a GraphQL server at the path /graphql. // In a PR preview, it connects to the Star Wars API externally. // Change this to point wherever you host your GraphQL server. @@ -101,16 +101,19 @@ function graphQLFetcher(graphQLParams, headers = {}) { : 'https://swapi-graphql.netlify.app/.netlify/functions/index'; // Convert headers to an object. - if (typeof headers === 'string') { - headers = JSON.parse(headers); + if (typeof opts.headers === 'string') { + headers = JSON.parse(opts.headers); } return fetch(api, { method: 'post', - headers: Object.assign({ - Accept: 'application/json', - 'Content-Type': 'application/json', - }, headers), + headers: Object.assign( + { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + headers, + ), body: JSON.stringify(graphQLParams), credentials: 'omit', }) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 2997d0aecf7..d17e84163ac 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -73,8 +73,12 @@ export type FetcherParams = { query: string; operationName: string; variables?: string; - headers?: string; }; + +export type FetcherOpts = { + headers?: { [key: string]: any }; +}; + export type FetcherResult = | { data: IntrospectionQuery; @@ -84,7 +88,7 @@ export type FetcherResult = export type Fetcher = ( graphQLParams: FetcherParams, - headers?: Object, + opts?: FetcherOpts, ) => Promise | Observable; type OnMouseMoveFn = Maybe< From d771071578b39f23a9b8b8068a676aceaeb8a8d4 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 27 May 2020 17:29:22 -0600 Subject: [PATCH 25/28] fix: headers needs to actually be defined --- packages/graphiql/resources/renderExample.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index ab037f4624a..d7d55f97bf7 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -100,8 +100,9 @@ function graphQLFetcher(graphQLParams, opts = { headers: {} }) { ? '/graphql' : 'https://swapi-graphql.netlify.app/.netlify/functions/index'; + let headers = opts.headers; // Convert headers to an object. - if (typeof opts.headers === 'string') { + if (typeof headers === 'string') { headers = JSON.parse(opts.headers); } From f699127c67808915384cbe7e755189012f6b20e5 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 27 May 2020 17:30:11 -0600 Subject: [PATCH 26/28] improvement: change the active/inactive tabs to grey and black --- packages/graphiql/src/components/GraphiQL.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index d17e84163ac..249ef71bdd5 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -597,7 +597,7 @@ export class GraphiQL extends React.Component {
{
Date: Wed, 27 May 2020 17:48:00 -0600 Subject: [PATCH 27/28] fix: pass the headers as part of an object --- packages/graphiql/src/components/GraphiQL.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 249ef71bdd5..88c10e9236a 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -932,7 +932,7 @@ export class GraphiQL extends React.Component { variables: jsonVariables, operationName, }, - jsonHeaders, + { headers: jsonHeaders }, ); if (isPromise(fetch)) { From ff8b7d952c6c8750971e51be30ee18ee064c75df Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 27 May 2020 18:39:36 -0600 Subject: [PATCH 28/28] improvement: set the enableHeaderEditor prop to false by default --- packages/graphiql/README.md | 3 +++ packages/graphiql/resources/renderExample.js | 1 + packages/graphiql/src/components/GraphiQL.tsx | 2 +- packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/graphiql/README.md b/packages/graphiql/README.md index 268be691a63..cbbe789dc87 100644 --- a/packages/graphiql/README.md +++ b/packages/graphiql/README.md @@ -199,6 +199,8 @@ GraphiQL supports customization in UI and behavior by accepting React props and - `docExplorerOpen`: an optional boolean which when `true` will ensure the `DocExplorer` is open by default when the user first renders the component. If the user has toggled the doc explorer on/off following this, however, the persisted UI state will override this default flag. +- `headerEditorEnabled`: an optional boolean which enables the header editor when `true`. Defaults to `false`. + ### Children (dropped as of 1.0.0-rc.2) - ``: Replace the GraphiQL logo with your own. @@ -259,6 +261,7 @@ class CustomGraphiQL extends React.Component { // Custom Event Handlers onEditQuery: null, onEditVariables: null, + onEditHeaders: null, onEditOperationName: null, // GraphiQL automatically fills in leaf nodes when the query diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js index d7d55f97bf7..fb02c646aa1 100644 --- a/packages/graphiql/resources/renderExample.js +++ b/packages/graphiql/resources/renderExample.js @@ -146,6 +146,7 @@ ReactDOM.render( onEditHeaders: onEditHeaders, defaultVariableEditorOpen: true, onEditOperationName: onEditOperationName, + headerEditorEnabled: true, }), document.getElementById('graphiql'), ); diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 88c10e9236a..d2b5c8a2d74 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -252,7 +252,7 @@ export class GraphiQL extends React.Component { secondaryEditorOpen = Boolean(variables || headers); } - const headerEditorEnabled = props.headerEditorEnabled ?? true; + const headerEditorEnabled = props.headerEditorEnabled ?? false; // Initialize state this.state = { diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index 6b165888ced..3f3a55a6ec0 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -287,6 +287,7 @@ describe('GraphiQL', () => { query={mockQuery1} variables={mockVariables1} headers={mockHeaders1} + headerEditorEnabled />, ); const executeQueryButton = getByTitle('Execute Query (Ctrl-Enter)');