From 1c4d93a2096d735cd8b072b63646a4948a24863a Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Tue, 22 Aug 2023 11:15:12 +1000 Subject: [PATCH 01/12] Add extensions to request payload in fetcher --- packages/graphiql-toolkit/src/create-fetcher/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graphiql-toolkit/src/create-fetcher/types.ts b/packages/graphiql-toolkit/src/create-fetcher/types.ts index 9ae06a67beb..70f154814d2 100644 --- a/packages/graphiql-toolkit/src/create-fetcher/types.ts +++ b/packages/graphiql-toolkit/src/create-fetcher/types.ts @@ -28,6 +28,7 @@ export type FetcherParams = { query: string; operationName?: string | null; variables?: any; + extensions?: any; }; export type FetcherOpts = { From bc6e80a134a98d44e5e0c2a57d0a6c3fd9c02eaa Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Tue, 22 Aug 2023 11:17:39 +1000 Subject: [PATCH 02/12] Add basic extensions input and propagate props --- .../src/editor/__tests__/tabs.spec.ts | 2 + .../editor/components/extension-editor.tsx | 37 +++++ .../src/editor/components/index.ts | 1 + .../graphiql-react/src/editor/context.tsx | 36 +++++ .../src/editor/extension-editor.ts | 131 ++++++++++++++++++ packages/graphiql-react/src/editor/index.ts | 3 + packages/graphiql-react/src/editor/tabs.ts | 26 +++- packages/graphiql-react/src/execution.tsx | 17 +++ packages/graphiql-react/src/index.ts | 3 + packages/graphiql/src/components/GraphiQL.tsx | 8 ++ 10 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 packages/graphiql-react/src/editor/components/extension-editor.tsx create mode 100644 packages/graphiql-react/src/editor/extension-editor.ts diff --git a/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts index 0314d220f9d..2133d4c19f3 100644 --- a/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts +++ b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts @@ -110,6 +110,7 @@ describe('getDefaultTabState', () => { headers: null, query: null, variables: null, + extensions: null, storage: null, }), ).toEqual({ @@ -142,6 +143,7 @@ describe('getDefaultTabState', () => { ], query: null, variables: null, + extensions: null, storage: null, }), ).toEqual({ diff --git a/packages/graphiql-react/src/editor/components/extension-editor.tsx b/packages/graphiql-react/src/editor/components/extension-editor.tsx new file mode 100644 index 00000000000..e36a01688fa --- /dev/null +++ b/packages/graphiql-react/src/editor/components/extension-editor.tsx @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; +import { clsx } from 'clsx'; + +import { useEditorContext } from '../context'; +import { useExtensionEditor, UseExtensionEditorArgs } from '../extension-editor'; + +import '../style/codemirror.css'; +import '../style/fold.css'; +import '../style/lint.css'; +import '../style/hint.css'; +import '../style/editor.css'; + +type ExtensionEditorProps = UseExtensionEditorArgs & { + /** + * Visually hide the header editor. + * @default false + */ + isHidden?: boolean; +}; + +export function ExtensionEditor({ isHidden, ...hookArgs }: ExtensionEditorProps) { + const { extensionEditor } = useEditorContext({ + nonNull: true, + caller: ExtensionEditor, + }); + const ref = useExtensionEditor(hookArgs, ExtensionEditor); + + useEffect(() => { + if (extensionEditor && !isHidden) { + extensionEditor.refresh(); + } + }, [extensionEditor, isHidden]); + + return ( +
+ ); +} diff --git a/packages/graphiql-react/src/editor/components/index.ts b/packages/graphiql-react/src/editor/components/index.ts index 9fbe6db2a47..adc9647457e 100644 --- a/packages/graphiql-react/src/editor/components/index.ts +++ b/packages/graphiql-react/src/editor/components/index.ts @@ -3,3 +3,4 @@ export { ImagePreview } from './image-preview'; export { QueryEditor } from './query-editor'; export { ResponseEditor } from './response-editor'; export { VariableEditor } from './variable-editor'; +export { ExtensionEditor } from './extension-editor'; diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 8515cb8c763..4ffdbf43b3a 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -19,6 +19,7 @@ import { import { useStorageContext } from '../storage'; import { createContextHook, createNullableContext } from '../utility/context'; import { STORAGE_KEY as STORAGE_KEY_HEADERS } from './header-editor'; +import { STORAGE_KEY as STORAGE_KEY_EXTENSIONS } from './extension-editor'; import { useSynchronizeValue } from './hooks'; import { STORAGE_KEY_QUERY } from './query-editor'; import { @@ -96,6 +97,10 @@ export type EditorContextType = TabsState & { * The CodeMirror editor instance for the variables editor. */ variableEditor: CodeMirrorEditor | null; + /** + * The CodeMirror editor instance for the extensions editor. + */ + extensionEditor: CodeMirrorEditor | null; /** * Set the CodeMirror editor instance for the headers editor. */ @@ -112,6 +117,10 @@ export type EditorContextType = TabsState & { * Set the CodeMirror editor instance for the variables editor. */ setVariableEditor(newEditor: CodeMirrorEditor): void; + /** + * Set the CodeMirror editor instance for the extensions editor. + */ + setExtensionEditor(newEditor: CodeMirrorEditor): void; /** * Changes the operation name and invokes the `onEditOperationName` callback. @@ -138,6 +147,11 @@ export type EditorContextType = TabsState & { * component. */ initialVariables: string; + /** + * The contents of the extensions editor when initially rendering the provider + * component. + */ + initialExtensions: string; /** * A map of fragment definitions using the fragment name as key which are @@ -255,6 +269,14 @@ export type EditorContextProviderProps = { */ variables?: string; + /** + * This prop can be used to set the contents of the extensions editor. Every + * time this prop changes, the contents of the extensions editor are replaced. + * Note that the editor contents can be changed in between these updates by + * typing in the editor. + */ + extensions?: string; + /** * Headers to be set when opening a new tab */ @@ -274,6 +296,9 @@ export function EditorContextProvider(props: EditorContextProviderProps) { const [variableEditor, setVariableEditor] = useState( null, ); + const [extensionEditor, setExtensionEditor] = useState( + null, + ); const [shouldPersistHeaders, setShouldPersistHeadersInternal] = useState( () => { @@ -288,6 +313,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { useSynchronizeValue(queryEditor, props.query); useSynchronizeValue(responseEditor, props.response); useSynchronizeValue(variableEditor, props.variables); + useSynchronizeValue(extensionEditor, props.extensions); const storeTabs = useStoreTabs({ storage, @@ -300,12 +326,15 @@ export function EditorContextProvider(props: EditorContextProviderProps) { const query = props.query ?? storage?.get(STORAGE_KEY_QUERY) ?? null; const variables = props.variables ?? storage?.get(STORAGE_KEY_VARIABLES) ?? null; + const extensions = + props.variables ?? storage?.get(STORAGE_KEY_EXTENSIONS) ?? null; const headers = props.headers ?? storage?.get(STORAGE_KEY_HEADERS) ?? null; const response = props.response ?? ''; const tabState = getDefaultTabState({ query, variables, + extensions, headers, defaultTabs: props.defaultTabs, defaultQuery: props.defaultQuery || DEFAULT_QUERY, @@ -321,6 +350,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { (tabState.activeTabIndex === 0 ? tabState.tabs[0].query : null) ?? '', variables: variables ?? '', + extensions: extensions ?? '', headers: headers ?? props.defaultHeaders ?? '', response, tabState, @@ -357,12 +387,14 @@ export function EditorContextProvider(props: EditorContextProviderProps) { const synchronizeActiveTabValues = useSynchronizeActiveTabValues({ queryEditor, variableEditor, + extensionEditor, headerEditor, responseEditor, }); const setEditorValues = useSetEditorValues({ queryEditor, variableEditor, + extensionEditor, headerEditor, responseEditor, }); @@ -504,15 +536,18 @@ export function EditorContextProvider(props: EditorContextProviderProps) { queryEditor, responseEditor, variableEditor, + extensionEditor, setHeaderEditor, setQueryEditor, setResponseEditor, setVariableEditor, + setExtensionEditor, setOperationName, initialQuery: initialState.query, initialVariables: initialState.variables, + initialExtensions: initialState.extensions, initialHeaders: initialState.headers, initialResponse: initialState.response, @@ -534,6 +569,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { queryEditor, responseEditor, variableEditor, + extensionEditor, setOperationName, diff --git a/packages/graphiql-react/src/editor/extension-editor.ts b/packages/graphiql-react/src/editor/extension-editor.ts new file mode 100644 index 00000000000..19006b49adf --- /dev/null +++ b/packages/graphiql-react/src/editor/extension-editor.ts @@ -0,0 +1,131 @@ +import { useEffect, useRef } from 'react'; + +import { useExecutionContext } from '../execution'; +import { + commonKeys, + DEFAULT_EDITOR_THEME, + DEFAULT_KEY_MAP, + importCodeMirror, +} from './common'; +import { useEditorContext } from './context'; +import { + useChangeHandler, + useKeyMap, + useMergeQuery, + usePrettifyEditors, + useSynchronizeOption, +} from './hooks'; +import { WriteableEditorProps } from './types'; + +export type UseExtensionEditorArgs = WriteableEditorProps & { + /** + * Invoked when the contents of the extension editor change. + * @param value The new contents of the editor. + */ + onEdit?(value: string): void; +}; + +export function useExtensionEditor( + { + editorTheme = DEFAULT_EDITOR_THEME, + keyMap = DEFAULT_KEY_MAP, + onEdit, + readOnly = false, + }: UseExtensionEditorArgs = {}, + caller?: Function, +) { + const { + initialExtensions, + extensionEditor, + setExtensionEditor, + } = useEditorContext({ + nonNull: true, + caller: caller || useExtensionEditor, + }); + const executionContext = useExecutionContext(); + const merge = useMergeQuery({ caller: caller || useExtensionEditor }); + const prettify = usePrettifyEditors({ caller: caller || useExtensionEditor }); + const ref = useRef(null); + + useEffect(() => { + let isActive = true; + + void importCodeMirror([ + // @ts-expect-error + import('codemirror/mode/javascript/javascript'), + ]).then(CodeMirror => { + // Don't continue if the effect has already been cleaned up + if (!isActive) { + return; + } + + const container = ref.current; + if (!container) { + return; + } + + const newEditor = CodeMirror(container, { + value: initialExtensions, + lineNumbers: true, + tabSize: 2, + mode: { name: 'javascript', json: true }, + theme: editorTheme, + autoCloseBrackets: true, + matchBrackets: true, + showCursorWhenSelecting: true, + readOnly: readOnly ? 'nocursor' : false, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + extraKeys: commonKeys, + }); + + newEditor.addKeyMap({ + 'Cmd-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + 'Ctrl-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + 'Alt-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + 'Shift-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + }); + + newEditor.on('keyup', (editorInstance, event) => { + const { code, key, shiftKey } = event; + const isLetter = code.startsWith('Key'); + const isNumber = !shiftKey && code.startsWith('Digit'); + if (isLetter || isNumber || key === '_' || key === '"') { + editorInstance.execCommand('autocomplete'); + } + }); + + setExtensionEditor(newEditor); + }); + + return () => { + isActive = false; + }; + }, [editorTheme, initialExtensions, readOnly, setExtensionEditor]); + + useSynchronizeOption(extensionEditor, 'keyMap', keyMap); + + useChangeHandler( + extensionEditor, + onEdit, + STORAGE_KEY, + 'extensions', + useExtensionEditor, + ); + + useKeyMap(extensionEditor, ['Cmd-Enter', 'Ctrl-Enter'], executionContext?.run); + useKeyMap(extensionEditor, ['Shift-Ctrl-P'], prettify); + useKeyMap(extensionEditor, ['Shift-Ctrl-M'], merge); + + return ref; +} + +export const STORAGE_KEY = 'extensions'; diff --git a/packages/graphiql-react/src/editor/index.ts b/packages/graphiql-react/src/editor/index.ts index 8a4f8fe1687..11d8612296a 100644 --- a/packages/graphiql-react/src/editor/index.ts +++ b/packages/graphiql-react/src/editor/index.ts @@ -4,6 +4,7 @@ export { QueryEditor, ResponseEditor, VariableEditor, + ExtensionEditor, } from './components'; export { EditorContext, @@ -22,6 +23,7 @@ export { export { useQueryEditor } from './query-editor'; export { useResponseEditor } from './response-editor'; export { useVariableEditor } from './variable-editor'; +export { useExtensionEditor } from './extension-editor'; export type { EditorContextType, EditorContextProviderProps } from './context'; export type { UseHeaderEditorArgs } from './header-editor'; @@ -32,5 +34,6 @@ export type { } from './response-editor'; export type { TabsState } from './tabs'; export type { UseVariableEditorArgs } from './variable-editor'; +export type { UseExtensionEditorArgs } from './extension-editor'; export type { CommonEditorProps, KeyMap, WriteableEditorProps } from './types'; diff --git a/packages/graphiql-react/src/editor/tabs.ts b/packages/graphiql-react/src/editor/tabs.ts index b9110dd5135..1bd0ba67a00 100644 --- a/packages/graphiql-react/src/editor/tabs.ts +++ b/packages/graphiql-react/src/editor/tabs.ts @@ -14,6 +14,10 @@ export type TabDefinition = { * The contents of the variable editor of this tab. */ variables?: string | null; + /** + * The contents of the extensions editor of this tab. + */ + extensions?: string | null; /** * The contents of the headers editor of this tab. */ @@ -30,7 +34,7 @@ export type TabState = TabDefinition & { id: string; /** * A hash that is unique for a combination of the contents of the query - * editor, the variable editor and the header editor (i.e. all the editor + * editor, the variable editor, the extension editor, and the header editor (i.e. all the editor * where the contents are persisted in storage). */ hash: string; @@ -71,6 +75,7 @@ export function getDefaultTabState({ defaultTabs, query, variables, + extensions, storage, shouldPersistHeaders, }: { @@ -80,6 +85,7 @@ export function getDefaultTabState({ defaultTabs?: TabDefinition[]; query: string | null; variables: string | null; + extensions: string | null; storage: StorageAPI | null; shouldPersistHeaders?: boolean; }) { @@ -96,6 +102,7 @@ export function getDefaultTabState({ const expectedHash = hashFromTabContents({ query, variables, + extensions, headers: headersForHash, }); let matchingTabIndex = -1; @@ -105,6 +112,7 @@ export function getDefaultTabState({ tab.hash = hashFromTabContents({ query: tab.query, variables: tab.variables, + extensions: tab.extensions, headers: tab.headers, }); if (tab.hash === expectedHash) { @@ -191,11 +199,13 @@ function hasStringOrNullKey(obj: Record, key: string) { export function useSynchronizeActiveTabValues({ queryEditor, variableEditor, + extensionEditor, headerEditor, responseEditor, }: { queryEditor: CodeMirrorEditorWithOperationFacts | null; variableEditor: CodeMirrorEditor | null; + extensionEditor: CodeMirrorEditor | null; headerEditor: CodeMirrorEditor | null; responseEditor: CodeMirrorEditor | null; }) { @@ -203,12 +213,14 @@ export function useSynchronizeActiveTabValues({ state => { const query = queryEditor?.getValue() ?? null; const variables = variableEditor?.getValue() ?? null; + const extensions = extensionEditor?.getValue() ?? null; const headers = headerEditor?.getValue() ?? null; const operationName = queryEditor?.operationName ?? null; const response = responseEditor?.getValue() ?? null; return setPropertiesInActiveTab(state, { query, variables, + extensions, headers, response, operationName, @@ -256,11 +268,13 @@ export function useStoreTabs({ export function useSetEditorValues({ queryEditor, variableEditor, + extensionEditor, headerEditor, responseEditor, }: { queryEditor: CodeMirrorEditorWithOperationFacts | null; variableEditor: CodeMirrorEditor | null; + extensionEditor: CodeMirrorEditor | null; headerEditor: CodeMirrorEditor | null; responseEditor: CodeMirrorEditor | null; }) { @@ -268,26 +282,30 @@ export function useSetEditorValues({ ({ query, variables, + extensions, headers, response, }: { query: string | null; variables?: string | null; + extensions?: string | null; headers?: string | null; response: string | null; }) => { queryEditor?.setValue(query ?? ''); variableEditor?.setValue(variables ?? ''); + extensionEditor?.setValue(extensions ?? ''); headerEditor?.setValue(headers ?? ''); responseEditor?.setValue(response ?? ''); }, - [headerEditor, queryEditor, responseEditor, variableEditor], + [headerEditor, queryEditor, responseEditor, variableEditor, extensionEditor], ); } export function createTab({ query = null, variables = null, + extensions = null, headers = null, }: Partial = {}): TabState { return { @@ -296,6 +314,7 @@ export function createTab({ title: (query && fuzzyExtractOperationName(query)) || DEFAULT_TITLE, query, variables, + extensions, headers, operationName: null, response: null, @@ -340,9 +359,10 @@ function guid(): string { function hashFromTabContents(args: { query: string | null; variables?: string | null; + extensions?: string | null; headers?: string | null; }): string { - return [args.query ?? '', args.variables ?? '', args.headers ?? ''].join('|'); + return [args.query ?? '', args.variables ?? '', args.extensions ?? '', args.headers ?? ''].join('|'); } export function fuzzyExtractOperationName(str: string): string | null { diff --git a/packages/graphiql-react/src/execution.tsx b/packages/graphiql-react/src/execution.tsx index d66b4eea78b..5d8abb1eb5e 100644 --- a/packages/graphiql-react/src/execution.tsx +++ b/packages/graphiql-react/src/execution.tsx @@ -87,6 +87,7 @@ export function ExecutionContextProvider({ queryEditor, responseEditor, variableEditor, + extensionEditor, updateActiveTabValues, } = useEditorContext({ nonNull: true, caller: ExecutionContextProvider }); const history = useHistoryContext(); @@ -141,6 +142,19 @@ export function ExecutionContextProvider({ return; } + const extensionsString = extensionEditor?.getValue(); + let extensions: Record | undefined; + try { + extensions = tryParseJsonObject({ + json: extensionsString, + errorMessageParse: 'Extensions are invalid JSON', + errorMessageType: 'Extensions are not a JSON object.', + }); + } catch (error) { + setResponse(error instanceof Error ? error.message : `${error}`); + return; + } + const headersString = headerEditor?.getValue(); let headers: Record | undefined; try { @@ -178,6 +192,7 @@ export function ExecutionContextProvider({ history?.addToHistory({ query, variables: variablesString, + // dz todo when adding extensions to history headers: headersString, operationName: opName, }); @@ -251,6 +266,7 @@ export function ExecutionContextProvider({ { query, variables, + extensions, operationName: opName, }, { @@ -312,6 +328,7 @@ export function ExecutionContextProvider({ subscription, updateActiveTabValues, variableEditor, + extensionEditor, ]); const isSubscribed = Boolean(subscription); diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts index 26cf7756193..155408af31f 100644 --- a/packages/graphiql-react/src/index.ts +++ b/packages/graphiql-react/src/index.ts @@ -19,6 +19,8 @@ export { useOperationsEditorState, useVariablesEditorState, VariableEditor, + ExtensionEditor, + useExtensionEditor, } from './editor'; export { ExecutionContext, @@ -84,6 +86,7 @@ export type { UseQueryEditorArgs, UseResponseEditorArgs, UseVariableEditorArgs, + UseExtensionEditorArgs, WriteableEditorProps, } from './editor'; export type { diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index 0bb9cd1c5f2..8e35b9f0fe1 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -678,6 +678,14 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { readOnly={props.readOnly} /> )} +
From 8a4d2a37cb6262b301fc1503a1f8733c560e8aff Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Tue, 22 Aug 2023 11:54:46 +1000 Subject: [PATCH 03/12] Add extensions tab to main GraphiQL --- packages/graphiql-react/src/editor/hooks.ts | 2 +- packages/graphiql-react/src/provider.tsx | 2 + packages/graphiql/src/components/GraphiQL.tsx | 62 ++++++++++++++----- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/packages/graphiql-react/src/editor/hooks.ts b/packages/graphiql-react/src/editor/hooks.ts index a9c37f5da68..a12bcd1343b 100644 --- a/packages/graphiql-react/src/editor/hooks.ts +++ b/packages/graphiql-react/src/editor/hooks.ts @@ -41,7 +41,7 @@ export function useChangeHandler( editor: CodeMirrorEditor | null, callback: ((value: string) => void) | undefined, storageKey: string | null, - tabProperty: 'variables' | 'headers', + tabProperty: 'variables' | 'headers' | 'extensions', caller: Function, ) { const { updateActiveTabValues } = useEditorContext({ nonNull: true, caller }); diff --git a/packages/graphiql-react/src/provider.tsx b/packages/graphiql-react/src/provider.tsx index ead1907de8e..0cf8d59167e 100644 --- a/packages/graphiql-react/src/provider.tsx +++ b/packages/graphiql-react/src/provider.tsx @@ -47,6 +47,7 @@ export function GraphiQLProvider({ storage, validationRules, variables, + extensions, visiblePlugin, }: GraphiQLProviderProps) { return ( @@ -65,6 +66,7 @@ export function GraphiQLProvider({ shouldPersistHeaders={shouldPersistHeaders} validationRules={validationRules} variables={variables} + extensions={extensions} > , 'Query'> & Pick & AddSuffix, 'Variables'> & + AddSuffix, 'Extensions'> & AddSuffix, 'Headers'> & Pick & { children?: ReactNode; @@ -189,11 +194,12 @@ export type GraphiQLInterfaceProps = WriteableEditorProps & * - `false` hides the editor tools * - `true` shows the editor tools * - `'variables'` specifically shows the variables editor + * - `'extensions'` specifically shows the extensions editor * - `'headers'` specifically shows the headers editor * By default the editor tools are initially shown when at least one of the * editors has contents. */ - defaultEditorToolsVisibility?: boolean | 'variables' | 'headers'; + defaultEditorToolsVisibility?: boolean | 'variables' | 'extensions' | 'headers'; /** * Toggle if the headers editor should be shown inside the editor tools. * @default true @@ -249,6 +255,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { initiallyHidden: (() => { if ( props.defaultEditorToolsVisibility === 'variables' || + props.defaultEditorToolsVisibility === 'extensions' || props.defaultEditorToolsVisibility === 'headers' ) { return; @@ -258,7 +265,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { return props.defaultEditorToolsVisibility ? undefined : 'second'; } - return editorContext.initialVariables || editorContext.initialHeaders + return (editorContext.initialVariables || editorContext.initialExtensions || editorContext.initialHeaders) ? undefined : 'second'; })(), @@ -267,19 +274,24 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { }); const [activeSecondaryEditor, setActiveSecondaryEditor] = useState< - 'variables' | 'headers' + 'variables' | 'extensions' | 'headers' >(() => { if ( props.defaultEditorToolsVisibility === 'variables' || + props.defaultEditorToolsVisibility === 'extensions' || props.defaultEditorToolsVisibility === 'headers' ) { return props.defaultEditorToolsVisibility; } - return !editorContext.initialVariables && - editorContext.initialHeaders && - isHeadersEditorEnabled - ? 'headers' - : 'variables'; + + if (editorContext.initialVariables) { + return 'variables'; + } if (editorContext.initialHeaders && isHeadersEditorEnabled) { + return 'headers'; + } if (editorContext.initialExtensions) { + return 'extensions'; + } + return 'variables'; }); const [showDialog, setShowDialog] = useState< 'settings' | 'short-keys' | null @@ -318,6 +330,16 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { isChildComponentType(child, GraphiQL.Footer), ); + const tabName = (currentTab: String) => { + if (currentTab === 'variables') { + return 'Variables'; + } if (currentTab === 'extensions') { + return 'Extensions'; + } if (currentTab === 'headers') { + return 'Headers'; + } + }; + const onClickReference = useCallback(() => { if (pluginResize.hiddenElement === 'first') { pluginResize.setHiddenElement(null); @@ -390,7 +412,7 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { editorToolsResize.setHiddenElement(null); } setActiveSecondaryEditor( - event.currentTarget.dataset.name as 'variables' | 'headers', + event.currentTarget.dataset.name as 'variables' | 'extensions' | 'headers', ); }, [editorToolsResize], @@ -618,6 +640,19 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { Headers )} + + Extensions +
Date: Tue, 22 Aug 2023 16:35:28 +1000 Subject: [PATCH 04/12] Add extensions to history --- packages/graphiql-react/src/execution.tsx | 2 +- packages/graphiql-react/src/history/context.tsx | 7 +++++-- packages/graphiql-toolkit/src/storage/history.ts | 11 ++++++++++- packages/graphiql-toolkit/src/storage/query.ts | 5 +++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/graphiql-react/src/execution.tsx b/packages/graphiql-react/src/execution.tsx index 5d8abb1eb5e..91e1d56e86b 100644 --- a/packages/graphiql-react/src/execution.tsx +++ b/packages/graphiql-react/src/execution.tsx @@ -192,7 +192,7 @@ export function ExecutionContextProvider({ history?.addToHistory({ query, variables: variablesString, - // dz todo when adding extensions to history + extensions: extensionsString, headers: headersString, operationName: opName, }); diff --git a/packages/graphiql-react/src/history/context.tsx b/packages/graphiql-react/src/history/context.tsx index 1a51e4a8487..1c9fc7e8df2 100644 --- a/packages/graphiql-react/src/history/context.tsx +++ b/packages/graphiql-react/src/history/context.tsx @@ -8,11 +8,12 @@ export type HistoryContextType = { /** * Add an operation to the history. * @param operation The operation that was executed, consisting of the query, - * variables, headers, and operation name. + * variables, extensions, headers, and operation name. */ addToHistory(operation: { query?: string; variables?: string; + extensions?: string; headers?: string; operationName?: string; }): void; @@ -29,6 +30,7 @@ export type HistoryContextType = { args: { query?: string; variables?: string; + extensions?: string; headers?: string; operationName?: string; label?: string; @@ -50,6 +52,7 @@ export type HistoryContextType = { toggleFavorite(args: { query?: string; variables?: string; + extensions?: string; headers?: string; operationName?: string; label?: string; @@ -58,7 +61,7 @@ export type HistoryContextType = { /** * Delete an operation from the history. * @param args The operation that was executed, consisting of the query, - * variables, headers, and operation name. + * variables, extensions, headers, and operation name. * @param clearFavorites This is only if you press the 'clear' button */ deleteFromHistory(args: QueryStoreItem, clearFavorites?: boolean): void; diff --git a/packages/graphiql-toolkit/src/storage/history.ts b/packages/graphiql-toolkit/src/storage/history.ts index 7f374be3a5b..a9848db3fed 100644 --- a/packages/graphiql-toolkit/src/storage/history.ts +++ b/packages/graphiql-toolkit/src/storage/history.ts @@ -28,6 +28,7 @@ export class HistoryStore { private shouldSaveQuery( query?: string, variables?: string, + extensions?: string, headers?: string, lastQuerySaved?: QueryStoreItem, ) { @@ -71,6 +72,7 @@ export class HistoryStore { updateHistory = ({ query, variables, + extensions, headers, operationName, }: QueryStoreItem) => { @@ -78,6 +80,7 @@ export class HistoryStore { !this.shouldSaveQuery( query, variables, + extensions, headers, this.history.fetchRecent(), ) @@ -87,6 +90,7 @@ export class HistoryStore { this.history.push({ query, variables, + extensions, headers, operationName, }); @@ -98,6 +102,7 @@ export class HistoryStore { toggleFavorite({ query, variables, + extensions, headers, operationName, label, @@ -106,6 +111,7 @@ export class HistoryStore { const item: QueryStoreItem = { query, variables, + extensions, headers, operationName, label, @@ -126,6 +132,7 @@ export class HistoryStore { { query, variables, + extensions, headers, operationName, label, @@ -136,6 +143,7 @@ export class HistoryStore { const item = { query, variables, + extensions, headers, operationName, label, @@ -149,7 +157,7 @@ export class HistoryStore { } deleteHistory = ( - { query, variables, headers, operationName, favorite }: QueryStoreItem, + { query, variables, extensions, headers, operationName, favorite }: QueryStoreItem, clearFavorites = false, ) => { function deleteFromStore(store: QueryStore) { @@ -157,6 +165,7 @@ export class HistoryStore { x => x.query === query && x.variables === variables && + x.extensions === extensions && x.headers === headers && x.operationName === operationName, ); diff --git a/packages/graphiql-toolkit/src/storage/query.ts b/packages/graphiql-toolkit/src/storage/query.ts index 9fea0e93b4a..ea59590075c 100644 --- a/packages/graphiql-toolkit/src/storage/query.ts +++ b/packages/graphiql-toolkit/src/storage/query.ts @@ -3,6 +3,7 @@ import { StorageAPI } from './base'; export type QueryStoreItem = { query?: string; variables?: string; + extensions?: string; headers?: string; operationName?: string; label?: string; @@ -29,6 +30,7 @@ export class QueryStore { x => x.query === item.query && x.variables === item.variables && + x.extensions === item.extensions && x.headers === item.headers && x.operationName === item.operationName, ); @@ -40,6 +42,7 @@ export class QueryStore { if ( found.query === item.query && found.variables === item.variables && + found.extensions === item.extensions && found.headers === item.headers && found.operationName === item.operationName ) { @@ -53,6 +56,7 @@ export class QueryStore { x => x.query === item.query && x.variables === item.variables && + x.extensions === item.extensions && x.headers === item.headers && x.operationName === item.operationName, ); @@ -67,6 +71,7 @@ export class QueryStore { x => x.query === item.query && x.variables === item.variables && + x.extensions === item.extensions && x.headers === item.headers && x.operationName === item.operationName, ); From 19f94ad3ee1db3ecbd05b39177003d3cadb5da5e Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:14:40 +1000 Subject: [PATCH 05/12] Set extensions in history, add prettify --- packages/graphiql-react/src/editor/hooks.ts | 21 +++++++++++++++++-- packages/graphiql-react/src/editor/tabs.ts | 5 ++++- .../graphiql-react/src/history/components.tsx | 7 ++++--- .../graphiql-toolkit/src/storage/history.ts | 3 +-- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/graphiql-react/src/editor/hooks.ts b/packages/graphiql-react/src/editor/hooks.ts index a12bcd1343b..cb8d22a2997 100644 --- a/packages/graphiql-react/src/editor/hooks.ts +++ b/packages/graphiql-react/src/editor/hooks.ts @@ -212,7 +212,7 @@ type UsePrettifyEditorsArgs = { }; export function usePrettifyEditors({ caller }: UsePrettifyEditorsArgs = {}) { - const { queryEditor, headerEditor, variableEditor } = useEditorContext({ + const { queryEditor, headerEditor, variableEditor, extensionEditor } = useEditorContext({ nonNull: true, caller: caller || usePrettifyEditors, }); @@ -233,6 +233,23 @@ export function usePrettifyEditors({ caller }: UsePrettifyEditorsArgs = {}) { } } + if (extensionEditor) { + const extensionEditorContent = extensionEditor.getValue(); + + try { + const prettifiedExtensionEditorContent = JSON.stringify( + JSON.parse(extensionEditorContent), + null, + 2, + ); + if (prettifiedExtensionEditorContent !== extensionEditorContent) { + extensionEditor.setValue(prettifiedExtensionEditorContent); + } + } catch { + /* Parsing JSON failed, skip prettification */ + } + } + if (headerEditor) { const headerEditorContent = headerEditor.getValue(); @@ -258,7 +275,7 @@ export function usePrettifyEditors({ caller }: UsePrettifyEditorsArgs = {}) { queryEditor.setValue(prettifiedEditorContent); } } - }, [queryEditor, variableEditor, headerEditor]); + }, [queryEditor, variableEditor, headerEditor, extensionEditor]); } export type UseAutoCompleteLeafsArgs = { diff --git a/packages/graphiql-react/src/editor/tabs.ts b/packages/graphiql-react/src/editor/tabs.ts index 1bd0ba67a00..d87391fec51 100644 --- a/packages/graphiql-react/src/editor/tabs.ts +++ b/packages/graphiql-react/src/editor/tabs.ts @@ -130,6 +130,7 @@ export function getDefaultTabState({ title: operationName || DEFAULT_TITLE, query, variables, + extensions, headers, operationName, response: null, @@ -148,6 +149,7 @@ export function getDefaultTabState({ { query: query ?? defaultQuery, variables, + extensions, headers: headers ?? defaultHeaders, }, ] @@ -178,6 +180,7 @@ function isTabState(obj: any): obj is TabState { hasStringKey(obj, 'title') && hasStringOrNullKey(obj, 'query') && hasStringOrNullKey(obj, 'variables') && + hasStringOrNullKey(obj, 'extensions') && hasStringOrNullKey(obj, 'headers') && hasStringOrNullKey(obj, 'operationName') && hasStringOrNullKey(obj, 'response') @@ -226,7 +229,7 @@ export function useSynchronizeActiveTabValues({ operationName, }); }, - [queryEditor, variableEditor, headerEditor, responseEditor], + [queryEditor, variableEditor, extensionEditor, headerEditor, responseEditor], ); } diff --git a/packages/graphiql-react/src/history/components.tsx b/packages/graphiql-react/src/history/components.tsx index 9ee49574be3..06f82025d36 100644 --- a/packages/graphiql-react/src/history/components.tsx +++ b/packages/graphiql-react/src/history/components.tsx @@ -112,7 +112,7 @@ export function HistoryItem(props: QueryHistoryItemProps) { nonNull: true, caller: HistoryItem, }); - const { headerEditor, queryEditor, variableEditor } = useEditorContext({ + const { headerEditor, queryEditor, variableEditor, extensionEditor } = useEditorContext({ nonNull: true, caller: HistoryItem, }); @@ -151,12 +151,13 @@ export function HistoryItem(props: QueryHistoryItemProps) { const handleHistoryItemClick: MouseEventHandler = useCallback(() => { - const { query, variables, headers } = props.item; + const { query, variables, extensions, headers } = props.item; queryEditor?.setValue(query ?? ''); variableEditor?.setValue(variables ?? ''); + extensionEditor?.setValue(extensions ?? ''); headerEditor?.setValue(headers ?? ''); setActive(props.item); - }, [headerEditor, props.item, queryEditor, setActive, variableEditor]); + }, [headerEditor, props.item, queryEditor, setActive, variableEditor, extensionEditor]); const handleDeleteItemFromHistory: MouseEventHandler = useCallback( diff --git a/packages/graphiql-toolkit/src/storage/history.ts b/packages/graphiql-toolkit/src/storage/history.ts index a9848db3fed..dc101eabbaa 100644 --- a/packages/graphiql-toolkit/src/storage/history.ts +++ b/packages/graphiql-toolkit/src/storage/history.ts @@ -28,7 +28,7 @@ export class HistoryStore { private shouldSaveQuery( query?: string, variables?: string, - extensions?: string, + // extensions?: string, // dz todo: ask about saving strategy for extensions headers?: string, lastQuerySaved?: QueryStoreItem, ) { @@ -80,7 +80,6 @@ export class HistoryStore { !this.shouldSaveQuery( query, variables, - extensions, headers, this.history.fetchRecent(), ) From 917263668fd93fbafd1f71734364d82333aba83f Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 23 Aug 2023 10:29:24 +1000 Subject: [PATCH 06/12] Add GraphiQL.spec test --- .../src/components/__tests__/GraphiQL.spec.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index 8eacd3b6719..0472f2c6240 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -239,6 +239,18 @@ describe('GraphiQL', () => { }); }); + it('correctly displays extensions editor when using defaultEditorToolsVisibility prop', async () => { + const { container } = render( + , + ); + await waitFor(() => { + expect(container.querySelector('[aria-label="Extensions"]')).toBeVisible(); + }); + }); + it('correctly displays headers editor when using defaultEditorToolsVisibility prop', async () => { const { container } = render( Date: Wed, 23 Aug 2023 10:34:44 +1000 Subject: [PATCH 07/12] Add components spec test --- .../src/history/__tests__/components.spec.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/graphiql-react/src/history/__tests__/components.spec.tsx b/packages/graphiql-react/src/history/__tests__/components.spec.tsx index 602e68d9e89..2dab29a2df7 100644 --- a/packages/graphiql-react/src/history/__tests__/components.spec.tsx +++ b/packages/graphiql-react/src/history/__tests__/components.spec.tsx @@ -8,12 +8,14 @@ import { Tooltip } from '../../ui'; jest.mock('../../editor', () => { const mockedSetQueryEditor = jest.fn(); const mockedSetVariableEditor = jest.fn(); + const mockedSetExtensionEditor = jest.fn(); const mockedSetHeaderEditor = jest.fn(); return { useEditorContext() { return { queryEditor: { setValue: mockedSetQueryEditor }, variableEditor: { setValue: mockedSetVariableEditor }, + extensionEditor: { setValue: mockedSetExtensionEditor }, headerEditor: { setValue: mockedSetHeaderEditor }, }; }, @@ -30,6 +32,8 @@ const mockQuery = /* GraphQL */ ` const mockVariables = JSON.stringify({ string: 'string' }); +const mockExtensions = JSON.stringify({ myExtension: 'myString' }); + const mockHeaders = JSON.stringify({ foo: 'bar' }); const mockOperationName = 'Test'; @@ -50,6 +54,7 @@ const baseMockProps: QueryHistoryItemProps = { item: { query: mockQuery, variables: mockVariables, + extensions: mockExtensions, headers: mockHeaders, favorite: false, }, @@ -70,11 +75,14 @@ describe('QueryHistoryItem', () => { ?.setValue as jest.Mock; const mockedSetVariableEditor = useEditorContext()?.variableEditor ?.setValue as jest.Mock; + const mockedSetExtensionEditor = useEditorContext()?.extensionEditor + ?.setValue as jest.Mock; const mockedSetHeaderEditor = useEditorContext()?.headerEditor ?.setValue as jest.Mock; beforeEach(() => { mockedSetQueryEditor.mockClear(); mockedSetVariableEditor.mockClear(); + mockedSetExtensionEditor.mockClear(); mockedSetHeaderEditor.mockClear(); }); it('renders operationName if label is not provided', () => { @@ -112,6 +120,10 @@ describe('QueryHistoryItem', () => { expect(mockedSetVariableEditor).toHaveBeenCalledWith( mockProps.item.variables, ); + expect(mockedSetExtensionEditor).toHaveBeenCalledTimes(1); + expect(mockedSetExtensionEditor).toHaveBeenCalledWith( + mockProps.item.extensions, + ); expect(mockedSetHeaderEditor).toHaveBeenCalledTimes(1); expect(mockedSetHeaderEditor).toHaveBeenCalledWith(mockProps.item.headers); }); From 91ea8b5ab80a8eaed97dff65a209bb766eb7b85f Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 23 Aug 2023 10:38:35 +1000 Subject: [PATCH 08/12] Add tabs test --- packages/graphiql-react/src/editor/__tests__/tabs.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts index 2133d4c19f3..f8d6726cdb7 100644 --- a/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts +++ b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts @@ -139,6 +139,7 @@ describe('getDefaultTabState', () => { headers: '{"x-header":"foo"}', query: 'query Image { image }', variables: null, + extensions: '{"myExtension":"myString"}', }, ], query: null, @@ -158,6 +159,7 @@ describe('getDefaultTabState', () => { headers: '{"x-header":"foo"}', query: 'query Image { image }', title: 'Image', + extensions: '{"myExtension":"myString"}', }), ], }); From 96fabcd521e1c9881f41c9f5d8e69e9dfa245ae3 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 23 Aug 2023 10:52:41 +1000 Subject: [PATCH 09/12] Add extensions to Cypress setup and tabs e2e test --- packages/graphiql/cypress/e2e/tabs.cy.ts | 9 ++++++ packages/graphiql/cypress/support/commands.ts | 29 +++++++++++++------ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/graphiql/cypress/e2e/tabs.cy.ts b/packages/graphiql/cypress/e2e/tabs.cy.ts index 4ad14f76d14..a7f0c78cfd9 100644 --- a/packages/graphiql/cypress/e2e/tabs.cy.ts +++ b/packages/graphiql/cypress/e2e/tabs.cy.ts @@ -31,6 +31,12 @@ describe('Tabs', () => { .eq(1) .type('{"someHeader":"someValue"', { force: true }); + // Enter extensions + cy.contains('Extensions').click(); + cy.get('.graphiql-editor-tool textarea') + .eq(2) + .type('{"myExtension":"myString"', { force: true }); + // Run the query cy.clickExecuteQuery(); @@ -45,6 +51,7 @@ describe('Tabs', () => { cy.assertHasValues({ query: '{id}', variablesString: '', + extensionsString: '', headersString: '', response: { data: { id: 'abc123' } }, }); @@ -60,6 +67,7 @@ describe('Tabs', () => { cy.assertHasValues({ query: 'query Foo {image}', variablesString: '{"someVar":42}', + extensionsString: '{"myExtension":"myString"}', headersString: '{"someHeader":"someValue"}', response: { data: { image: '/images/logo.svg' } }, }); @@ -74,6 +82,7 @@ describe('Tabs', () => { cy.assertHasValues({ query: '{id}', variablesString: '', + extensionsString: '', headersString: '', response: { data: { id: 'abc123' } }, }); diff --git a/packages/graphiql/cypress/support/commands.ts b/packages/graphiql/cypress/support/commands.ts index 0f2254afbf9..6075e50788e 100644 --- a/packages/graphiql/cypress/support/commands.ts +++ b/packages/graphiql/cypress/support/commands.ts @@ -14,6 +14,7 @@ type Op = { query: string; variables?: Record; variablesString?: string; + extensionsString?: string; headersString?: string; response?: Record; }; @@ -73,7 +74,7 @@ Cypress.Commands.add('visitWithOp', ({ query, variables, variablesString }) => { Cypress.Commands.add( 'assertHasValues', - ({ query, variables, variablesString, headersString, response }: Op) => { + ({ query, variables, variablesString, extensionsString, headersString, response }: Op) => { cy.get('.graphiql-query-editor').should(element => { expect(normalize(element.get(0).innerText)).to.equal( codeWithLineNumbers(query), @@ -100,14 +101,24 @@ Cypress.Commands.add( }); } if (headersString !== undefined) { - cy.contains('Headers').click(); - cy.get('.graphiql-editor-tool .graphiql-editor') - .eq(1) - .should(element => { - expect(normalize(element.get(0).innerText)).to.equal( - codeWithLineNumbers(headersString), - ); - }); + cy.contains('Headers').click(); + cy.get('.graphiql-editor-tool .graphiql-editor') + .eq(1) + .should(element => { + expect(normalize(element.get(0).innerText)).to.equal( + codeWithLineNumbers(headersString), + ); + }); + } + if (extensionsString !== undefined) { + cy.contains('Extensions').click(); + cy.get('.graphiql-editor-tool .graphiql-editor') + .eq(2) + .should(element => { + expect(normalize(element.get(0).innerText)).to.equal( + codeWithLineNumbers(extensionsString), + ); + }); } if (response !== undefined) { cy.get('.result-window').should(element => { From eef1f03290e8eaa601917f83fab1caccbece6ab7 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:47:49 +1000 Subject: [PATCH 10/12] Fix documentation --- DEVELOPMENT.md | 4 ++-- packages/graphiql-react/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index b4c17ab4d7b..f8670047823 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -44,7 +44,7 @@ this repo._ If you are focused on GraphiQL development, you can run — ```sh - yarn start-graphiql + yarn dev-graphiql ``` 5. Get coding! If you've added code, add tests. If you've changed APIs, update @@ -89,7 +89,7 @@ First, you'll need to `yarn build` all the packages from the root. Then, you can run these commands: -- `yarn start-graphiql` — which will launch `webpack` dev server for graphiql +- `yarn dev-graphiql` — which will launch `webpack` dev server for graphiql from the root > The GraphiQL UI is available at http://localhost:8080/dev.html diff --git a/packages/graphiql-react/README.md b/packages/graphiql-react/README.md index 5bf7b262082..54ac1e2615e 100644 --- a/packages/graphiql-react/README.md +++ b/packages/graphiql-react/README.md @@ -130,6 +130,6 @@ elements background. If you want to develop with `@graphiql/react` locally - in particular when working on the `graphiql` package - all you need to do is run `yarn dev` in the package folder in a separate terminal. This will build the package using Vite. -When using it in combination with `yarn start-graphiql` (running in the repo +When using it in combination with `yarn dev-graphiql` (running in the repo root) this will give you auto-reloading when working on `graphiql` and `@graphiql/react` simultaneously. From da66056ec77a9a4bd35052a56c5e741bf77f4a41 Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 23 Aug 2023 14:51:22 +1000 Subject: [PATCH 11/12] Prettify code changes --- .../editor/components/extension-editor.tsx | 46 ++++++++++-------- .../graphiql-react/src/editor/context.tsx | 7 ++- .../src/editor/extension-editor.ts | 43 ++++++++--------- packages/graphiql-react/src/editor/hooks.ts | 15 +++--- packages/graphiql-react/src/editor/tabs.ts | 23 +++++++-- .../src/history/__tests__/components.spec.tsx | 4 +- .../graphiql-react/src/history/components.tsx | 18 +++++-- .../graphiql-toolkit/src/storage/history.ts | 9 +++- packages/graphiql/cypress/e2e/tabs.cy.ts | 4 +- packages/graphiql/cypress/support/commands.ts | 41 +++++++++------- packages/graphiql/src/components/GraphiQL.tsx | 47 ++++++++++++------- .../components/__tests__/GraphiQL.spec.tsx | 12 +++-- 12 files changed, 165 insertions(+), 104 deletions(-) diff --git a/packages/graphiql-react/src/editor/components/extension-editor.tsx b/packages/graphiql-react/src/editor/components/extension-editor.tsx index e36a01688fa..ddb499b5f9d 100644 --- a/packages/graphiql-react/src/editor/components/extension-editor.tsx +++ b/packages/graphiql-react/src/editor/components/extension-editor.tsx @@ -2,7 +2,10 @@ import { useEffect } from 'react'; import { clsx } from 'clsx'; import { useEditorContext } from '../context'; -import { useExtensionEditor, UseExtensionEditorArgs } from '../extension-editor'; +import { + useExtensionEditor, + UseExtensionEditorArgs, +} from '../extension-editor'; import '../style/codemirror.css'; import '../style/fold.css'; @@ -11,27 +14,30 @@ import '../style/hint.css'; import '../style/editor.css'; type ExtensionEditorProps = UseExtensionEditorArgs & { - /** - * Visually hide the header editor. - * @default false - */ - isHidden?: boolean; + /** + * Visually hide the header editor. + * @default false + */ + isHidden?: boolean; }; -export function ExtensionEditor({ isHidden, ...hookArgs }: ExtensionEditorProps) { - const { extensionEditor } = useEditorContext({ - nonNull: true, - caller: ExtensionEditor, - }); - const ref = useExtensionEditor(hookArgs, ExtensionEditor); +export function ExtensionEditor({ + isHidden, + ...hookArgs +}: ExtensionEditorProps) { + const { extensionEditor } = useEditorContext({ + nonNull: true, + caller: ExtensionEditor, + }); + const ref = useExtensionEditor(hookArgs, ExtensionEditor); - useEffect(() => { - if (extensionEditor && !isHidden) { - extensionEditor.refresh(); - } - }, [extensionEditor, isHidden]); + useEffect(() => { + if (extensionEditor && !isHidden) { + extensionEditor.refresh(); + } + }, [extensionEditor, isHidden]); - return ( -
- ); + return ( +
+ ); } diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index 4ffdbf43b3a..aeaf8f6ff5c 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -296,9 +296,8 @@ export function EditorContextProvider(props: EditorContextProviderProps) { const [variableEditor, setVariableEditor] = useState( null, ); - const [extensionEditor, setExtensionEditor] = useState( - null, - ); + const [extensionEditor, setExtensionEditor] = + useState(null); const [shouldPersistHeaders, setShouldPersistHeadersInternal] = useState( () => { @@ -327,7 +326,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { const variables = props.variables ?? storage?.get(STORAGE_KEY_VARIABLES) ?? null; const extensions = - props.variables ?? storage?.get(STORAGE_KEY_EXTENSIONS) ?? null; + props.variables ?? storage?.get(STORAGE_KEY_EXTENSIONS) ?? null; const headers = props.headers ?? storage?.get(STORAGE_KEY_HEADERS) ?? null; const response = props.response ?? ''; diff --git a/packages/graphiql-react/src/editor/extension-editor.ts b/packages/graphiql-react/src/editor/extension-editor.ts index 19006b49adf..59019127513 100644 --- a/packages/graphiql-react/src/editor/extension-editor.ts +++ b/packages/graphiql-react/src/editor/extension-editor.ts @@ -26,22 +26,19 @@ export type UseExtensionEditorArgs = WriteableEditorProps & { }; export function useExtensionEditor( - { - editorTheme = DEFAULT_EDITOR_THEME, - keyMap = DEFAULT_KEY_MAP, - onEdit, - readOnly = false, - }: UseExtensionEditorArgs = {}, - caller?: Function, + { + editorTheme = DEFAULT_EDITOR_THEME, + keyMap = DEFAULT_KEY_MAP, + onEdit, + readOnly = false, + }: UseExtensionEditorArgs = {}, + caller?: Function, ) { - const { - initialExtensions, - extensionEditor, - setExtensionEditor, - } = useEditorContext({ - nonNull: true, - caller: caller || useExtensionEditor, - }); + const { initialExtensions, extensionEditor, setExtensionEditor } = + useEditorContext({ + nonNull: true, + caller: caller || useExtensionEditor, + }); const executionContext = useExecutionContext(); const merge = useMergeQuery({ caller: caller || useExtensionEditor }); const prettify = usePrettifyEditors({ caller: caller || useExtensionEditor }); @@ -114,14 +111,18 @@ export function useExtensionEditor( useSynchronizeOption(extensionEditor, 'keyMap', keyMap); useChangeHandler( - extensionEditor, - onEdit, - STORAGE_KEY, - 'extensions', - useExtensionEditor, + extensionEditor, + onEdit, + STORAGE_KEY, + 'extensions', + useExtensionEditor, ); - useKeyMap(extensionEditor, ['Cmd-Enter', 'Ctrl-Enter'], executionContext?.run); + useKeyMap( + extensionEditor, + ['Cmd-Enter', 'Ctrl-Enter'], + executionContext?.run, + ); useKeyMap(extensionEditor, ['Shift-Ctrl-P'], prettify); useKeyMap(extensionEditor, ['Shift-Ctrl-M'], merge); diff --git a/packages/graphiql-react/src/editor/hooks.ts b/packages/graphiql-react/src/editor/hooks.ts index cb8d22a2997..fc0ee7d7f48 100644 --- a/packages/graphiql-react/src/editor/hooks.ts +++ b/packages/graphiql-react/src/editor/hooks.ts @@ -212,10 +212,11 @@ type UsePrettifyEditorsArgs = { }; export function usePrettifyEditors({ caller }: UsePrettifyEditorsArgs = {}) { - const { queryEditor, headerEditor, variableEditor, extensionEditor } = useEditorContext({ - nonNull: true, - caller: caller || usePrettifyEditors, - }); + const { queryEditor, headerEditor, variableEditor, extensionEditor } = + useEditorContext({ + nonNull: true, + caller: caller || usePrettifyEditors, + }); return useCallback(() => { if (variableEditor) { const variableEditorContent = variableEditor.getValue(); @@ -238,9 +239,9 @@ export function usePrettifyEditors({ caller }: UsePrettifyEditorsArgs = {}) { try { const prettifiedExtensionEditorContent = JSON.stringify( - JSON.parse(extensionEditorContent), - null, - 2, + JSON.parse(extensionEditorContent), + null, + 2, ); if (prettifiedExtensionEditorContent !== extensionEditorContent) { extensionEditor.setValue(prettifiedExtensionEditorContent); diff --git a/packages/graphiql-react/src/editor/tabs.ts b/packages/graphiql-react/src/editor/tabs.ts index d87391fec51..5c3280b7883 100644 --- a/packages/graphiql-react/src/editor/tabs.ts +++ b/packages/graphiql-react/src/editor/tabs.ts @@ -229,7 +229,13 @@ export function useSynchronizeActiveTabValues({ operationName, }); }, - [queryEditor, variableEditor, extensionEditor, headerEditor, responseEditor], + [ + queryEditor, + variableEditor, + extensionEditor, + headerEditor, + responseEditor, + ], ); } @@ -301,7 +307,13 @@ export function useSetEditorValues({ headerEditor?.setValue(headers ?? ''); responseEditor?.setValue(response ?? ''); }, - [headerEditor, queryEditor, responseEditor, variableEditor, extensionEditor], + [ + headerEditor, + queryEditor, + responseEditor, + variableEditor, + extensionEditor, + ], ); } @@ -365,7 +377,12 @@ function hashFromTabContents(args: { extensions?: string | null; headers?: string | null; }): string { - return [args.query ?? '', args.variables ?? '', args.extensions ?? '', args.headers ?? ''].join('|'); + return [ + args.query ?? '', + args.variables ?? '', + args.extensions ?? '', + args.headers ?? '', + ].join('|'); } export function fuzzyExtractOperationName(str: string): string | null { diff --git a/packages/graphiql-react/src/history/__tests__/components.spec.tsx b/packages/graphiql-react/src/history/__tests__/components.spec.tsx index 2dab29a2df7..e623a8dde1c 100644 --- a/packages/graphiql-react/src/history/__tests__/components.spec.tsx +++ b/packages/graphiql-react/src/history/__tests__/components.spec.tsx @@ -76,7 +76,7 @@ describe('QueryHistoryItem', () => { const mockedSetVariableEditor = useEditorContext()?.variableEditor ?.setValue as jest.Mock; const mockedSetExtensionEditor = useEditorContext()?.extensionEditor - ?.setValue as jest.Mock; + ?.setValue as jest.Mock; const mockedSetHeaderEditor = useEditorContext()?.headerEditor ?.setValue as jest.Mock; beforeEach(() => { @@ -122,7 +122,7 @@ describe('QueryHistoryItem', () => { ); expect(mockedSetExtensionEditor).toHaveBeenCalledTimes(1); expect(mockedSetExtensionEditor).toHaveBeenCalledWith( - mockProps.item.extensions, + mockProps.item.extensions, ); expect(mockedSetHeaderEditor).toHaveBeenCalledTimes(1); expect(mockedSetHeaderEditor).toHaveBeenCalledWith(mockProps.item.headers); diff --git a/packages/graphiql-react/src/history/components.tsx b/packages/graphiql-react/src/history/components.tsx index 06f82025d36..e8819b2e30a 100644 --- a/packages/graphiql-react/src/history/components.tsx +++ b/packages/graphiql-react/src/history/components.tsx @@ -112,10 +112,11 @@ export function HistoryItem(props: QueryHistoryItemProps) { nonNull: true, caller: HistoryItem, }); - const { headerEditor, queryEditor, variableEditor, extensionEditor } = useEditorContext({ - nonNull: true, - caller: HistoryItem, - }); + const { headerEditor, queryEditor, variableEditor, extensionEditor } = + useEditorContext({ + nonNull: true, + caller: HistoryItem, + }); const inputRef = useRef(null); const buttonRef = useRef(null); const [isEditable, setIsEditable] = useState(false); @@ -157,7 +158,14 @@ export function HistoryItem(props: QueryHistoryItemProps) { extensionEditor?.setValue(extensions ?? ''); headerEditor?.setValue(headers ?? ''); setActive(props.item); - }, [headerEditor, props.item, queryEditor, setActive, variableEditor, extensionEditor]); + }, [ + headerEditor, + props.item, + queryEditor, + setActive, + variableEditor, + extensionEditor, + ]); const handleDeleteItemFromHistory: MouseEventHandler = useCallback( diff --git a/packages/graphiql-toolkit/src/storage/history.ts b/packages/graphiql-toolkit/src/storage/history.ts index dc101eabbaa..03d61fdcb3c 100644 --- a/packages/graphiql-toolkit/src/storage/history.ts +++ b/packages/graphiql-toolkit/src/storage/history.ts @@ -156,7 +156,14 @@ export class HistoryStore { } deleteHistory = ( - { query, variables, extensions, headers, operationName, favorite }: QueryStoreItem, + { + query, + variables, + extensions, + headers, + operationName, + favorite, + }: QueryStoreItem, clearFavorites = false, ) => { function deleteFromStore(store: QueryStore) { diff --git a/packages/graphiql/cypress/e2e/tabs.cy.ts b/packages/graphiql/cypress/e2e/tabs.cy.ts index a7f0c78cfd9..434fdab2883 100644 --- a/packages/graphiql/cypress/e2e/tabs.cy.ts +++ b/packages/graphiql/cypress/e2e/tabs.cy.ts @@ -34,8 +34,8 @@ describe('Tabs', () => { // Enter extensions cy.contains('Extensions').click(); cy.get('.graphiql-editor-tool textarea') - .eq(2) - .type('{"myExtension":"myString"', { force: true }); + .eq(2) + .type('{"myExtension":"myString"', { force: true }); // Run the query cy.clickExecuteQuery(); diff --git a/packages/graphiql/cypress/support/commands.ts b/packages/graphiql/cypress/support/commands.ts index 6075e50788e..6d38f55814d 100644 --- a/packages/graphiql/cypress/support/commands.ts +++ b/packages/graphiql/cypress/support/commands.ts @@ -74,7 +74,14 @@ Cypress.Commands.add('visitWithOp', ({ query, variables, variablesString }) => { Cypress.Commands.add( 'assertHasValues', - ({ query, variables, variablesString, extensionsString, headersString, response }: Op) => { + ({ + query, + variables, + variablesString, + extensionsString, + headersString, + response, + }: Op) => { cy.get('.graphiql-query-editor').should(element => { expect(normalize(element.get(0).innerText)).to.equal( codeWithLineNumbers(query), @@ -101,24 +108,24 @@ Cypress.Commands.add( }); } if (headersString !== undefined) { - cy.contains('Headers').click(); - cy.get('.graphiql-editor-tool .graphiql-editor') - .eq(1) - .should(element => { - expect(normalize(element.get(0).innerText)).to.equal( - codeWithLineNumbers(headersString), - ); - }); + cy.contains('Headers').click(); + cy.get('.graphiql-editor-tool .graphiql-editor') + .eq(1) + .should(element => { + expect(normalize(element.get(0).innerText)).to.equal( + codeWithLineNumbers(headersString), + ); + }); } if (extensionsString !== undefined) { - cy.contains('Extensions').click(); - cy.get('.graphiql-editor-tool .graphiql-editor') - .eq(2) - .should(element => { - expect(normalize(element.get(0).innerText)).to.equal( - codeWithLineNumbers(extensionsString), - ); - }); + cy.contains('Extensions').click(); + cy.get('.graphiql-editor-tool .graphiql-editor') + .eq(2) + .should(element => { + expect(normalize(element.get(0).innerText)).to.equal( + codeWithLineNumbers(extensionsString), + ); + }); } if (response !== undefined) { cy.get('.result-window').should(element => { diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx index c10c7c1a84b..ddca4c54ff2 100644 --- a/packages/graphiql/src/components/GraphiQL.tsx +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -199,7 +199,11 @@ export type GraphiQLInterfaceProps = WriteableEditorProps & * By default the editor tools are initially shown when at least one of the * editors has contents. */ - defaultEditorToolsVisibility?: boolean | 'variables' | 'extensions' | 'headers'; + defaultEditorToolsVisibility?: + | boolean + | 'variables' + | 'extensions' + | 'headers'; /** * Toggle if the headers editor should be shown inside the editor tools. * @default true @@ -265,7 +269,9 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { return props.defaultEditorToolsVisibility ? undefined : 'second'; } - return (editorContext.initialVariables || editorContext.initialExtensions || editorContext.initialHeaders) + return editorContext.initialVariables || + editorContext.initialExtensions || + editorContext.initialHeaders ? undefined : 'second'; })(), @@ -286,9 +292,11 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { if (editorContext.initialVariables) { return 'variables'; - } if (editorContext.initialHeaders && isHeadersEditorEnabled) { + } + if (editorContext.initialHeaders && isHeadersEditorEnabled) { return 'headers'; - } if (editorContext.initialExtensions) { + } + if (editorContext.initialExtensions) { return 'extensions'; } return 'variables'; @@ -333,13 +341,15 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { const tabName = (currentTab: String) => { if (currentTab === 'variables') { return 'Variables'; - } if (currentTab === 'extensions') { + } + if (currentTab === 'extensions') { return 'Extensions'; - } if (currentTab === 'headers') { + } + if (currentTab === 'headers') { return 'Headers'; } }; - + const onClickReference = useCallback(() => { if (pluginResize.hiddenElement === 'first') { pluginResize.setHiddenElement(null); @@ -412,7 +422,10 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { editorToolsResize.setHiddenElement(null); } setActiveSecondaryEditor( - event.currentTarget.dataset.name as 'variables' | 'extensions' | 'headers', + event.currentTarget.dataset.name as + | 'variables' + | 'extensions' + | 'headers', ); }, [editorToolsResize], @@ -641,15 +654,15 @@ export function GraphiQLInterface(props: GraphiQLInterfaceProps) { )} Extensions diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx index 0472f2c6240..6ded4219e77 100644 --- a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -241,13 +241,15 @@ describe('GraphiQL', () => { it('correctly displays extensions editor when using defaultEditorToolsVisibility prop', async () => { const { container } = render( - , + , ); await waitFor(() => { - expect(container.querySelector('[aria-label="Extensions"]')).toBeVisible(); + expect( + container.querySelector('[aria-label="Extensions"]'), + ).toBeVisible(); }); }); From 69c24ffe50445bd55d84e7937090c6383c14f7fe Mon Sep 17 00:00:00 2001 From: dondonz <13839920+dondonz@users.noreply.github.com> Date: Wed, 23 Aug 2023 15:15:58 +1000 Subject: [PATCH 12/12] Add changeset --- .changeset/tricky-lions-try.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/tricky-lions-try.md diff --git a/.changeset/tricky-lions-try.md b/.changeset/tricky-lions-try.md new file mode 100644 index 00000000000..ea766149ba2 --- /dev/null +++ b/.changeset/tricky-lions-try.md @@ -0,0 +1,7 @@ +--- +'graphiql': minor +'@graphiql/react': minor +'@graphiql/toolkit': minor +--- + +Add extensions to GraphQL request payload and add Extensions tab to UI