diff --git a/.changeset/nasty-cows-train.md b/.changeset/nasty-cows-train.md new file mode 100644 index 0000000000..5cf9a22b9d --- /dev/null +++ b/.changeset/nasty-cows-train.md @@ -0,0 +1,9 @@ +--- +'@graphiql/react': minor +'graphiql': major +--- + +- Remove `query`, `variables`, `headers`, and `response` props from `` and `` +- Add `initialQuery`, `initialVariables` and `initialHeaders` props +- Fix `defaultQuery`, when is set will only be used for the first tab. When opening more tabs, the query editor will start out empty +- remove `useSynchronizeValue` hook diff --git a/packages/graphiql-react/src/components/header-editor.tsx b/packages/graphiql-react/src/components/header-editor.tsx index 0e57eaf563..9ecee47119 100644 --- a/packages/graphiql-react/src/components/header-editor.tsx +++ b/packages/graphiql-react/src/components/header-editor.tsx @@ -33,7 +33,6 @@ export const HeaderEditor: FC = ({ onEdit, ...props }) => { ); useEffect(() => { const model = getOrCreateModel({ uri: HEADER_URI, value: initialHeaders }); - // Build the editor const editor = createEditor(ref, { model }); setEditor({ headerEditor: editor }); const disposables = [ diff --git a/packages/graphiql-react/src/components/provider.tsx b/packages/graphiql-react/src/components/provider.tsx index 2825f37726..98df19e15d 100644 --- a/packages/graphiql-react/src/components/provider.tsx +++ b/packages/graphiql-react/src/components/provider.tsx @@ -1,11 +1,5 @@ /* eslint sort-keys: "error" */ -import type { - ComponentPropsWithoutRef, - FC, - ReactElement, - ReactNode, - RefObject, -} from 'react'; +import type { ComponentPropsWithoutRef, FC, ReactNode, RefObject } from 'react'; import { createContext, useContext, useRef, useEffect } from 'react'; import { create, useStore, UseBoundStore, StoreApi } from 'zustand'; import { useShallow } from 'zustand/shallow'; @@ -22,7 +16,7 @@ import { import { StorageStore, useStorage } from '../stores/storage'; import { ThemeStore } from '../stores/theme'; import type { SlicesWithActions } from '../types'; -import { pick, useDidUpdate, useSynchronizeValue } from '../utility'; +import { useDidUpdate } from '../utility'; import { FragmentDefinitionNode, parse, @@ -69,8 +63,56 @@ export const GraphiQLProvider: FC = ({ } // @ts-expect-error -- runtime check if (props.validationRules) { - throw new Error( - '`validationRules` prop is removed. Use custom GraphQL worker, see https://github.com/graphql/graphiql/tree/main/packages/monaco-graphql#custom-webworker-for-passing-non-static-config-to-worker.', + throw new TypeError( + 'The `validationRules` prop has been removed. Use custom GraphQL worker, see https://github.com/graphql/graphiql/tree/main/packages/monaco-graphql#custom-webworker-for-passing-non-static-config-to-worker.', + ); + } + // @ts-expect-error -- runtime check + if (props.query) { + throw new TypeError( + 'The `query` prop has been removed. Use `initialQuery` prop instead, or set value programmatically using:\n' + + ` +const queryEditor = useGraphiQL(state => state.queryEditor) + +useEffect(() => { + queryEditor.setValue(query) +}, [query])`, + ); + } + // @ts-expect-error -- runtime check + if (props.variables) { + throw new TypeError( + 'The `variables` prop has been removed. Use `initialVariables` prop instead, or set value programmatically using:\n' + + ` +const variableEditor = useGraphiQL(state => state.variableEditor) + +useEffect(() => { + variableEditor.setValue(variables) +}, [variables])`, + ); + } + // @ts-expect-error -- runtime check + if (props.headers) { + throw new TypeError( + 'The `headers` prop has been removed. Use `initialHeaders` prop instead, or set value programmatically using:\n' + + ` +const headerEditor = useGraphiQL(state => state.headerEditor) + +useEffect(() => { + headerEditor.setValue(headers) +}, [headers])`, + ); + } + // @ts-expect-error -- runtime check + if (props.response) { + throw new TypeError( + 'The `response` prop has been removed. Set value programmatically using:\n' + + ` +const responseEditor = useGraphiQL(state => state.responseEditor) + +useEffect(() => { + responseEditor.setValue(response) +}, [response])`, ); } return ( @@ -82,14 +124,9 @@ export const GraphiQLProvider: FC = ({ ); }; -interface SynchronizeValueProps - extends Pick { - children: ReactNode; -} - const InnerGraphiQLProvider: FC = ({ defaultHeaders, - defaultQuery, + defaultQuery = DEFAULT_QUERY, defaultTabs, externalFragments, onEditOperationName, @@ -114,6 +151,7 @@ const InnerGraphiQLProvider: FC = ({ referencePlugin, visiblePlugin, children, + ...props }) => { const storage = useStorage(); @@ -137,15 +175,14 @@ const InnerGraphiQLProvider: FC = ({ function getInitialState() { // We only need to compute it lazily during the initial render. - const query = props.query ?? storage.get(STORAGE_KEY.query) ?? null; + const query = props.initialQuery ?? storage.get(STORAGE_KEY.query); const variables = - props.variables ?? storage.get(STORAGE_KEY.variables) ?? null; - const headers = props.headers ?? storage.get(STORAGE_KEY.headers) ?? null; - const response = props.response ?? ''; + props.initialVariables ?? storage.get(STORAGE_KEY.variables); + const headers = props.initialHeaders ?? storage.get(STORAGE_KEY.headers); const { tabs, activeTabIndex } = getDefaultTabState({ defaultHeaders, - defaultQuery: defaultQuery ?? DEFAULT_QUERY, + defaultQuery, defaultTabs, headers, query, @@ -169,7 +206,6 @@ const InnerGraphiQLProvider: FC = ({ initialHeaders: headers ?? defaultHeaders ?? '', initialQuery: query ?? (activeTabIndex === 0 ? tabs[0]!.query : null) ?? '', - initialResponse: response, initialVariables: variables ?? '', onCopyQuery, onEditOperationName, @@ -289,34 +325,15 @@ const InnerGraphiQLProvider: FC = ({ return ( - {children} + {children} ); }; -const SynchronizeValue: FC = ({ - children, - headers, - query, - response, - variables, -}) => { - const { headerEditor, queryEditor, responseEditor, variableEditor } = - useGraphiQL( - pick('headerEditor', 'queryEditor', 'responseEditor', 'variableEditor'), - ); - - useSynchronizeValue(headerEditor, headers); - useSynchronizeValue(queryEditor, query); - useSynchronizeValue(responseEditor, response); - useSynchronizeValue(variableEditor, variables); - return children as ReactElement; -}; - export function useGraphiQL(selector: (state: SlicesWithActions) => T): T { const store = useContext(GraphiQLContext); if (!store) { - throw new Error('Missing `GraphiQLContext.Provider` in the tree'); + throw new Error('Missing `GraphiQLContext.Provider` in the tree.'); } return useStore(store.current, useShallow(selector)); } diff --git a/packages/graphiql-react/src/components/query-editor.tsx b/packages/graphiql-react/src/components/query-editor.tsx index 93facace90..c32724715d 100644 --- a/packages/graphiql-react/src/components/query-editor.tsx +++ b/packages/graphiql-react/src/components/query-editor.tsx @@ -218,10 +218,6 @@ export const QueryEditor: FC = ({ storage.set(STORAGE_KEY.query, query); const operationFacts = getAndUpdateOperationFacts(editor); - if (operationFacts?.operationName !== undefined) { - storage.set(STORAGE_KEY.operationName, operationFacts.operationName); - } - // Invoke callback props only after the operation facts have been updated onEdit?.(query, operationFacts?.documentAST); if ( diff --git a/packages/graphiql-react/src/components/response-editor.tsx b/packages/graphiql-react/src/components/response-editor.tsx index a23d5d38b6..8188d4efda 100644 --- a/packages/graphiql-react/src/components/response-editor.tsx +++ b/packages/graphiql-react/src/components/response-editor.tsx @@ -39,15 +39,9 @@ export const ResponseEditor: FC = ({ ...props }) => { const { setEditor, run } = useGraphiQLActions(); - const { fetchError, validationErrors, initialResponse, responseEditor } = - useGraphiQL( - pick( - 'fetchError', - 'validationErrors', - 'initialResponse', - 'responseEditor', - ), - ); + const { fetchError, validationErrors, responseEditor } = useGraphiQL( + pick('fetchError', 'validationErrors', 'responseEditor'), + ); const ref = useRef(null!); useEffect(() => { if (fetchError) { @@ -59,11 +53,7 @@ export const ResponseEditor: FC = ({ }, [responseEditor, fetchError, validationErrors]); useEffect(() => { - const model = getOrCreateModel({ - uri: RESPONSE_URI, - value: initialResponse, - }); - // Build the editor + const model = getOrCreateModel({ uri: RESPONSE_URI, value: '' }); const editor = createEditor(ref, { model, readOnly: true, diff --git a/packages/graphiql-react/src/constants.ts b/packages/graphiql-react/src/constants.ts index 9fe12b54ec..d52f87ad77 100644 --- a/packages/graphiql-react/src/constants.ts +++ b/packages/graphiql-react/src/constants.ts @@ -48,7 +48,6 @@ export const STORAGE_KEY = { query: 'query', variables: 'variables', tabs: 'tabState', - operationName: 'operationName', persistHeaders: 'shouldPersistHeaders', theme: 'theme', } as const; diff --git a/packages/graphiql-react/src/stores/editor.ts b/packages/graphiql-react/src/stores/editor.ts index 8e0b8974ad..fad01441b5 100644 --- a/packages/graphiql-react/src/stores/editor.ts +++ b/packages/graphiql-react/src/stores/editor.ts @@ -54,12 +54,6 @@ export interface EditorSlice extends TabsState { */ initialQuery: string; - /** - * The contents of the response editor when initially rendering the provider - * component. - */ - initialResponse: string; - /** * The contents of the variables editor when initially rendering the provider * component. @@ -78,12 +72,13 @@ export interface EditorSlice extends TabsState { shouldPersistHeaders: boolean; /** - * The initial contents of the query editor when loading GraphiQL and there - * is no other source for the editor state. Other sources can be: - * - The `query` prop - * - The value persisted in storage - * These default contents will only be used for the first tab. When opening - * more tabs, the query editor will start out empty. + * The initial content of the query editor when loading GraphiQL and there is + * no saved query in storage and no `initialQuery` prop. + * + * This value is used only for the first tab. Additional tabs will open with + * an empty query editor. + * + * @default "# Welcome to GraphiQL..." */ defaultQuery?: string; @@ -245,14 +240,6 @@ export interface EditorProps */ externalFragments?: string | FragmentDefinitionNode[]; - /** - * This prop can be used to set the contents of the headers editor. Every - * time this prop changes, the contents of the headers editor are replaced. - * Note that the editor contents can be changed in between these updates by - * typing in the editor. - */ - headers?: string; - /** * This prop can be used to define the default set of tabs, with their * queries, variables, and headers. It will be used as default only if @@ -270,22 +257,6 @@ export interface EditorProps */ defaultTabs?: TabDefinition[]; - /** - * This prop can be used to set the contents of the query editor. Every time - * this prop changes, the contents of the query editor are replaced. Note - * that the editor contents can be changed in between these updates by typing - * in the editor. - */ - query?: string; - - /** - * This prop can be used to set the contents of the response editor. Every - * time this prop changes, the contents of the response editor are replaced. - * Note that the editor contents can change in between these updates by - * executing queries that will show a response. - */ - response?: string; - /** * This prop toggles if the contents of the headers editor are persisted in * storage. @@ -293,15 +264,10 @@ export interface EditorProps */ shouldPersistHeaders?: boolean; - /** - * This prop can be used to set the contents of the variables editor. Every - * time this prop changes, the contents of the variables editor are replaced. - * Note that the editor contents can be changed in between these updates by - * typing in the editor. - */ - variables?: string; - onPrettifyQuery?: EditorSlice['onPrettifyQuery']; + initialQuery?: EditorSlice['initialQuery']; + initialVariables?: EditorSlice['initialVariables']; + initialHeaders?: EditorSlice['initialHeaders']; } type CreateEditorSlice = ( @@ -313,7 +279,6 @@ type CreateEditorSlice = ( | 'initialQuery' | 'initialVariables' | 'initialHeaders' - | 'initialResponse' | 'onEditOperationName' | 'externalFragments' | 'onTabChange' @@ -373,36 +338,21 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { const $actions: EditorActions = { addTab() { - set( - ({ - defaultQuery, - defaultHeaders, - onTabChange, + set(({ defaultHeaders, onTabChange, tabs, activeTabIndex, actions }) => { + // Make sure the current tab stores the latest values + const updatedValues = synchronizeActiveTabValues({ tabs, activeTabIndex, - actions, - }) => { - // Make sure the current tab stores the latest values - const updatedValues = synchronizeActiveTabValues({ - tabs, - activeTabIndex, - }); - const updated = { - tabs: [ - ...updatedValues.tabs, - createTab({ - headers: defaultHeaders, - query: defaultQuery, - }), - ], - activeTabIndex: updatedValues.tabs.length, - }; - actions.storeTabs(updated); - setEditorValues(updated.tabs[updated.activeTabIndex]!); - onTabChange?.(updated); - return updated; - }, - ); + }); + const updated = { + tabs: [...updatedValues.tabs, createTab({ headers: defaultHeaders })], + activeTabIndex: updatedValues.tabs.length, + }; + actions.storeTabs(updated); + setEditorValues(updated.tabs[updated.activeTabIndex]!); + onTabChange?.(updated); + return updated; + }); }, changeTab(index) { set(({ actions, onTabChange, tabs }) => { @@ -516,9 +466,8 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { try { await navigator.clipboard.writeText(query); } catch (error) { - const msg = error instanceof Error ? error.message : error; // eslint-disable-next-line no-console - console.error('Failed to copy query!', msg); + console.warn('Failed to copy query!', error); } }, async prettifyEditors() { @@ -534,7 +483,7 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { } } catch (error) { // eslint-disable-next-line no-console - console.error( + console.warn( 'Parsing variables JSON failed, skip prettification.', error, ); @@ -550,7 +499,7 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { } } catch (error) { // eslint-disable-next-line no-console - console.error( + console.warn( 'Parsing headers JSON failed, skip prettification.', error, ); @@ -568,7 +517,7 @@ export const createEditorSlice: CreateEditorSlice = initial => (set, get) => { } } catch (error) { // eslint-disable-next-line no-console - console.error('Parsing query failed, skip prettification.', error); + console.warn('Parsing query failed, skip prettification.', error); } }, mergeQuery() { diff --git a/packages/graphiql-react/src/utility/hooks.ts b/packages/graphiql-react/src/utility/hooks.ts index 9205c2eba9..f7b8343588 100644 --- a/packages/graphiql-react/src/utility/hooks.ts +++ b/packages/graphiql-react/src/utility/hooks.ts @@ -2,18 +2,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { storageStore } from '../stores'; import { debounce } from './debounce'; -import type { MonacoEditor } from '../types'; import type { editor as monacoEditor } from '../monaco-editor'; import { useGraphiQL, useGraphiQLActions } from '../components'; -export function useSynchronizeValue(editor?: MonacoEditor, value?: string) { - useEffect(() => { - if (typeof value === 'string' && editor && editor.getValue() !== value) { - editor.setValue(value); - } - }, [editor, value]); -} - export function useChangeHandler( callback: ((value: string) => void) | undefined, storageKey: string | null, diff --git a/packages/graphiql-react/src/utility/index.ts b/packages/graphiql-react/src/utility/index.ts index b6463bd46c..d52f006c9c 100644 --- a/packages/graphiql-react/src/utility/index.ts +++ b/packages/graphiql-react/src/utility/index.ts @@ -12,7 +12,6 @@ export { pick } from './pick'; export { useDragResize } from './resize'; export { clsx as cn } from 'clsx'; export { - useSynchronizeValue, useOptimisticState, useEditorState, useOperationsEditorState, diff --git a/packages/graphiql/cypress.config.ts b/packages/graphiql/cypress.config.ts index 5a837a00d6..e48832c01c 100644 --- a/packages/graphiql/cypress.config.ts +++ b/packages/graphiql/cypress.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ e2e: { baseUrl: `http://localhost:${PORT}`, }, - video: false, + video: true, viewportWidth: 1920, viewportHeight: 1080, }); diff --git a/packages/graphiql/cypress/e2e/headers.cy.ts b/packages/graphiql/cypress/e2e/headers.cy.ts index 7a88f950f5..73e610ae25 100644 --- a/packages/graphiql/cypress/e2e/headers.cy.ts +++ b/packages/graphiql/cypress/e2e/headers.cy.ts @@ -1,26 +1,12 @@ const DEFAULT_HEADERS = '{"foo":2}'; describe('Headers', () => { - describe('`defaultHeaders`', () => { - it('should have default headers while open new tabs', () => { - cy.visit(`?query={test}&defaultHeaders=${DEFAULT_HEADERS}`); - cy.assertHasValues({ query: '{test}', headersString: DEFAULT_HEADERS }); - cy.get('.graphiql-tab-add').click(); - cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); - cy.get('.graphiql-tab-add').click(); - cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); - }); - - it('in case `headers` and `defaultHeaders` are set, `headers` should be on 1st tab and `defaultHeaders` for other opened tabs', () => { - const HEADERS = '{"bar":true}'; - cy.visit( - `?query={test}&defaultHeaders=${DEFAULT_HEADERS}&headers=${HEADERS}`, - ); - cy.assertHasValues({ query: '{test}', headersString: HEADERS }); - cy.get('.graphiql-tab-add').click(); - cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); - cy.get('.graphiql-tab-add').click(); - cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); - }); + it('should have default query only on first tab and default headers in all tabs', () => { + cy.visit(`?defaultQuery={test}&defaultHeaders=${DEFAULT_HEADERS}`); + cy.assertHasValues({ query: '{test}', headersString: DEFAULT_HEADERS }); + cy.get('.graphiql-tab-add').click(); + cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); + cy.get('.graphiql-tab-add').click(); + cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); }); }); diff --git a/packages/graphiql/cypress/e2e/history.cy.ts b/packages/graphiql/cypress/e2e/history.cy.ts index 061af44770..e38a5b2598 100644 --- a/packages/graphiql/cypress/e2e/history.cy.ts +++ b/packages/graphiql/cypress/e2e/history.cy.ts @@ -9,22 +9,11 @@ import { } from '../fixtures/fixtures'; describe('history', () => { - it('defaults to closed history panel', () => { - cy.visit('/'); - cy.get('.graphiql-history').should('not.exist'); - }); - it('will save history item even when history panel is closed', () => { cy.visit('?query={test}'); cy.clickExecuteQuery(); cy.get('button[aria-label="Show History"]').click(); cy.get('ul.graphiql-history-items').should('have.length', 1); - }); - - it('will save history item even when history panel is closed', () => { - cy.visit('?query={test}'); - cy.clickExecuteQuery(); - cy.get('button[aria-label="Show History"]').click(); cy.get('ul.graphiql-history-items li').should('have.length', 1); }); diff --git a/packages/graphiql/cypress/support/commands.ts b/packages/graphiql/cypress/support/commands.ts index 0ca3ce67c3..e580076511 100644 --- a/packages/graphiql/cypress/support/commands.ts +++ b/packages/graphiql/cypress/support/commands.ts @@ -15,6 +15,7 @@ interface Op { headersString?: string; response?: Record; } + declare namespace Cypress { type MockResult = | { data: any } @@ -63,7 +64,7 @@ Cypress.Commands.add('clickPrettify', () => { }); Cypress.Commands.add('visitWithOp', ({ query, variables, variablesString }) => { - let url = `/?query=${encodeURIComponent(query)}`; + let url = `?query=${encodeURIComponent(query)}`; if (variables || variablesString) { url += `&variables=${encodeURIComponent( JSON.stringify(variables, null, 2) || variablesString, diff --git a/packages/graphiql/src/GraphiQL.spec.tsx b/packages/graphiql/src/GraphiQL.spec.tsx index a5bb9c7490..1dbbda7e1b 100644 --- a/packages/graphiql/src/GraphiQL.spec.tsx +++ b/packages/graphiql/src/GraphiQL.spec.tsx @@ -148,7 +148,7 @@ describe('GraphiQL', () => { it('should not throw error if schema missing and query provided', async () => { await act(async () => { expect(() => - render(), + render(), ).not.toThrow(); }); }); @@ -262,7 +262,7 @@ describe('GraphiQL', () => { const { container } = render( , ); diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index f053da2d9f..985212e986 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -78,13 +78,13 @@ const GraphiQL_: FC = ({ // @ts-expect-error -- Prop is removed if (props.toolbar?.additionalContent) { throw new TypeError( - '`toolbar.additionalContent` was removed. Use render props on `GraphiQL.Toolbar` component instead.', + 'The `toolbar.additionalContent` prop has been removed. Use render props on `GraphiQL.Toolbar` component instead.', ); } // @ts-expect-error -- Prop is removed if (props.toolbar?.additionalComponent) { throw new TypeError( - '`toolbar.additionalComponent` was removed. Use render props on `GraphiQL.Toolbar` component instead.', + 'The `toolbar.additionalComponent` prop has been removed. Use render props on `GraphiQL.Toolbar` component instead.', ); } // @ts-expect-error -- Prop is removed @@ -95,7 +95,7 @@ const GraphiQL_: FC = ({ } // @ts-expect-error -- Prop is removed if (props.readOnly) { - throw new TypeError('`readOnly` was removed.'); + throw new TypeError('The `readOnly` prop has been removed.'); } const interfaceProps: GraphiQLInterfaceProps = { // TODO check if `showPersistHeadersSettings` prop is needed, or we can just use `shouldPersistHeaders` instead diff --git a/packages/graphiql/src/e2e.ts b/packages/graphiql/src/e2e.ts index 830848b163..69829ff26a 100644 --- a/packages/graphiql/src/e2e.ts +++ b/packages/graphiql/src/e2e.ts @@ -22,12 +22,14 @@ interface Params { query?: string; variables?: string; headers?: string; + + defaultQuery?: string; + defaultHeaders?: string; + confirmCloseTab?: 'true'; onPrettifyQuery?: 'true'; forcedTheme?: 'light' | 'dark' | 'system'; - defaultQuery?: string; defaultTheme?: Theme; - defaultHeaders?: string; } // Parse the search string to get url parameters. @@ -101,10 +103,14 @@ const props: ComponentProps = { url: getSchemaUrl(), subscriptionUrl: 'ws://localhost:8081/subscriptions', }), - query: parameters.query, - variables: parameters.variables, - headers: parameters.headers, + + initialQuery: parameters.query, + initialVariables: parameters.variables, + initialHeaders: parameters.headers, + + defaultQuery: parameters.defaultQuery, defaultHeaders: parameters.defaultHeaders, + onEditQuery, onEditVariables, onEditHeaders, @@ -118,14 +124,15 @@ const props: ComponentProps = { parameters.onPrettifyQuery === 'true' ? onPrettifyQuery : undefined, onTabChange, forcedTheme: parameters.forcedTheme, - defaultQuery: parameters.defaultQuery, defaultTheme: parameters.defaultTheme, }; -root.render( - React.createElement( +function App() { + return React.createElement( React.StrictMode, null, React.createElement(GraphiQL, props), - ), -); + ); +} + +root.render(React.createElement(App));