diff --git a/.changeset/chilly-sloths-heal.md b/.changeset/chilly-sloths-heal.md new file mode 100644 index 00000000000..7b7eed930bc --- /dev/null +++ b/.changeset/chilly-sloths-heal.md @@ -0,0 +1,27 @@ +--- +'@graphiql/plugin-doc-explorer': patch +'@graphiql/plugin-explorer': patch +'graphql-language-service': patch +'@graphiql/plugin-history': patch +'codemirror-graphql': patch +'@graphiql/react': minor +'graphiql': patch +--- + +- replace `onCopyQuery` hook with `copyQuery` function +- replace `onMergeQuery` hook with `mergeQuery` function +- replace `onPrettifyEditors` hook with `prettifyEditors` function +- remove `fetcher` prop from `SchemaContextProvider` and `schemaStore` and add `fetcher` to `executionStore` +- add `onCopyQuery` and `onPrettifyQuery` props to `EditorContextProvider` +- remove exports (use `GraphiQLProvider`) + - `EditorContextProvider` + - `ExecutionContextProvider` + - `PluginContextProvider` + - `SchemaContextProvider` + - `StorageContextProvider` + - `ExecutionContextType` + - `PluginContextType` +- feat(@graphiql/react): migrate React context to zustand: + - replace `useExecutionContext` with `useExecutionStore` hook + - replace `useEditorContext` with `useEditorStore` hook +- prefer `getComputedStyle` over `window.getComputedStyle` diff --git a/.eslintrc.js b/.eslintrc.js index 5f94c62ea4b..9d097d4f6d7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -142,6 +142,11 @@ module.exports = { property: 'navigator', message: 'Use `navigator` instead', }, + { + object: 'window', + property: 'getComputedStyle', + message: 'Use `getComputedStyle` instead', + }, ], 'no-return-assign': 'error', 'no-return-await': 'error', diff --git a/packages/codemirror-graphql/src/utils/info-addon.ts b/packages/codemirror-graphql/src/utils/info-addon.ts index cff916607ce..d1837d1927a 100644 --- a/packages/codemirror-graphql/src/utils/info-addon.ts +++ b/packages/codemirror-graphql/src/utils/info-addon.ts @@ -119,17 +119,19 @@ function showPopup(cm: CodeMirror.Editor, box: DOMRect, info: HTMLDivElement) { document.body.append(popup); const popupBox = popup.getBoundingClientRect(); - const popupStyle = window.getComputedStyle(popup); + const { marginLeft, marginRight, marginBottom, marginTop } = + getComputedStyle(popup); + const popupWidth = popupBox.right - popupBox.left + - parseFloat(popupStyle.marginLeft) + - parseFloat(popupStyle.marginRight); + parseFloat(marginLeft) + + parseFloat(marginRight); const popupHeight = popupBox.bottom - popupBox.top + - parseFloat(popupStyle.marginTop) + - parseFloat(popupStyle.marginBottom); + parseFloat(marginTop) + + parseFloat(marginBottom); let topPos = box.bottom; if ( diff --git a/packages/graphiql-plugin-doc-explorer/src/components/search.tsx b/packages/graphiql-plugin-doc-explorer/src/components/search.tsx index 2e215dc1355..4a3d9b923aa 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/search.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/search.tsx @@ -158,7 +158,7 @@ type FieldMatch = { export function useSearchResults() { const explorerNavStack = useDocExplorer(); - const { schema } = useSchemaStore(); + const schema = useSchemaStore(store => store.schema); const navItem = explorerNavStack.at(-1)!; diff --git a/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx b/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx index 29dde9b71f2..b95867efbc0 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx @@ -215,7 +215,7 @@ const EnumValue: FC<{ value: GraphQLEnumValue }> = ({ value }) => { }; const PossibleTypes: FC<{ type: GraphQLNamedType }> = ({ type }) => { - const { schema } = useSchemaStore(); + const schema = useSchemaStore(store => store.schema); if (!schema || !isAbstractType(type)) { return null; } diff --git a/packages/graphiql-plugin-doc-explorer/src/index.tsx b/packages/graphiql-plugin-doc-explorer/src/index.tsx index 076c484dfae..2b34c809eeb 100644 --- a/packages/graphiql-plugin-doc-explorer/src/index.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/index.tsx @@ -24,7 +24,7 @@ export type { export const DOC_EXPLORER_PLUGIN: GraphiQLPlugin = { title: 'Documentation Explorer', icon: function Icon() { - const { visiblePlugin } = usePluginStore(); + const visiblePlugin = usePluginStore(store => store.visiblePlugin); return visiblePlugin === DOC_EXPLORER_PLUGIN ? ( ) : ( diff --git a/packages/graphiql-plugin-explorer/src/index.tsx b/packages/graphiql-plugin-explorer/src/index.tsx index 8e35d6cd870..15e70f990ce 100644 --- a/packages/graphiql-plugin-explorer/src/index.tsx +++ b/packages/graphiql-plugin-explorer/src/index.tsx @@ -62,9 +62,9 @@ export type GraphiQLExplorerPluginProps = Omit< >; const ExplorerPlugin: FC = props => { - const { setOperationName } = useEditorStore(); - const { schema } = useSchemaStore(); - const { run } = useExecutionStore(); + const setOperationName = useEditorStore(store => store.setOperationName); + const schema = useSchemaStore(store => store.schema); + const run = useExecutionStore(store => store.run); // handle running the current operation from the plugin const handleRunOperation = useCallback( diff --git a/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx b/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx index d0d913bb2ed..a81b238badc 100644 --- a/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx +++ b/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx @@ -1,34 +1,9 @@ -import type { Mock } from 'vitest'; import { fireEvent, render } from '@testing-library/react'; import type { ComponentProps } from 'react'; import { formatQuery, HistoryItem } from '../components'; import { HistoryContextProvider } from '../context'; -import { - useEditorStore, - Tooltip, - StorageContextProvider, -} from '@graphiql/react'; - -vi.mock('@graphiql/react', async () => { - const originalModule = await vi.importActual('@graphiql/react'); - const mockedSetQueryEditor = vi.fn(); - const mockedSetVariableEditor = vi.fn(); - const mockedSetHeaderEditor = vi.fn(); - return { - ...originalModule, - useEditorStore() { - return { - queryEditor: { setValue: mockedSetQueryEditor }, - variableEditor: { setValue: mockedSetVariableEditor }, - headerEditor: { setValue: mockedSetHeaderEditor }, - tabs: [], - }; - }, - useExecutionStore() { - return {}; - }, - }; -}); +import { Tooltip, GraphiQLProvider } from '@graphiql/react'; +import { editorStore } from '../../../graphiql-react/dist/editor/context'; const mockQuery = /* GraphQL */ ` query Test($string: String) { @@ -49,11 +24,11 @@ type QueryHistoryItemProps = ComponentProps; const QueryHistoryItemWithContext: typeof HistoryItem = props => { return ( - + - + ); }; @@ -78,15 +53,6 @@ function getMockProps( } describe('QueryHistoryItem', () => { - const store = useEditorStore(); - const mockedSetQueryEditor = store.queryEditor!.setValue as Mock; - const mockedSetVariableEditor = store.variableEditor!.setValue as Mock; - const mockedSetHeaderEditor = store.headerEditor!.setValue as Mock; - beforeEach(() => { - mockedSetQueryEditor.mockClear(); - mockedSetVariableEditor.mockClear(); - mockedSetHeaderEditor.mockClear(); - }); it('renders operationName if label is not provided', () => { const otherMockProps = { item: { operationName: mockOperationName } }; const props = getMockProps(otherMockProps); @@ -108,6 +74,29 @@ describe('QueryHistoryItem', () => { }); it('selects the item when history label button is clicked', () => { + const mockedSetQueryEditor = vi.fn(); + const mockedSetVariableEditor = vi.fn(); + const mockedSetHeaderEditor = vi.fn(); + type MonacoEditorWithOperationFacts = NonNullable< + ReturnType['queryEditor'] + >; + type MonacoEditor = NonNullable< + ReturnType['variableEditor'] + >; + + editorStore.setState({ + queryEditor: { + setValue: mockedSetQueryEditor, + } as unknown as MonacoEditorWithOperationFacts, + variableEditor: { + setValue: mockedSetVariableEditor, + } as unknown as MonacoEditor, + headerEditor: { + setValue: mockedSetHeaderEditor, + getValue: () => '', + } as unknown as MonacoEditor, + }); + const otherMockProps = { item: { operationName: mockOperationName } }; const mockProps = getMockProps(otherMockProps); const { container } = render( diff --git a/packages/graphiql-plugin-history/src/context.tsx b/packages/graphiql-plugin-history/src/context.tsx index c8580f1c697..a7cc0d39466 100644 --- a/packages/graphiql-plugin-history/src/context.tsx +++ b/packages/graphiql-plugin-history/src/context.tsx @@ -119,7 +119,7 @@ export const HistoryContextProvider: FC = ({ maxHistoryLength = 20, children, }) => { - const { isFetching } = useExecutionStore(); + const isFetching = useExecutionStore(store => store.isFetching); const { tabs, activeTabIndex } = useEditorStore(); const activeTab = tabs[activeTabIndex]; const storage = useStorage(); diff --git a/packages/graphiql-react/src/constants.ts b/packages/graphiql-react/src/constants.ts index 088deb13103..c51034910b2 100644 --- a/packages/graphiql-react/src/constants.ts +++ b/packages/graphiql-react/src/constants.ts @@ -1,7 +1,18 @@ +export const KEY_MAP = Object.freeze({ + prettify: ['Shift-Ctrl-P'], + mergeFragments: ['Shift-Ctrl-M'], + runQuery: ['Ctrl-Enter', 'Cmd-Enter'], + autoComplete: ['Ctrl-Space'], + copyQuery: ['Shift-Ctrl-C'], + refetchSchema: ['Shift-Ctrl-R'], + searchInEditor: ['Ctrl-F'], + searchInDocs: ['Ctrl-K'], +} as const); + export const DEFAULT_QUERY = `# Welcome to GraphiQL # -# GraphiQL is an in-browser tool for writing, validating, and -# testing GraphQL queries. +# GraphiQL is an in-browser tool for writing, validating, and testing +# GraphQL queries. # # Type queries into this side of the screen, and you will see intelligent # typeaheads aware of the current GraphQL type schema and live syntax and @@ -20,13 +31,13 @@ export const DEFAULT_QUERY = `# Welcome to GraphiQL # # Keyboard shortcuts: # -# Prettify query: Shift-Ctrl-P (or press the prettify button) +# Prettify query: ${KEY_MAP.prettify[0]} (or press the prettify button) # -# Merge fragments: Shift-Ctrl-M (or press the merge button) +# Merge fragments: ${KEY_MAP.mergeFragments[0]} (or press the merge button) # -# Run Query: Ctrl-Enter (or press the play button) +# Run Query: ${KEY_MAP.runQuery[0]} (or press the play button) # -# Auto Complete: Ctrl-Space (or just start typing) +# Auto Complete: ${KEY_MAP.autoComplete[0]} (or just start typing) # `; diff --git a/packages/graphiql-react/src/editor/completion.ts b/packages/graphiql-react/src/editor/completion.ts index 6dd601a1151..f7330a06061 100644 --- a/packages/graphiql-react/src/editor/completion.ts +++ b/packages/graphiql-react/src/editor/completion.ts @@ -7,251 +7,234 @@ import { isListType, isNonNullType, } from 'graphql'; -import { markdown } from '../markdown'; -import { PluginContextType } from '../plugin'; +import { markdown } from '../utility'; +import { pluginStore } from '../plugin'; import { importCodeMirror } from './common'; -import { SchemaContextType } from '../schema'; +import { schemaStore } from '../schema'; /** * Render a custom UI for CodeMirror's hint which includes additional info * about the type and description for the selected context. */ -export function onHasCompletion( +export async function onHasCompletion( _cm: Editor, data: EditorChange | undefined, - { - schema, - setSchemaReference, - }: Pick, - plugin: PluginContextType | null, callback?: (type: GraphQLNamedType) => void, -): void { - void importCodeMirror([], { useCommonAddons: false }).then(CodeMirror => { - let information: HTMLDivElement | null; - let fieldName: HTMLSpanElement | null; - let typeNamePill: HTMLSpanElement | null; - let typeNamePrefix: HTMLSpanElement | null; - let typeName: HTMLAnchorElement | null; - let typeNameSuffix: HTMLSpanElement | null; - let description: HTMLDivElement | null; - let deprecation: HTMLDivElement | null; - let deprecationReason: HTMLDivElement | null; - CodeMirror.on( - data, - 'select', - // @ts-expect-error - (ctx: IHint, el: HTMLDivElement) => { - // Only the first time (usually when the hint UI is first displayed) - // do we create the information nodes. - if (!information) { - const hintsUl = el.parentNode as HTMLUListElement & ParentNode; - - // This "information" node will contain the additional info about the - // highlighted typeahead option. - information = document.createElement('div'); - information.className = 'CodeMirror-hint-information'; - hintsUl.append(information); - - const header = document.createElement('header'); - header.className = 'CodeMirror-hint-information-header'; - information.append(header); - - fieldName = document.createElement('span'); - fieldName.className = 'CodeMirror-hint-information-field-name'; - header.append(fieldName); - - typeNamePill = document.createElement('span'); - typeNamePill.className = 'CodeMirror-hint-information-type-name-pill'; - header.append(typeNamePill); - - typeNamePrefix = document.createElement('span'); - typeNamePill.append(typeNamePrefix); - - typeName = document.createElement('a'); - typeName.className = 'CodeMirror-hint-information-type-name'; - typeName.href = 'javascript:void 0'; // eslint-disable-line no-script-url - typeName.addEventListener('click', onClickHintInformation); - typeNamePill.append(typeName); - - typeNameSuffix = document.createElement('span'); - typeNamePill.append(typeNameSuffix); - - description = document.createElement('div'); - description.className = 'CodeMirror-hint-information-description'; - information.append(description); - - deprecation = document.createElement('div'); - deprecation.className = 'CodeMirror-hint-information-deprecation'; - information.append(deprecation); - - const deprecationLabel = document.createElement('span'); - deprecationLabel.className = - 'CodeMirror-hint-information-deprecation-label'; - deprecationLabel.textContent = 'Deprecated'; - deprecation.append(deprecationLabel); - - deprecationReason = document.createElement('div'); - deprecationReason.className = - 'CodeMirror-hint-information-deprecation-reason'; - deprecation.append(deprecationReason); - - /** - * This is a bit hacky: By default, codemirror renders all hints - * inside a single container element. The only possibility to add - * something into this list is to add to the container element (which - * is a `ul` element). - * - * However, in the UI we want to have a two-column layout for the - * hints: - * - The first column contains the actual hints, i.e. the things that - * are returned from the `hint` module from `codemirror-graphql`. - * - The second column contains the description and optionally the - * deprecation reason for the given field. - * - * We solve this with a CSS grid layout that has an auto number of - * rows and two columns. All the hints go in the first column, and - * the description container (which is the `information` element - * here) goes in the second column. To make the hints scrollable, the - * container element has `overflow-y: auto`. - * - * Now here comes the crux: When scrolling down the list of hints we - * still want the description to be "sticky" to the top. We can't - * solve this with `position: sticky` as the container element itself - * is already positioned absolutely. - * - * There are two things to the solution here: - * - We add a `max-height` and another `overflow: auto` to the - * `information` element. This makes it scrollable on its own - * if the description or deprecation reason is higher that the - * container element. - * - We add an `onscroll` handler to the container element. When the - * user scrolls here we dynamically adjust the top padding and the - * max-height of the information element such that it looks like - * it's sticking to the top. (Since the `information` element has - * some padding by default we also have to make sure to use this - * as baseline for the total padding.) - * Note that we need to also adjust the max-height because we - * default to using `border-box` for box sizing. When using - * `content-box` this would not be necessary. - */ - const defaultInformationPadding = - parseInt( - window - .getComputedStyle(information) - .paddingBottom.replace(/px$/, ''), - 10, - ) || 0; - const defaultInformationMaxHeight = - parseInt( - window.getComputedStyle(information).maxHeight.replace(/px$/, ''), - 10, - ) || 0; - const handleScroll = () => { - if (information) { - information.style.paddingTop = - hintsUl.scrollTop + defaultInformationPadding + 'px'; - information.style.maxHeight = - hintsUl.scrollTop + defaultInformationMaxHeight + 'px'; +): Promise { + const CodeMirror = await importCodeMirror([], { useCommonAddons: false }); + + let information: HTMLDivElement | null; + let fieldName: HTMLSpanElement | null; + let typeNamePill: HTMLSpanElement | null; + let typeNamePrefix: HTMLSpanElement | null; + let typeName: HTMLAnchorElement | null; + let typeNameSuffix: HTMLSpanElement | null; + let description: HTMLDivElement | null; + let deprecation: HTMLDivElement | null; + let deprecationReason: HTMLDivElement | null; + CodeMirror.on( + data, + 'select', + // @ts-expect-error + (ctx: IHint, el: HTMLDivElement) => { + // Only the first time (usually when the hint UI is first displayed) + // do we create the information nodes. + if (!information) { + const hintsUl = el.parentNode as HTMLUListElement & ParentNode; + + // This "information" node will contain the additional info about the + // highlighted typeahead option. + information = document.createElement('div'); + information.className = 'CodeMirror-hint-information'; + hintsUl.append(information); + + const header = document.createElement('header'); + header.className = 'CodeMirror-hint-information-header'; + information.append(header); + + fieldName = document.createElement('span'); + fieldName.className = 'CodeMirror-hint-information-field-name'; + header.append(fieldName); + + typeNamePill = document.createElement('span'); + typeNamePill.className = 'CodeMirror-hint-information-type-name-pill'; + header.append(typeNamePill); + + typeNamePrefix = document.createElement('span'); + typeNamePill.append(typeNamePrefix); + + typeName = document.createElement('a'); + typeName.className = 'CodeMirror-hint-information-type-name'; + typeName.href = 'javascript:void 0'; // eslint-disable-line no-script-url + typeName.addEventListener('click', onClickHintInformation); + typeNamePill.append(typeName); + + typeNameSuffix = document.createElement('span'); + typeNamePill.append(typeNameSuffix); + + description = document.createElement('div'); + description.className = 'CodeMirror-hint-information-description'; + information.append(description); + + deprecation = document.createElement('div'); + deprecation.className = 'CodeMirror-hint-information-deprecation'; + information.append(deprecation); + + const deprecationLabel = document.createElement('span'); + deprecationLabel.className = + 'CodeMirror-hint-information-deprecation-label'; + deprecationLabel.textContent = 'Deprecated'; + deprecation.append(deprecationLabel); + + deprecationReason = document.createElement('div'); + deprecationReason.className = + 'CodeMirror-hint-information-deprecation-reason'; + deprecation.append(deprecationReason); + + /** + * This is a bit hacky: By default, codemirror renders all hints + * inside a single container element. The only possibility to add + * something into this list is to add to the container element (which + * is a `ul` element). + * + * However, in the UI we want to have a two-column layout for the + * hints: + * - The first column contains the actual hints, i.e. the things that + * are returned from the `hint` module from `codemirror-graphql`. + * - The second column contains the description and optionally the + * deprecation reason for the given field. + * + * We solve this with a CSS grid layout that has an auto number of + * rows and two columns. All the hints go in the first column, and + * the description container (which is the `information` element + * here) goes in the second column. To make the hints scrollable, the + * container element has `overflow-y: auto`. + * + * Now here comes the crux: When scrolling down the list of hints we + * still want the description to be "sticky" to the top. We can't + * solve this with `position: sticky` as the container element itself + * is already positioned absolutely. + * + * There are two things to the solution here: + * - We add a `max-height` and another `overflow: auto` to the + * `information` element. This makes it scrollable on its own + * if the description or deprecation reason is higher that the + * container element. + * - We add an `onscroll` handler to the container element. When the + * user scrolls here we dynamically adjust the top padding and the + * max-height of the information element such that it looks like + * it's sticking to the top. (Since the `information` element has + * some padding by default we also have to make sure to use this + * as baseline for the total padding.) + * Note that we need to also adjust the max-height because we + * default to using `border-box` for box sizing. When using + * `content-box` this would not be necessary. + */ + const { paddingBottom, maxHeight } = getComputedStyle(information); + const defaultInformationPadding = + parseInt(paddingBottom.replace(/px$/, ''), 10) || 0; + const defaultInformationMaxHeight = + parseInt(maxHeight.replace(/px$/, ''), 10) || 0; + const handleScroll = () => { + if (information) { + information.style.paddingTop = + hintsUl.scrollTop + defaultInformationPadding + 'px'; + information.style.maxHeight = + hintsUl.scrollTop + defaultInformationMaxHeight + 'px'; + } + }; + hintsUl.addEventListener('scroll', handleScroll); + + // When CodeMirror attempts to remove the hint UI, we detect that it was + // removed and in turn remove the information nodes. + let onRemoveFn: EventListener | null; + hintsUl.addEventListener( + 'DOMNodeRemoved', + (onRemoveFn = (event: Event) => { + if (event.target !== hintsUl) { + return; + } + hintsUl.removeEventListener('scroll', handleScroll); + hintsUl.removeEventListener('DOMNodeRemoved', onRemoveFn); + information?.removeEventListener('click', onClickHintInformation); + information = null; + fieldName = null; + typeNamePill = null; + typeNamePrefix = null; + typeName = null; + typeNameSuffix = null; + description = null; + deprecation = null; + deprecationReason = null; + onRemoveFn = null; + }), + ); + } + + if (fieldName) { + fieldName.textContent = ctx.text; + } + + if (typeNamePill && typeNamePrefix && typeName && typeNameSuffix) { + if (ctx.type) { + typeNamePill.style.display = 'inline'; + + const renderType = (type: GraphQLType) => { + if (isNonNullType(type)) { + typeNameSuffix!.textContent = '!' + typeNameSuffix!.textContent; + renderType(type.ofType); + } else if (isListType(type)) { + typeNamePrefix!.textContent += '['; + typeNameSuffix!.textContent = ']' + typeNameSuffix!.textContent; + renderType(type.ofType); + } else { + typeName!.textContent = type.name; } }; - hintsUl.addEventListener('scroll', handleScroll); - - // When CodeMirror attempts to remove the hint UI, we detect that it was - // removed and in turn remove the information nodes. - let onRemoveFn: EventListener | null; - hintsUl.addEventListener( - 'DOMNodeRemoved', - (onRemoveFn = (event: Event) => { - if (event.target !== hintsUl) { - return; - } - hintsUl.removeEventListener('scroll', handleScroll); - hintsUl.removeEventListener('DOMNodeRemoved', onRemoveFn); - information?.removeEventListener('click', onClickHintInformation); - information = null; - fieldName = null; - typeNamePill = null; - typeNamePrefix = null; - typeName = null; - typeNameSuffix = null; - description = null; - deprecation = null; - deprecationReason = null; - onRemoveFn = null; - }), - ); - } - - if (fieldName) { - fieldName.textContent = ctx.text; - } - - if (typeNamePill && typeNamePrefix && typeName && typeNameSuffix) { - if (ctx.type) { - typeNamePill.style.display = 'inline'; - const renderType = (type: GraphQLType) => { - if (isNonNullType(type)) { - typeNameSuffix!.textContent = '!' + typeNameSuffix!.textContent; - renderType(type.ofType); - } else if (isListType(type)) { - typeNamePrefix!.textContent += '['; - typeNameSuffix!.textContent = ']' + typeNameSuffix!.textContent; - renderType(type.ofType); - } else { - typeName!.textContent = type.name; - } - }; - - typeNamePrefix.textContent = ''; - typeNameSuffix.textContent = ''; - renderType(ctx.type); - } else { - typeNamePrefix.textContent = ''; - typeName.textContent = ''; - typeNameSuffix.textContent = ''; - typeNamePill.style.display = 'none'; - } + typeNamePrefix.textContent = ''; + typeNameSuffix.textContent = ''; + renderType(ctx.type); + } else { + typeNamePrefix.textContent = ''; + typeName.textContent = ''; + typeNameSuffix.textContent = ''; + typeNamePill.style.display = 'none'; } - - if (description) { - if (ctx.description) { - description.style.display = 'block'; - description.innerHTML = markdown.render(ctx.description); - } else { - description.style.display = 'none'; - description.innerHTML = ''; - } + } + + if (description) { + if (ctx.description) { + description.style.display = 'block'; + description.innerHTML = markdown.render(ctx.description); + } else { + description.style.display = 'none'; + description.innerHTML = ''; } - - if (deprecation && deprecationReason) { - if (ctx.deprecationReason) { - deprecation.style.display = 'block'; - deprecationReason.innerHTML = markdown.render( - ctx.deprecationReason, - ); - } else { - deprecation.style.display = 'none'; - deprecationReason.innerHTML = ''; - } + } + + if (deprecation && deprecationReason) { + if (ctx.deprecationReason) { + deprecation.style.display = 'block'; + deprecationReason.innerHTML = markdown.render(ctx.deprecationReason); + } else { + deprecation.style.display = 'none'; + deprecationReason.innerHTML = ''; } - }, - ); - }); + } + }, + ); function onClickHintInformation(event: Event) { - const referencePlugin = plugin?.referencePlugin; - if ( - !schema || - !referencePlugin || - !(event.currentTarget instanceof HTMLElement) - ) { + const { schema, setSchemaReference } = schemaStore.getState(); + const { referencePlugin, setVisiblePlugin } = pluginStore.getState(); + const element = event.currentTarget; + if (!schema || !referencePlugin || !(element instanceof HTMLElement)) { return; } - const typeName = event.currentTarget.textContent || ''; - const type = schema.getType(typeName); + const type = schema.getType(element.textContent ?? ''); if (type) { - plugin.setVisiblePlugin(referencePlugin); + setVisiblePlugin(referencePlugin); setSchemaReference({ kind: 'Type', type }); callback?.(type); } diff --git a/packages/graphiql-react/src/editor/components/query-editor.tsx b/packages/graphiql-react/src/editor/components/query-editor.tsx index 9a4f67c456f..c4dfead3a4f 100644 --- a/packages/graphiql-react/src/editor/components/query-editor.tsx +++ b/packages/graphiql-react/src/editor/components/query-editor.tsx @@ -10,6 +10,6 @@ import '../style/auto-insertion.css'; import '../style/editor.css'; export const QueryEditor: FC = props => { - const ref = useQueryEditor(props, QueryEditor); + const ref = useQueryEditor(props); return
; }; diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx index d275756220b..f4e35052f91 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/editor/context.tsx @@ -6,9 +6,11 @@ import { parse, ValidationRule, visit, + print, } from 'graphql'; import { VariableToType } from 'graphql-language-service'; -import { FC, ReactElement, ReactNode, useEffect, useRef } from 'react'; +import { FC, ReactElement, ReactNode, useEffect } from 'react'; +import { MaybePromise } from '@graphiql/toolkit'; import { storageStore, useStorage } from '../storage'; import { STORAGE_KEY as STORAGE_KEY_HEADERS } from './header-editor'; @@ -33,6 +35,7 @@ import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from './variable-editor'; import { DEFAULT_QUERY } from '../constants'; import { createStore } from 'zustand'; import { createBoundedUseStore } from '../utility'; +import { executionStore } from '../execution'; export type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { documentAST: DocumentNode | null; @@ -202,11 +205,33 @@ interface EditorStore extends TabsState { * Headers to be set when opening a new tab */ defaultHeaders?: string; + + /** + * Invoked when the current contents of the query editor are copied to the + * clipboard. + * @param query The content that has been copied. + */ + onCopyQuery?: (query: string) => void; + + /** + * Invoked when the prettify callback is invoked. + * @param query The current value of the query editor. + * @default + * import { parse, print } from 'graphql' + * + * (query) => print(parse(query)) + * @returns The formatted query. + */ + onPrettifyQuery: (query: string) => MaybePromise; } type EditorContextProviderProps = Pick< EditorStore, - 'onTabChange' | 'onEditOperationName' | 'defaultHeaders' | 'defaultQuery' + | 'onTabChange' + | 'onEditOperationName' + | 'defaultHeaders' + | 'defaultQuery' + | 'onCopyQuery' > & { children: ReactNode; /** @@ -272,8 +297,12 @@ type EditorContextProviderProps = Pick< * typing in the editor. */ variables?: string; + onPrettifyQuery?: EditorStore['onPrettifyQuery']; }; +const DEFAULT_PRETTIFY_QUERY: EditorStore['onPrettifyQuery'] = query => + print(parse(query)); + export const editorStore = createStore((set, get) => ({ tabs: null!, activeTabIndex: null!, @@ -300,6 +329,9 @@ export const editorStore = createStore((set, get) => ({ }); }, changeTab(index) { + const { stop } = executionStore.getState(); + stop(); + set(current => { const { onTabChange } = get(); const updated = { @@ -327,8 +359,14 @@ export const editorStore = createStore((set, get) => ({ }); }, closeTab(index) { + const { activeTabIndex, onTabChange } = get(); + + if (activeTabIndex === index) { + const { stop } = executionStore.getState(); + stop(); + } + set(current => { - const { onTabChange } = get(); const updated = { tabs: current.tabs.filter((_tab, i) => index !== i), activeTabIndex: Math.max(current.activeTabIndex - 1, 0), @@ -403,6 +441,7 @@ export const editorStore = createStore((set, get) => ({ initialQuery: null!, initialResponse: null!, initialVariables: null!, + onPrettifyQuery: DEFAULT_PRETTIFY_QUERY, })); export const EditorContextProvider: FC = ({ @@ -414,6 +453,8 @@ export const EditorContextProvider: FC = ({ children, shouldPersistHeaders = false, validationRules = [], + onCopyQuery, + onPrettifyQuery = DEFAULT_PRETTIFY_QUERY, ...props }) => { const storage = useStorage(); @@ -459,14 +500,7 @@ export const EditorContextProvider: FC = ({ return map; })(); - const initialRendered = useRef(false); - useEffect(() => { - if (initialRendered.current) { - return; - } - initialRendered.current = true; - // We only need to compute it lazily during the initial render. const query = props.query ?? storage.get(STORAGE_KEY_QUERY) ?? null; const variables = @@ -474,7 +508,7 @@ export const EditorContextProvider: FC = ({ const headers = props.headers ?? storage.get(STORAGE_KEY_HEADERS) ?? null; const response = props.response ?? ''; - const tabState = getDefaultTabState({ + const { tabs, activeTabIndex } = getDefaultTabState({ query, variables, headers, @@ -483,7 +517,7 @@ export const EditorContextProvider: FC = ({ defaultHeaders, shouldPersistHeaders, }); - storeTabs(tabState); + storeTabs({ tabs, activeTabIndex }); const isStored = storage.get(PERSIST_HEADERS_STORAGE_KEY) !== null; @@ -494,11 +528,10 @@ export const EditorContextProvider: FC = ({ editorStore.setState({ shouldPersistHeaders: $shouldPersistHeaders, - ...tabState, + tabs, + activeTabIndex, initialQuery: - query ?? - (tabState.activeTabIndex === 0 ? tabState.tabs[0].query : null) ?? - '', + query ?? (activeTabIndex === 0 ? tabs[0].query : null) ?? '', initialVariables: variables ?? '', initialHeaders: headers ?? defaultHeaders ?? '', initialResponse: response, @@ -513,6 +546,8 @@ export const EditorContextProvider: FC = ({ defaultQuery, defaultHeaders, validationRules, + onCopyQuery, + onPrettifyQuery, }); }, [ $externalFragments, @@ -521,6 +556,8 @@ export const EditorContextProvider: FC = ({ defaultQuery, defaultHeaders, validationRules, + onCopyQuery, + onPrettifyQuery, ]); if (!isMounted) { diff --git a/packages/graphiql-react/src/editor/header-editor.ts b/packages/graphiql-react/src/editor/header-editor.ts index 5a84eabe9ed..f2f1c8713b0 100644 --- a/packages/graphiql-react/src/editor/header-editor.ts +++ b/packages/graphiql-react/src/editor/header-editor.ts @@ -10,12 +10,13 @@ import { useEditorStore } from './context'; import { useChangeHandler, useKeyMap, - useMergeQuery, - usePrettifyEditors, + mergeQuery, + prettifyEditors, useSynchronizeOption, } from './hooks'; import { WriteableEditorProps } from './types'; import { useExecutionStore } from '../execution'; +import { KEY_MAP } from '../constants'; export type UseHeaderEditorArgs = WriteableEditorProps & { /** @@ -45,9 +46,7 @@ export function useHeaderEditor({ setHeaderEditor, shouldPersistHeaders, } = useEditorStore(); - const { run } = useExecutionStore(); - const merge = useMergeQuery(); - const prettify = usePrettifyEditors(); + const run = useExecutionStore(store => store.run); const ref = useRef(null); useEffect(() => { @@ -79,19 +78,15 @@ export function useHeaderEditor({ extraKeys: commonKeys, }); + function showHint() { + newEditor.showHint({ completeSingle: false, container }); + } + 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 }); - }, + 'Cmd-Space': showHint, + 'Ctrl-Space': showHint, + 'Alt-Space': showHint, + 'Shift-Space': showHint, }); newEditor.on('keyup', (editorInstance, event) => { @@ -120,9 +115,9 @@ export function useHeaderEditor({ 'headers', ); - useKeyMap(headerEditor, ['Cmd-Enter', 'Ctrl-Enter'], run); - useKeyMap(headerEditor, ['Shift-Ctrl-P'], prettify); - useKeyMap(headerEditor, ['Shift-Ctrl-M'], merge); + useKeyMap(headerEditor, KEY_MAP.runQuery, run); + useKeyMap(headerEditor, KEY_MAP.prettify, prettifyEditors); + useKeyMap(headerEditor, KEY_MAP.mergeFragments, mergeQuery); return ref; } diff --git a/packages/graphiql-react/src/editor/hooks.ts b/packages/graphiql-react/src/editor/hooks.ts index cacc01340a0..9442794dc91 100644 --- a/packages/graphiql-react/src/editor/hooks.ts +++ b/packages/graphiql-react/src/editor/hooks.ts @@ -1,12 +1,11 @@ -import { fillLeafs, mergeAst, MaybePromise } from '@graphiql/toolkit'; +import { fillLeafs, mergeAst } from '@graphiql/toolkit'; import type { EditorChange, EditorConfiguration } from 'codemirror'; import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; import copyToClipboard from 'copy-to-clipboard'; -import { parse, print } from 'graphql'; +import { print } from 'graphql'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -- TODO: check why query builder update only 1st field https://github.com/graphql/graphiql/issues/3836 import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { usePluginStore } from '../plugin'; -import { schemaStore, useSchemaStore } from '../schema'; +import { schemaStore } from '../schema'; import { storageStore } from '../storage'; import { debounce } from '../utility'; import { onHasCompletion } from './completion'; @@ -83,8 +82,6 @@ export function useCompletion( editor: CodeMirrorEditor | null, callback?: (reference: SchemaReference) => void, ) { - const { schema, setSchemaReference } = useSchemaStore(); - const plugin = usePluginStore(); useEffect(() => { if (!editor) { return; @@ -94,12 +91,11 @@ export function useCompletion( instance: CodeMirrorEditor, changeObj?: EditorChange, ) => { - const schemaContext = { schema, setSchemaReference }; - onHasCompletion(instance, changeObj, schemaContext, plugin, type => { + void onHasCompletion(instance, changeObj, type => { callback?.({ kind: 'Type', type, - schema: schema || undefined, + schema: schemaStore.getState().schema || undefined, }); }); }; @@ -114,14 +110,14 @@ export function useCompletion( 'hasCompletion', handleCompletion, ); - }, [callback, editor, plugin, schema, setSchemaReference]); + }, [callback, editor]); } type EmptyCallback = () => void; export function useKeyMap( editor: CodeMirrorEditor | null, - keys: string[], + keys: string[] | readonly string[], callback?: EmptyCallback, ) { useEffect(() => { @@ -132,125 +128,88 @@ export function useKeyMap( editor.removeKeyMap(key); } - if (callback) { - const keyMap: Record = {}; - for (const key of keys) { - keyMap[key] = () => callback(); - } - editor.addKeyMap(keyMap); + if (!callback) { + return; } + const keyMap: Record = {}; + for (const key of keys) { + keyMap[key] = () => callback(); + } + editor.addKeyMap(keyMap); }, [editor, keys, callback]); } -export type UseCopyQueryArgs = { - /** - * This is only meant to be used internally in `@graphiql/react`. - */ - caller?: Function; - /** - * Invoked when the current contents of the query editor are copied to the - * clipboard. - * @param query The content that has been copied. - */ - onCopyQuery?: (query: string) => void; -}; - -export function useCopyQuery({ onCopyQuery }: UseCopyQueryArgs = {}) { - return () => { - const { queryEditor } = editorStore.getState(); - if (!queryEditor) { - return; - } - - const query = queryEditor.getValue(); - copyToClipboard(query); - - onCopyQuery?.(query); - }; -} +export function copyQuery() { + const { queryEditor, onCopyQuery } = editorStore.getState(); + if (!queryEditor) { + return; + } -export function useMergeQuery() { - return () => { - const { queryEditor } = editorStore.getState(); - const documentAST = queryEditor?.documentAST; - const query = queryEditor?.getValue(); - if (!documentAST || !query) { - return; - } + const query = queryEditor.getValue(); + copyToClipboard(query); - const { schema } = schemaStore.getState(); - queryEditor.setValue(print(mergeAst(documentAST, schema))); - }; + onCopyQuery?.(query); } -export type UsePrettifyEditorsArgs = { - /** - * Invoked when the prettify callback is invoked. - * @param query The current value of the query editor. - * @default - * import { parse, print } from 'graphql' - * - * (query) => print(parse(query)) - * @returns The formatted query. - */ - onPrettifyQuery?: (query: string) => MaybePromise; -}; +export function mergeQuery() { + const { queryEditor } = editorStore.getState(); + const documentAST = queryEditor?.documentAST; + const query = queryEditor?.getValue(); + if (!documentAST || !query) { + return; + } -function DEFAULT_PRETTIFY_QUERY(query: string): string { - return print(parse(query)); + const { schema } = schemaStore.getState(); + queryEditor.setValue(print(mergeAst(documentAST, schema))); } -export function usePrettifyEditors({ - onPrettifyQuery = DEFAULT_PRETTIFY_QUERY, -}: UsePrettifyEditorsArgs = {}) { - return async () => { - const { queryEditor, headerEditor, variableEditor } = - editorStore.getState(); - if (variableEditor) { - const variableEditorContent = variableEditor.getValue(); - try { - const prettifiedVariableEditorContent = JSON.stringify( - JSON.parse(variableEditorContent), - null, - 2, - ); - if (prettifiedVariableEditorContent !== variableEditorContent) { - variableEditor.setValue(prettifiedVariableEditorContent); - } - } catch { - /* Parsing JSON failed, skip prettification */ +export async function prettifyEditors() { + const { queryEditor, headerEditor, variableEditor, onPrettifyQuery } = + editorStore.getState(); + if (variableEditor) { + const variableEditorContent = variableEditor.getValue(); + try { + const prettifiedVariableEditorContent = JSON.stringify( + JSON.parse(variableEditorContent), + null, + 2, + ); + if (prettifiedVariableEditorContent !== variableEditorContent) { + variableEditor.setValue(prettifiedVariableEditorContent); } + } catch { + /* Parsing JSON failed, skip prettification */ } + } - if (headerEditor) { - 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 */ + if (headerEditor) { + 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 */ } + } - if (queryEditor) { - const editorContent = queryEditor.getValue(); - try { - const prettifiedEditorContent = await onPrettifyQuery(editorContent); - if (prettifiedEditorContent !== editorContent) { - queryEditor.setValue(prettifiedEditorContent); - } - } catch { - /* Parsing query failed, skip prettification */ + if (queryEditor) { + const editorContent = queryEditor.getValue(); + try { + const prettifiedEditorContent = await onPrettifyQuery(editorContent); + if (prettifiedEditorContent !== editorContent) { + queryEditor.setValue(prettifiedEditorContent); } + } catch { + /* Parsing query failed, skip prettification */ } - }; + } } export function getAutoCompleteLeafs() { @@ -381,26 +340,28 @@ export function useOptimisticState([ useEffect(() => { if (lastStateRef.current.last === upstreamState) { // No change; ignore - } else { - lastStateRef.current.last = upstreamState; - if (lastStateRef.current.pending === null) { - // Gracefully accept update from upstream - setOperationsText(upstreamState); - } else if (lastStateRef.current.pending === upstreamState) { - // They received our update and sent it back to us - clear pending, and - // send next if appropriate - lastStateRef.current.pending = null; - if (upstreamState !== state) { - // Change has occurred; upstream it - lastStateRef.current.pending = state; - upstreamSetState(state); - } - } else { - // They got a different update; overwrite our local state (!!) - lastStateRef.current.pending = null; - setOperationsText(upstreamState); + return; + } + lastStateRef.current.last = upstreamState; + if (lastStateRef.current.pending === null) { + // Gracefully accept update from upstream + setOperationsText(upstreamState); + return; + } + if (lastStateRef.current.pending === upstreamState) { + // They received our update and sent it back to us - clear pending, and + // send next if appropriate + lastStateRef.current.pending = null; + if (upstreamState !== state) { + // Change has occurred; upstream it + lastStateRef.current.pending = state; + upstreamSetState(state); } + return; } + // They got a different update; overwrite our local state (!!) + lastStateRef.current.pending = null; + setOperationsText(upstreamState); }, [upstreamState, state, upstreamSetState]); const setState = (newState: string) => { diff --git a/packages/graphiql-react/src/editor/index.ts b/packages/graphiql-react/src/editor/index.ts index 9e2213eb88c..1c548e52080 100644 --- a/packages/graphiql-react/src/editor/index.ts +++ b/packages/graphiql-react/src/editor/index.ts @@ -9,9 +9,9 @@ export { EditorContextProvider, useEditorStore } from './context'; export { useHeaderEditor } from './header-editor'; export { getAutoCompleteLeafs, - useCopyQuery, - useMergeQuery, - usePrettifyEditors, + copyQuery, + mergeQuery, + prettifyEditors, useEditorState, useOperationsEditorState, useOptimisticState, diff --git a/packages/graphiql-react/src/editor/query-editor.ts b/packages/graphiql-react/src/editor/query-editor.ts index e591e387f70..04621a76091 100644 --- a/packages/graphiql-react/src/editor/query-editor.ts +++ b/packages/graphiql-react/src/editor/query-editor.ts @@ -13,11 +13,10 @@ import { } from 'graphql-language-service'; import { RefObject, useEffect, useRef } from 'react'; import { executionStore } from '../execution'; -import { markdown } from '../markdown'; -import { usePluginStore } from '../plugin'; -import { useSchemaStore } from '../schema'; +import { markdown, debounce } from '../utility'; +import { pluginStore } from '../plugin'; +import { schemaStore, useSchemaStore } from '../schema'; import { useStorage } from '../storage'; -import { debounce } from '../utility/debounce'; import { commonKeys, DEFAULT_EDITOR_THEME, @@ -27,12 +26,10 @@ import { import { CodeMirrorEditorWithOperationFacts, useEditorStore } from './context'; import { useCompletion, - useCopyQuery, - UseCopyQueryArgs, - UsePrettifyEditorsArgs, + copyQuery, useKeyMap, - useMergeQuery, - usePrettifyEditors, + mergeQuery, + prettifyEditors, useSynchronizeOption, } from './hooks'; import { @@ -41,23 +38,22 @@ import { WriteableEditorProps, } from './types'; import { normalizeWhitespace } from './whitespace'; +import { KEY_MAP } from '../constants'; -export type UseQueryEditorArgs = WriteableEditorProps & - Pick & - Pick & { - /** - * Invoked when a reference to the GraphQL schema (type or field) is clicked - * as part of the editor or one of its tooltips. - * @param reference The reference that has been clicked. - */ - onClickReference?(reference: SchemaReference): void; - /** - * Invoked when the contents of the query editor change. - * @param value The new contents of the editor. - * @param documentAST The editor contents parsed into a GraphQL document. - */ - onEdit?(value: string, documentAST?: DocumentNode): void; - }; +export type UseQueryEditorArgs = WriteableEditorProps & { + /** + * Invoked when a reference to the GraphQL schema (type or field) is clicked + * as part of the editor or one of its tooltips. + * @param reference The reference that has been clicked. + */ + onClickReference?(reference: SchemaReference): void; + /** + * Invoked when the contents of the query editor change. + * @param value The new contents of the editor. + * @param documentAST The editor contents parsed into a GraphQL document. + */ + onEdit?(value: string, documentAST?: DocumentNode): void; +}; // To make react-compiler happy, otherwise complains about using dynamic imports in Component function importCodeMirrorImports() { @@ -72,8 +68,6 @@ function importCodeMirrorImports() { ]); } -const _useQueryEditor = useQueryEditor; - // To make react-compiler happy since we mutate variableEditor function updateVariableEditor( variableEditor: CodeMirrorEditor, @@ -114,34 +108,22 @@ function updateEditorExternalFragments( editor.options.hintOptions.externalFragments = externalFragmentList; } -export function useQueryEditor( - { - editorTheme = DEFAULT_EDITOR_THEME, - keyMap = DEFAULT_KEY_MAP, - onClickReference, - onCopyQuery, - onEdit, - onPrettifyQuery, - readOnly = false, - }: UseQueryEditorArgs = {}, - caller?: Function, -) { - const { schema, setSchemaReference } = useSchemaStore(); +export function useQueryEditor({ + editorTheme = DEFAULT_EDITOR_THEME, + keyMap = DEFAULT_KEY_MAP, + onClickReference, + onEdit, + readOnly = false, +}: UseQueryEditorArgs = {}) { const { - externalFragments, initialQuery, queryEditor, setOperationName, setQueryEditor, - validationRules, variableEditor, updateActiveTabValues, } = useEditorStore(); const storage = useStorage(); - const plugin = usePluginStore(); - const copy = useCopyQuery({ caller: caller || _useQueryEditor, onCopyQuery }); - const merge = useMergeQuery(); - const prettify = usePrettifyEditors({ onPrettifyQuery }); const ref = useRef(null); const codeMirrorRef = useRef(undefined); @@ -150,16 +132,17 @@ export function useQueryEditor( >(() => {}); useEffect(() => { + const { referencePlugin, setVisiblePlugin } = pluginStore.getState(); + const { setSchemaReference } = schemaStore.getState(); onClickReferenceRef.current = reference => { - const referencePlugin = plugin?.referencePlugin; if (!referencePlugin) { return; } - plugin.setVisiblePlugin(referencePlugin); + setVisiblePlugin(referencePlugin); setSchemaReference(reference); onClickReference?.(reference); }; - }, [onClickReference, plugin, setSchemaReference]); + }, [onClickReference]); useEffect(() => { let isActive = true; @@ -306,7 +289,7 @@ export function useQueryEditor( editorInstance: CodeMirrorEditorWithOperationFacts, ) { const operationFacts = getOperationFacts( - schema, + schemaStore.getState().schema, editorInstance.getValue(), ); @@ -367,31 +350,20 @@ export function useQueryEditor( }, [ onEdit, queryEditor, - schema, setOperationName, storage, variableEditor, updateActiveTabValues, ]); - useSynchronizeSchema(queryEditor, schema ?? null, codeMirrorRef); - useSynchronizeValidationRules( - queryEditor, - validationRules ?? null, - codeMirrorRef, - ); - useSynchronizeExternalFragments( - queryEditor, - externalFragments, - codeMirrorRef, - ); + useSynchronizeSchema(queryEditor, codeMirrorRef); + useSynchronizeValidationRules(queryEditor, codeMirrorRef); + useSynchronizeExternalFragments(queryEditor, codeMirrorRef); useCompletion(queryEditor, onClickReference); const runAtCursor = () => { - const { run } = executionStore.getState(); - - if (!queryEditor || !queryEditor.operations || !queryEditor.hasFocus()) { + if (!queryEditor?.operations || !queryEditor.hasFocus()) { return; } @@ -412,31 +384,25 @@ export function useQueryEditor( if (operationName && operationName !== queryEditor.operationName) { setOperationName(operationName); } - + const { run } = executionStore.getState(); run(); }; - useKeyMap(queryEditor, ['Cmd-Enter', 'Ctrl-Enter'], runAtCursor); - useKeyMap(queryEditor, ['Shift-Ctrl-C'], copy); - useKeyMap( - queryEditor, - [ - 'Shift-Ctrl-P', - // Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to prettify - 'Shift-Ctrl-F', - ], - prettify, - ); - useKeyMap(queryEditor, ['Shift-Ctrl-M'], merge); + useKeyMap(queryEditor, KEY_MAP.runQuery, runAtCursor); + useKeyMap(queryEditor, KEY_MAP.copyQuery, copyQuery); + // Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to prettify + useKeyMap(queryEditor, ['Shift-Ctrl-P', 'Shift-Ctrl-F'], prettifyEditors); + useKeyMap(queryEditor, KEY_MAP.mergeFragments, mergeQuery); return ref; } function useSynchronizeSchema( editor: CodeMirrorEditor | null, - schema: GraphQLSchema | null, codeMirrorRef: RefObject, ) { + const schema = useSchemaStore(store => store.schema ?? null); + useEffect(() => { if (!editor) { return; @@ -445,17 +411,17 @@ function useSynchronizeSchema( const didChange = editor.options.lint.schema !== schema; updateEditorSchema(editor, schema); - if (didChange && codeMirrorRef.current) { - codeMirrorRef.current.signal(editor, 'change', editor); + if (didChange) { + codeMirrorRef.current?.signal(editor, 'change', editor); } }, [editor, schema, codeMirrorRef]); } function useSynchronizeValidationRules( editor: CodeMirrorEditor | null, - validationRules: ValidationRule[] | null, codeMirrorRef: RefObject, ) { + const validationRules = useEditorStore(store => store.validationRules); useEffect(() => { if (!editor) { return; @@ -464,32 +430,30 @@ function useSynchronizeValidationRules( const didChange = editor.options.lint.validationRules !== validationRules; updateEditorValidationRules(editor, validationRules); - if (didChange && codeMirrorRef.current) { - codeMirrorRef.current.signal(editor, 'change', editor); + if (didChange) { + codeMirrorRef.current?.signal(editor, 'change', editor); } }, [editor, validationRules, codeMirrorRef]); } function useSynchronizeExternalFragments( editor: CodeMirrorEditor | null, - externalFragments: Map, codeMirrorRef: RefObject, ) { - const externalFragmentList = [...externalFragments.values()]; // eslint-disable-line react-hooks/exhaustive-deps -- false positive, variable is optimized by react-compiler, no need to wrap with useMemo - + const externalFragments = useEditorStore(store => store.externalFragments); useEffect(() => { if (!editor) { return; } - + const externalFragmentList = [...externalFragments.values()]; const didChange = editor.options.lint.externalFragments !== externalFragmentList; updateEditorExternalFragments(editor, externalFragmentList); - if (didChange && codeMirrorRef.current) { - codeMirrorRef.current.signal(editor, 'change', editor); + if (didChange) { + codeMirrorRef.current?.signal(editor, 'change', editor); } - }, [editor, externalFragmentList, codeMirrorRef]); + }, [editor, externalFragments, codeMirrorRef]); } const AUTO_COMPLETE_AFTER_KEY = /^[a-zA-Z0-9_@(]$/; diff --git a/packages/graphiql-react/src/editor/variable-editor.ts b/packages/graphiql-react/src/editor/variable-editor.ts index b78f6b6ac89..55ce95c2126 100644 --- a/packages/graphiql-react/src/editor/variable-editor.ts +++ b/packages/graphiql-react/src/editor/variable-editor.ts @@ -13,11 +13,12 @@ import { useChangeHandler, useCompletion, useKeyMap, - useMergeQuery, - usePrettifyEditors, + mergeQuery, + prettifyEditors, useSynchronizeOption, } from './hooks'; import { WriteableEditorProps } from './types'; +import { KEY_MAP } from '../constants'; export type UseVariableEditorArgs = WriteableEditorProps & { /** @@ -51,9 +52,7 @@ export function useVariableEditor({ }: UseVariableEditorArgs = {}) { const { initialVariables, variableEditor, setVariableEditor } = useEditorStore(); - const { run } = useExecutionStore(); - const merge = useMergeQuery(); - const prettify = usePrettifyEditors(); + const run = useExecutionStore(store => store.run); const ref = useRef(null); useEffect(() => { let isActive = true; @@ -93,20 +92,15 @@ export function useVariableEditor({ gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], extraKeys: commonKeys, }); + function showHint() { + newEditor.showHint({ completeSingle: false, container }); + } 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 }); - }, + 'Cmd-Space': showHint, + 'Ctrl-Space': showHint, + 'Alt-Space': showHint, + 'Shift-Space': showHint, }); newEditor.on('keyup', (editorInstance, event) => { @@ -132,9 +126,9 @@ export function useVariableEditor({ useCompletion(variableEditor, onClickReference); - useKeyMap(variableEditor, ['Cmd-Enter', 'Ctrl-Enter'], run); - useKeyMap(variableEditor, ['Shift-Ctrl-P'], prettify); - useKeyMap(variableEditor, ['Shift-Ctrl-M'], merge); + useKeyMap(variableEditor, KEY_MAP.runQuery, run); + useKeyMap(variableEditor, KEY_MAP.prettify, prettifyEditors); + useKeyMap(variableEditor, KEY_MAP.mergeFragments, mergeQuery); return ref; } diff --git a/packages/graphiql-react/src/execution.tsx b/packages/graphiql-react/src/execution.tsx index 90e171572d5..fde831955ae 100644 --- a/packages/graphiql-react/src/execution.tsx +++ b/packages/graphiql-react/src/execution.tsx @@ -22,10 +22,9 @@ import getValue from 'get-value'; import { getAutoCompleteLeafs } from './editor'; import { createStore } from 'zustand'; import { editorStore } from './editor/context'; -import { schemaStore } from './schema'; import { createBoundedUseStore } from './utility'; -export type ExecutionContextType = { +type ExecutionContextType = { /** * If there is currently a GraphQL request in-flight. For multipart * requests like subscriptions, this will be `true` while fetching the @@ -67,13 +66,7 @@ export type ExecutionContextType = { * @default 0 */ queryId: number; -}; -type ExecutionContextProviderProps = Pick< - ExecutionContextType, - 'getDefaultFieldNames' -> & { - children: ReactNode; /** * A function which accepts GraphQL HTTP parameters and returns a `Promise`, * `Observable` or `AsyncIterable` that returns the GraphQL response in @@ -85,6 +78,13 @@ type ExecutionContextProviderProps = Pick< * @see {@link https://graphiql-test.netlify.app/typedoc/modules/graphiql_toolkit.html#creategraphiqlfetcher-2|`createGraphiQLFetcher`} */ fetcher: Fetcher; +}; + +type ExecutionContextProviderProps = Pick< + ExecutionContextType, + 'getDefaultFieldNames' | 'fetcher' +> & { + children: ReactNode; /** * This prop sets the operation name that is passed with a GraphQL request. */ @@ -100,6 +100,7 @@ export const executionStore = createStore< operationName: null, getDefaultFieldNames: undefined, queryId: 0, + fetcher: null!, stop() { const { subscription } = get(); subscription?.unsubscribe(); @@ -117,7 +118,7 @@ export const executionStore = createStore< if (!queryEditor || !responseEditor) { return; } - const { subscription, operationName, queryId } = get(); + const { subscription, operationName, queryId, fetcher } = get(); // If there's an active subscription, unsubscribe it and return if (subscription) { @@ -213,7 +214,6 @@ export const executionStore = createStore< setResponse(formatResult(result)); } }; - const { fetcher } = schemaStore.getState(); const fetch = fetcher( { query, @@ -274,17 +274,13 @@ export const ExecutionContextProvider: FC = ({ children, operationName = null, }) => { - if (!fetcher) { - throw new TypeError( - 'The `ExecutionContextProvider` component requires a `fetcher` function to be passed as prop.', - ); - } useEffect(() => { executionStore.setState({ operationName, getDefaultFieldNames, + fetcher, }); - }, [getDefaultFieldNames, operationName]); + }, [getDefaultFieldNames, operationName, fetcher]); return children as ReactElement; }; diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts index b1e8ee02073..20010c6aa02 100644 --- a/packages/graphiql-react/src/index.ts +++ b/packages/graphiql-react/src/index.ts @@ -1,32 +1,37 @@ import './style/root.css'; export { - EditorContextProvider, - HeaderEditor, - ImagePreview, - QueryEditor, - ResponseEditor, - getAutoCompleteLeafs, - useCopyQuery, useEditorStore, - useHeaderEditor, - useMergeQuery, - usePrettifyEditors, + // + QueryEditor, useQueryEditor, - useResponseEditor, - useVariableEditor, - useEditorState, useOperationsEditorState, - useOptimisticState, + // + VariableEditor, + useVariableEditor, useVariablesEditorState, + // + HeaderEditor, + useHeaderEditor, useHeadersEditorState, - VariableEditor, + // + ResponseEditor, + useResponseEditor, + // + copyQuery, + prettifyEditors, + mergeQuery, + // + ImagePreview, + getAutoCompleteLeafs, + useEditorState, + useOptimisticState, } from './editor'; -export { ExecutionContextProvider, useExecutionStore } from './execution'; -export { PluginContextProvider, usePluginStore } from './plugin'; +export { useExecutionStore } from './execution'; +export { usePluginStore } from './plugin'; export { GraphiQLProvider } from './provider'; -export { SchemaContextProvider, useSchemaStore } from './schema'; -export { StorageContextProvider, useStorage } from './storage'; +export { useSchemaStore } from './schema'; +export { useStorage } from './storage'; export { useTheme } from './theme'; export * from './utility'; @@ -45,8 +50,8 @@ export type { UseVariableEditorArgs, WriteableEditorProps, } from './editor'; -export type { ExecutionContextType } from './execution'; -export type { GraphiQLPlugin, PluginContextType } from './plugin'; +export type { GraphiQLPlugin } from './plugin'; export type { SchemaContextType } from './schema'; export type { Theme } from './theme'; export { clsx as cn } from 'clsx'; +export { KEY_MAP } from './constants'; diff --git a/packages/graphiql-react/src/plugin.tsx b/packages/graphiql-react/src/plugin.tsx index 60778f830c5..6b4da222687 100644 --- a/packages/graphiql-react/src/plugin.tsx +++ b/packages/graphiql-react/src/plugin.tsx @@ -20,7 +20,7 @@ export type GraphiQLPlugin = { title: string; }; -export type PluginContextType = { +type PluginContextType = { /** * A list of all current plugins, including the built-in ones (the doc * explorer and the history). diff --git a/packages/graphiql-react/src/provider.tsx b/packages/graphiql-react/src/provider.tsx index 40d19b653c1..20290b36a8a 100644 --- a/packages/graphiql-react/src/provider.tsx +++ b/packages/graphiql-react/src/provider.tsx @@ -1,3 +1,4 @@ +/* eslint sort-keys: "error" */ import type { ComponentPropsWithoutRef, FC } from 'react'; import { EditorContextProvider } from './editor'; import { ExecutionContextProvider } from './execution'; @@ -14,41 +15,55 @@ type GraphiQLProviderProps = ComponentPropsWithoutRef; export const GraphiQLProvider: FC = ({ - children, - dangerouslyAssumeSchemaIsValid, - defaultQuery, defaultHeaders, + defaultQuery, defaultTabs, externalFragments, - fetcher, - getDefaultFieldNames, headers, - inputValueDeprecation, - introspectionQueryName, onEditOperationName, - onSchemaChange, onTabChange, - onTogglePluginVisibility, - operationName, - plugins, - referencePlugin, query, response, - schema, - schemaDescription, shouldPersistHeaders, - storage, validationRules, variables, + onCopyQuery, + onPrettifyQuery, + + dangerouslyAssumeSchemaIsValid, + fetcher, + inputValueDeprecation, + introspectionQueryName, + onSchemaChange, + schema, + schemaDescription, + + getDefaultFieldNames, + operationName, + + onTogglePluginVisibility, + plugins, + referencePlugin, visiblePlugin, + + storage, + + children, }) => { + if (!fetcher) { + throw new TypeError( + 'The `GraphiQLProvider` component requires a `fetcher` function to be passed as prop.', + ); + } const editorContextProps = { - defaultQuery, defaultHeaders, + defaultQuery, defaultTabs, externalFragments, headers, + onCopyQuery, onEditOperationName, + onPrettifyQuery, onTabChange, query, response, @@ -66,15 +81,15 @@ export const GraphiQLProvider: FC = ({ schemaDescription, }; const executionContextProps = { - getDefaultFieldNames, fetcher, + getDefaultFieldNames, operationName, }; const pluginContextProps = { onTogglePluginVisibility, plugins, - visiblePlugin, referencePlugin, + visiblePlugin, }; return ( diff --git a/packages/graphiql-react/src/schema.ts b/packages/graphiql-react/src/schema.ts index 14c943c4350..cd03053e5db 100644 --- a/packages/graphiql-react/src/schema.ts +++ b/packages/graphiql-react/src/schema.ts @@ -1,5 +1,4 @@ import { - Fetcher, FetcherOpts, fetcherReturnToPromise, formatError, @@ -20,6 +19,7 @@ import { createStore } from 'zustand'; import { editorStore } from './editor/context'; import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; import { createBoundedUseStore } from './utility'; +import { executionStore, useExecutionStore } from './execution'; type MaybeGraphQLSchema = GraphQLSchema | null | undefined; @@ -29,7 +29,6 @@ type SchemaStore = SchemaContextType & | 'inputValueDeprecation' | 'introspectionQueryName' | 'schemaDescription' - | 'fetcher' | 'onSchemaChange' >; @@ -57,13 +56,7 @@ export const schemaStore = createStore((set, get) => ({ * Fetch the schema */ async introspect() { - const { - requestCounter, - fetcher, - onSchemaChange, - shouldIntrospect, - ...rest - } = get(); + const { requestCounter, shouldIntrospect, onSchemaChange, ...rest } = get(); /** * Only introspect if there is no schema provided via props. If the @@ -97,7 +90,7 @@ export const schemaStore = createStore((set, get) => ({ introspectionQueryName, introspectionQuerySansSubscriptions, } = generateIntrospectionQuery(rest); - + const { fetcher } = executionStore.getState(); const fetch = fetcherReturnToPromise( fetcher( { @@ -238,17 +231,7 @@ type SchemaContextProviderProps = { * @default false */ dangerouslyAssumeSchemaIsValid?: boolean; - /** - * A function which accepts GraphQL HTTP parameters and returns a `Promise`, - * `Observable` or `AsyncIterable` that returns the GraphQL response in - * parsed JSON format. - * - * We suggest using the `createGraphiQLFetcher` utility from `@graphiql/toolkit` - * to create these fetcher functions. - * - * @see {@link https://graphiql-test.netlify.app/typedoc/modules/graphiql_toolkit.html#creategraphiqlfetcher-2|`createGraphiQLFetcher`} - */ - fetcher: Fetcher; + /** * Invoked after a new GraphQL schema was built. This includes both fetching * the schema via introspection and passing the schema using the `schema` @@ -276,7 +259,6 @@ type SchemaContextProviderProps = { } & IntrospectionArgs; export const SchemaContextProvider: FC = ({ - fetcher, onSchemaChange, dangerouslyAssumeSchemaIsValid = false, children, @@ -285,11 +267,8 @@ export const SchemaContextProvider: FC = ({ introspectionQueryName = 'IntrospectionQuery', schemaDescription = false, }) => { - if (!fetcher) { - throw new TypeError( - 'The `SchemaContextProvider` component requires a `fetcher` function to be passed as prop.', - ); - } + const fetcher = useExecutionStore(store => store.fetcher); + /** * Synchronize prop changes with state */ @@ -302,7 +281,6 @@ export const SchemaContextProvider: FC = ({ : validateSchema(newSchema); schemaStore.setState(({ requestCounter }) => ({ - fetcher, onSchemaChange, schema: newSchema, shouldIntrospect: !isSchema(schema) && schema !== null, @@ -323,10 +301,10 @@ export const SchemaContextProvider: FC = ({ schema, dangerouslyAssumeSchemaIsValid, onSchemaChange, - fetcher, inputValueDeprecation, introspectionQueryName, schemaDescription, + fetcher, // should refresh schema with new fetcher after a fetchError ]); /** diff --git a/packages/graphiql-react/src/toolbar/execute.tsx b/packages/graphiql-react/src/toolbar/execute.tsx index 9addafbb2a9..9cd9c2bb87a 100644 --- a/packages/graphiql-react/src/toolbar/execute.tsx +++ b/packages/graphiql-react/src/toolbar/execute.tsx @@ -3,6 +3,7 @@ import { useEditorStore } from '../editor'; import { useExecutionStore } from '../execution'; import { PlayIcon, StopIcon } from '../icons'; import { DropdownMenu, Tooltip } from '../ui'; +import { KEY_MAP } from '../constants'; import './execute.css'; export const ExecuteButton: FC = () => { @@ -14,7 +15,7 @@ export const ExecuteButton: FC = () => { const hasOptions = operations.length > 1 && typeof operationName !== 'string'; const isRunning = isFetching || Boolean(subscription); - const label = `${isRunning ? 'Stop' : 'Execute'} query (Ctrl-Enter)`; + const label = `${isRunning ? 'Stop' : 'Execute'} query (${KEY_MAP.runQuery[0]})`; const buttonProps = { type: 'button' as const, className: 'graphiql-execute-button', diff --git a/packages/graphiql-react/src/ui/markdown.tsx b/packages/graphiql-react/src/ui/markdown.tsx index 1468a43b01e..99a6b843702 100644 --- a/packages/graphiql-react/src/ui/markdown.tsx +++ b/packages/graphiql-react/src/ui/markdown.tsx @@ -1,6 +1,6 @@ import { forwardRef, JSX } from 'react'; import { clsx } from 'clsx'; -import { markdown } from '../markdown'; +import { markdown } from '../utility'; import './markdown.css'; diff --git a/packages/graphiql-react/src/utility/index.ts b/packages/graphiql-react/src/utility/index.ts index 9f73b06bc03..37669ff4b6f 100644 --- a/packages/graphiql-react/src/utility/index.ts +++ b/packages/graphiql-react/src/utility/index.ts @@ -1,4 +1,5 @@ export { createBoundedUseStore } from './create-bounded-use-store'; export { debounce } from './debounce'; export { isMacOs } from './is-macos'; +export { markdown } from './markdown'; export { useDragResize } from './resize'; diff --git a/packages/graphiql-react/src/markdown.ts b/packages/graphiql-react/src/utility/markdown.ts similarity index 77% rename from packages/graphiql-react/src/markdown.ts rename to packages/graphiql-react/src/utility/markdown.ts index c1fe6a36465..ef56a177b82 100644 --- a/packages/graphiql-react/src/markdown.ts +++ b/packages/graphiql-react/src/utility/markdown.ts @@ -3,7 +3,7 @@ import MarkdownIt from 'markdown-it'; export const markdown = new MarkdownIt({ - // we don't want to convert \n to
because in markdown a single newline is not a line break + // we don't want to convert \n to
because in Markdown a single newline is not a line break // https://github.com/graphql/graphiql/issues/3155 breaks: false, linkify: true, diff --git a/packages/graphiql/src/GraphiQL.spec.tsx b/packages/graphiql/src/GraphiQL.spec.tsx index 6c6a782dd13..36079195ca3 100644 --- a/packages/graphiql/src/GraphiQL.spec.tsx +++ b/packages/graphiql/src/GraphiQL.spec.tsx @@ -43,7 +43,7 @@ describe('GraphiQL', () => { // @ts-expect-error fetcher is a required prop to GraphiQL expect(() => render()).toThrow( - 'The `SchemaContextProvider` component requires a `fetcher` function to be passed as prop.', + 'The `GraphiQLProvider` component requires a `fetcher` function to be passed as prop.', ); spy.mockRestore(); }); @@ -100,6 +100,7 @@ describe('GraphiQL', () => { function firstFetcher() { return Promise.reject('Schema Error'); } + function secondFetcher() { return Promise.resolve(simpleIntrospection); } diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index b705ac33138..806dd88db46 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -10,7 +10,7 @@ import type { FC, ComponentPropsWithoutRef, } from 'react'; -import { Fragment, useState, useEffect, Children, cloneElement } from 'react'; +import { Fragment, useState, useEffect, Children } from 'react'; import { Button, ButtonGroup, @@ -36,14 +36,14 @@ import { ToolbarButton, Tooltip, UnStyledButton, - useCopyQuery, + copyQuery, useDragResize, useEditorStore, useExecutionStore, UseHeaderEditorArgs, - useMergeQuery, + mergeQuery, usePluginStore, - usePrettifyEditors, + prettifyEditors, UseQueryEditorArgs, UseResponseEditorArgs, useSchemaStore, @@ -54,6 +54,7 @@ import { WriteableEditorProps, isMacOs, cn, + KEY_MAP, } from '@graphiql/react'; import { HistoryContextProvider, @@ -83,34 +84,26 @@ export type GraphiQLProps = * @see https://github.com/graphql/graphiql#usage */ const GraphiQL_: FC = ({ - dangerouslyAssumeSchemaIsValid, - confirmCloseTab, - defaultQuery, - defaultTabs, - externalFragments, - fetcher, - getDefaultFieldNames, - headers, - inputValueDeprecation, - introspectionQueryName, maxHistoryLength, - onEditOperationName, - onSchemaChange, - onTabChange, - onTogglePluginVisibility, - operationName, plugins = [], referencePlugin = DOC_EXPLORER_PLUGIN, - query, - response, - schema, - schemaDescription, - shouldPersistHeaders, - storage, - validationRules, - variables, - visiblePlugin, - defaultHeaders, + + editorTheme, + keyMap, + readOnly, + onEditQuery, + onEditVariables, + onEditHeaders, + responseTooltip, + defaultEditorToolsVisibility, + isHeadersEditorEnabled, + showPersistHeadersSettings, + defaultTheme, + forcedTheme, + confirmCloseTab, + className, + + children, ...props }) => { // @ts-expect-error -- Prop is removed @@ -125,44 +118,34 @@ const GraphiQL_: FC = ({ '`toolbar.additionalComponent` was removed. Use render props on `GraphiQL.Toolbar` component instead.', ); } - const graphiqlProps = { - getDefaultFieldNames, - dangerouslyAssumeSchemaIsValid, - defaultQuery, - defaultHeaders, - defaultTabs, - externalFragments, - fetcher, - headers, - inputValueDeprecation, - introspectionQueryName, - onEditOperationName, - onSchemaChange, - onTabChange, - onTogglePluginVisibility, - plugins: [referencePlugin, HISTORY_PLUGIN, ...plugins], - referencePlugin, - visiblePlugin, - operationName, - query, - response, - schema, - schemaDescription, - shouldPersistHeaders, - storage, - validationRules, - variables, + const interfaceProps: GraphiQLInterfaceProps = { + // TODO check if `showPersistHeadersSettings` prop is needed, or we can just use `shouldPersistHeaders` instead + showPersistHeadersSettings: + showPersistHeadersSettings ?? props.shouldPersistHeaders !== false, + editorTheme, + keyMap, + readOnly, + onEditQuery, + onEditVariables, + onEditHeaders, + responseTooltip, + defaultEditorToolsVisibility, + isHeadersEditorEnabled: isHeadersEditorEnabled ?? true, + defaultTheme, + forcedTheme: + forcedTheme && THEMES.includes(forcedTheme) ? forcedTheme : undefined, + confirmCloseTab, + className, }; return ( - + - + {children} @@ -175,7 +158,6 @@ type AddSuffix, Suffix extends string> = { export type GraphiQLInterfaceProps = WriteableEditorProps & AddSuffix, 'Query'> & - Pick & AddSuffix, 'Variables'> & AddSuffix, 'Headers'> & Pick & { @@ -225,8 +207,25 @@ const THEMES = ['light', 'dark', 'system'] as const; const TAB_CLASS_PREFIX = 'graphiql-session-tab-'; -export const GraphiQLInterface: FC = props => { - const isHeadersEditorEnabled = props.isHeadersEditorEnabled ?? true; +type ButtonHandler = MouseEventHandler; + +export const GraphiQLInterface: FC = ({ + forcedTheme, + isHeadersEditorEnabled, + defaultTheme, + defaultEditorToolsVisibility, + children, + confirmCloseTab, + className, + editorTheme, + keyMap, + onEditQuery, + readOnly, + onEditVariables, + onEditHeaders, + responseTooltip, + showPersistHeadersSettings, +}) => { const { initialVariables, initialHeaders, @@ -239,15 +238,11 @@ export const GraphiQLInterface: FC = props => { tabs, activeTabIndex, } = useEditorStore(); - const executionContext = useExecutionStore(); + const isExecutionFetching = useExecutionStore(store => store.isFetching); const { isFetching: isSchemaFetching, introspect } = useSchemaStore(); const storageContext = useStorage(); const { visiblePlugin, setVisiblePlugin, plugins } = usePluginStore(); - const forcedTheme = - props.forcedTheme && THEMES.includes(props.forcedTheme) - ? props.forcedTheme - : undefined; - const { theme, setTheme } = useTheme(props.defaultTheme); + const { theme, setTheme } = useTheme(defaultTheme); useEffect(() => { if (forcedTheme === 'system') { @@ -280,14 +275,14 @@ export const GraphiQLInterface: FC = props => { direction: 'vertical', initiallyHidden: (() => { if ( - props.defaultEditorToolsVisibility === 'variables' || - props.defaultEditorToolsVisibility === 'headers' + defaultEditorToolsVisibility === 'variables' || + defaultEditorToolsVisibility === 'headers' ) { return; } - if (typeof props.defaultEditorToolsVisibility === 'boolean') { - return props.defaultEditorToolsVisibility ? undefined : 'second'; + if (typeof defaultEditorToolsVisibility === 'boolean') { + return defaultEditorToolsVisibility ? undefined : 'second'; } return initialVariables || initialHeaders ? undefined : 'second'; @@ -300,10 +295,10 @@ export const GraphiQLInterface: FC = props => { 'variables' | 'headers' >(() => { if ( - props.defaultEditorToolsVisibility === 'variables' || - props.defaultEditorToolsVisibility === 'headers' + defaultEditorToolsVisibility === 'variables' || + defaultEditorToolsVisibility === 'headers' ) { - return props.defaultEditorToolsVisibility; + return defaultEditorToolsVisibility; } return !initialVariables && initialHeaders && isHeadersEditorEnabled ? 'headers' @@ -316,7 +311,7 @@ export const GraphiQLInterface: FC = props => { 'success' | 'error' | null >(null); - const { logo, toolbar, footer } = Children.toArray(props.children).reduce<{ + const { logo, toolbar, footer } = Children.toArray(children).reduce<{ logo?: ReactNode; toolbar?: ReactNode; footer?: ReactNode; @@ -327,11 +322,7 @@ export const GraphiQLInterface: FC = props => { acc.logo = curr; break; case GraphiQL.Toolbar: - // @ts-expect-error -- fix type error - acc.toolbar = cloneElement(curr, { - onCopyQuery: props.onCopyQuery, - onPrettifyQuery: props.onPrettifyQuery, - }); + acc.toolbar = curr; break; case GraphiQL.Footer: acc.footer = curr; @@ -341,36 +332,30 @@ export const GraphiQLInterface: FC = props => { }, { logo: , - toolbar: ( -