-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add extensions to request payload & UI #3409
base: main
Are you sure you want to change the base?
Changes from all commits
1c4d93a
bc6e80a
8a4d2a3
5f2124e
19f94ad
9172636
ef4c207
91ea8b5
96fabcd
eef1f03
da66056
69c24ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'graphiql': minor | ||
'@graphiql/react': minor | ||
'@graphiql/toolkit': minor | ||
--- | ||
|
||
Add extensions to GraphQL request payload and add Extensions tab to UI |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,7 +44,7 @@ this repo._ | |
If you are focused on GraphiQL development, you can run — | ||
|
||
```sh | ||
yarn start-graphiql | ||
yarn dev-graphiql | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updating the documentation |
||
``` | ||
|
||
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
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 ( | ||
<div className={clsx('graphiql-editor', isHidden && 'hidden')} ref={ref} /> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,8 @@ export function EditorContextProvider(props: EditorContextProviderProps) { | |
const [variableEditor, setVariableEditor] = useState<CodeMirrorEditor | null>( | ||
null, | ||
); | ||
const [extensionEditor, setExtensionEditor] = | ||
useState<CodeMirrorEditor | null>(null); | ||
|
||
const [shouldPersistHeaders, setShouldPersistHeadersInternal] = useState( | ||
() => { | ||
|
@@ -288,6 +312,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { | |
useSynchronizeValue(queryEditor, props.query); | ||
useSynchronizeValue(responseEditor, props.response); | ||
useSynchronizeValue(variableEditor, props.variables); | ||
useSynchronizeValue(extensionEditor, props.extensions); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. awesome stuff! i would prefer this to be possible via improvements to the plugin ecosystem, however There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's an example to illustrate: say you have 4 tabs, each with the exact same query/variables/headers combination, but each with unique extension values. when you reload the browser, you'll only have one tab, because without extensions, the persistence only finds one unique operation context |
||
|
||
const storeTabs = useStoreTabs({ | ||
storage, | ||
|
@@ -300,12 +325,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 +349,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 +386,14 @@ export function EditorContextProvider(props: EditorContextProviderProps) { | |
const synchronizeActiveTabValues = useSynchronizeActiveTabValues({ | ||
queryEditor, | ||
variableEditor, | ||
extensionEditor, | ||
headerEditor, | ||
responseEditor, | ||
}); | ||
const setEditorValues = useSetEditorValues({ | ||
queryEditor, | ||
variableEditor, | ||
extensionEditor, | ||
headerEditor, | ||
responseEditor, | ||
}); | ||
|
@@ -504,15 +535,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 +568,7 @@ export function EditorContextProvider(props: EditorContextProviderProps) { | |
queryEditor, | ||
responseEditor, | ||
variableEditor, | ||
extensionEditor, | ||
|
||
setOperationName, | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
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<HTMLDivElement>(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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I set these as minor changes but I am happy to be corrected. I chose minor as
extensions
key in the request payload)FetcherParams
are backwards compatible, it is fine to not setextensions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dondonz agreed on the minor release, and users will appreciate the backwards compatibility!