diff --git a/.changeset/warm-shoes-boil.md b/.changeset/warm-shoes-boil.md new file mode 100644 index 00000000000..44805346809 --- /dev/null +++ b/.changeset/warm-shoes-boil.md @@ -0,0 +1,16 @@ +--- +'@graphiql/react': minor +'@graphiql/plugin-history': minor +'@graphiql/plugin-doc-explorer': minor +'graphiql': patch +--- + +- remove `useQueryEditor`, `useVariableEditor`, `useHeaderEditor`, `useResponseEditor` hooks +- remove `UseHeaderEditorArgs`, `UseQueryEditorArgs`, `UseResponseEditorArgs`, `UseVariableEditorArgs` exports +- rename components + - `StorageContextProvider` => `StorageStore` + - `EditorContextProvider` => `EditorStore` + - `SchemaContextProvider` => `SchemaStore` + - `ExecutionContextProvider` => `ExecutionStore` + - `HistoryContextProvider` => `HistoryStore` + - `ExplorerContextProvider` => `ExplorerStore` diff --git a/packages/graphiql-plugin-doc-explorer/README.md b/packages/graphiql-plugin-doc-explorer/README.md index 7a0dfdbbba8..34d7ec909b9 100644 --- a/packages/graphiql-plugin-doc-explorer/README.md +++ b/packages/graphiql-plugin-doc-explorer/README.md @@ -1 +1,6 @@ # `@graphiql/plugin-doc-explorer` + +## API + +- `useDocExplorer`: Handles the state for the doc explorer +- `useDocExplorerActions`: Actions related to the doc explorer diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/doc-explorer.spec.tsx b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/doc-explorer.spec.tsx index 76c11e54e30..71620efc856 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/doc-explorer.spec.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/doc-explorer.spec.tsx @@ -2,12 +2,12 @@ import { act, render } from '@testing-library/react'; import { GraphQLInt, GraphQLObjectType, GraphQLSchema } from 'graphql'; import { FC, useEffect } from 'react'; import { - DocExplorerContextProvider, + DocExplorerStore, useDocExplorer, useDocExplorerActions, } from '../../context'; import { DocExplorer } from '../doc-explorer'; -import { schemaStore } from '../../../../graphiql-react/dist/schema'; +import { schemaStore } from '../../../../graphiql-react/dist/stores/schema'; function makeSchema(fieldName = 'field') { return new GraphQLSchema({ @@ -43,9 +43,9 @@ const withErrorSchemaContext = { const DocExplorerWithContext: FC = () => { return ( - + - + ); }; @@ -109,9 +109,9 @@ describe('DocExplorer', () => { schema: initialSchema, }); const { container, rerender } = render( - + - , + , ); // First proper render of doc explorer @@ -122,9 +122,9 @@ describe('DocExplorer', () => { }); }); rerender( - + - , + , ); const [title] = container.querySelectorAll('.graphiql-doc-explorer-title'); @@ -138,9 +138,9 @@ describe('DocExplorer', () => { }); }); rerender( - + - , + , ); const [title2] = container.querySelectorAll('.graphiql-doc-explorer-title'); // Because `Query.field` still exists in the new schema, we can still render it @@ -171,9 +171,9 @@ describe('DocExplorer', () => { schema: initialSchema, }); const { container, rerender } = render( - + - , + , ); // First proper render of doc explorer @@ -184,9 +184,9 @@ describe('DocExplorer', () => { }); }); rerender( - + - , + , ); const title = container.querySelector('.graphiql-doc-explorer-title')!; @@ -200,9 +200,9 @@ describe('DocExplorer', () => { }); }); rerender( - + - , + , ); const title2 = container.querySelector('.graphiql-doc-explorer-title')!; // Because `Query.field` doesn't exist anymore, the top-most item we can render is `Query` diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-documentation.spec.tsx b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-documentation.spec.tsx index 728f5d53e21..0141a621080 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-documentation.spec.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/type-documentation.spec.tsx @@ -10,7 +10,7 @@ import { import { docExplorerStore } from '../../context'; import { TypeDocumentation } from '../type-documentation'; import { unwrapType } from './test-utils'; -import { schemaStore } from '../../../../graphiql-react/dist/schema'; +import { schemaStore } from '../../../../graphiql-react/dist/stores/schema'; const TypeDocumentationWithContext: FC<{ type: GraphQLNamedType }> = ({ type, diff --git a/packages/graphiql-plugin-doc-explorer/src/context.ts b/packages/graphiql-plugin-doc-explorer/src/context.ts index f00ace927a8..2483e1fe5d5 100644 --- a/packages/graphiql-plugin-doc-explorer/src/context.ts +++ b/packages/graphiql-plugin-doc-explorer/src/context.ts @@ -16,7 +16,7 @@ import { } from 'graphql'; import { FC, ReactElement, ReactNode, useEffect } from 'react'; import { - SchemaContextType, + SchemaReference, useSchemaStore, createBoundedUseStore, } from '@graphiql/react'; @@ -45,7 +45,7 @@ export type DocExplorerNavStack = [ ...DocExplorerNavStackItem[], ]; -export type DocExplorerContextType = { +export type DocExplorerStoreType = { /** * A stack of navigation items. The last item in the list is the current one. * This list always contains at least one item. @@ -67,7 +67,7 @@ export type DocExplorerContextType = { */ reset(): void; resolveSchemaReferenceToNavItem( - schemaReference: SchemaContextType['schemaReference'], + schemaReference: SchemaReference | null, ): void; /** * Replace the nav stack with an updated version using the new schema. @@ -78,7 +78,7 @@ export type DocExplorerContextType = { const INITIAL_NAV_STACK: DocExplorerNavStack = [{ name: 'Docs' }]; -export const docExplorerStore = createStore( +export const docExplorerStore = createStore( (set, get) => ({ explorerNavStack: INITIAL_NAV_STACK, actions: { @@ -235,7 +235,7 @@ export const docExplorerStore = createStore( }), ); -export const DocExplorerContextProvider: FC<{ +export const DocExplorerStore: FC<{ children: ReactNode; }> = ({ children }) => { const { schema, validationErrors, schemaReference } = useSchemaStore(); diff --git a/packages/graphiql-plugin-doc-explorer/src/index.tsx b/packages/graphiql-plugin-doc-explorer/src/index.tsx index 2b34c809eeb..37e5e47b849 100644 --- a/packages/graphiql-plugin-doc-explorer/src/index.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/index.tsx @@ -9,13 +9,12 @@ import { DocExplorer } from './components'; export * from './components'; export { - DocExplorerContextProvider, + DocExplorerStore, useDocExplorer, useDocExplorerActions, } from './context'; export type { - DocExplorerContextType, DocExplorerFieldDef, DocExplorerNavStack, DocExplorerNavStackItem, diff --git a/packages/graphiql-plugin-history/README.md b/packages/graphiql-plugin-history/README.md index 7a5f86745f6..7b0bfc28a01 100644 --- a/packages/graphiql-plugin-history/README.md +++ b/packages/graphiql-plugin-history/README.md @@ -1 +1,6 @@ # `@graphiql/plugin-history` + +## API + +- `useHistory`: Persists executed requests in storage +- `useHistoryActions`: Actions related to the history diff --git a/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx b/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx index a81b238badc..004000607ee 100644 --- a/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx +++ b/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx @@ -1,9 +1,9 @@ import { fireEvent, render } from '@testing-library/react'; import type { ComponentProps } from 'react'; import { formatQuery, HistoryItem } from '../components'; -import { HistoryContextProvider } from '../context'; +import { HistoryStore } from '../context'; import { Tooltip, GraphiQLProvider } from '@graphiql/react'; -import { editorStore } from '../../../graphiql-react/dist/editor/context'; +import { editorStore } from '../../../graphiql-react/dist/stores/editor'; const mockQuery = /* GraphQL */ ` query Test($string: String) { @@ -25,9 +25,9 @@ const QueryHistoryItemWithContext: typeof HistoryItem = props => { return ( - + - + ); diff --git a/packages/graphiql-plugin-history/src/context.tsx b/packages/graphiql-plugin-history/src/context.tsx index a7cc0d39466..62aed7455c7 100644 --- a/packages/graphiql-plugin-history/src/context.tsx +++ b/packages/graphiql-plugin-history/src/context.tsx @@ -1,7 +1,10 @@ // eslint-disable-next-line react/jsx-filename-extension -- TODO import { FC, ReactElement, ReactNode, useEffect } from 'react'; import { createStore } from 'zustand'; -import { HistoryStore, QueryStoreItem } from '@graphiql/toolkit'; +import { + HistoryStore as ToolkitHistoryStore, + QueryStoreItem, +} from '@graphiql/toolkit'; import { useExecutionStore, useEditorStore, @@ -9,7 +12,7 @@ import { createBoundedUseStore, } from '@graphiql/react'; -const historyStore = createStore((set, get) => ({ +const historyStore = createStore((set, get) => ({ historyStorage: null!, actions: { addToHistory(operation) { @@ -36,8 +39,8 @@ const historyStore = createStore((set, get) => ({ }, })); -type HistoryContextType = { - historyStorage: HistoryStore; +type HistoryStoreType = { + historyStorage: ToolkitHistoryStore; actions: { /** * Add an operation to the history. @@ -100,7 +103,7 @@ type HistoryContextType = { }; }; -type HistoryContextProviderProps = { +type HistoryStoreProps = { children: ReactNode; /** * The maximum number of executed operations to store. @@ -110,12 +113,12 @@ type HistoryContextProviderProps = { }; /** - * The functions send the entire operation so users can customize their own application with - * and get access to the operation plus - * any additional props they added for their needs (i.e., build their own functions that may save - * to a backend instead of localStorage and might need an id property added to the QueryStoreItem) + * The functions send the entire operation so users can customize their own application and get + * access to the operation plus any additional props they added for their needs (i.e., build their + * own functions that may save to a backend instead of localStorage and might need an id property + * added to the `QueryStoreItem`) */ -export const HistoryContextProvider: FC = ({ +export const HistoryStore: FC = ({ maxHistoryLength = 20, children, }) => { @@ -125,7 +128,7 @@ export const HistoryContextProvider: FC = ({ const storage = useStorage(); const historyStorage = // eslint-disable-line react-hooks/exhaustive-deps -- false positive, code is optimized by React Compiler - new HistoryStore(storage, maxHistoryLength); + new ToolkitHistoryStore(storage, maxHistoryLength); useEffect(() => { historyStore.setState({ historyStorage }); diff --git a/packages/graphiql-plugin-history/src/index.ts b/packages/graphiql-plugin-history/src/index.ts index 7f16625bd91..1ea1e91b7b3 100644 --- a/packages/graphiql-plugin-history/src/index.ts +++ b/packages/graphiql-plugin-history/src/index.ts @@ -11,8 +11,4 @@ export const HISTORY_PLUGIN: GraphiQLPlugin = { export { History }; -export { - HistoryContextProvider, - useHistory, - useHistoryActions, -} from './context'; +export { HistoryStore, useHistory, useHistoryActions } from './context'; diff --git a/packages/graphiql-react/README.md b/packages/graphiql-react/README.md index 0f3d461c6ff..586a7b38a96 100644 --- a/packages/graphiql-react/README.md +++ b/packages/graphiql-react/README.md @@ -81,24 +81,21 @@ Further details on how to use `@graphiql/react` can be found in the reference implementation of a GraphQL IDE - Graph*i*QL - in the [`graphiql` package](https://github.com/graphql/graphiql/blob/main/packages/graphiql/src/components/GraphiQL.tsx). -## Available contexts +## Available stores -There are multiple contexts that own different parts of the state that make up a -complete GraphQL IDE. For each context there is a provider component -(`ContextProvider`) that makes sure the context is initialized and managed +There are multiple stores that own different parts of the state that make up a +complete GraphQL IDE. For each store there is a component +(`Store`) that makes sure the store is initialized and managed properly. These components contains all the logic related to state management. -In addition, for each context there is also a hook (`useContext`) that -allows you to consume its current value. -Here is a list of all contexts that come with `@graphiql/react` +In addition, for each store, there is also a hook that +allows you to consume its current value: -- `StorageContext`: Provides a storage API that can be used to persist state in +- `useStorage`: Provides a storage API that can be used to persist state in the browser (by default using `localStorage`) -- `EditorContext`: Manages all the editors and tabs -- `SchemaContext`: Fetches, validates and stores the GraphQL schema -- `ExecutionContext`: Executes GraphQL requests -- `HistoryContext`: Persists executed requests in storage -- `ExplorerContext`: Handles the state for the docs explorer +- `useEditorStore`: Manages all the editors and tabs +- `useSchemaStore`: Fetches, validates and stores the GraphQL schema +- `useExecutionStore`: Executes GraphQL requests All context properties are documented using JSDoc comments. If you're using an IDE like VSCode for development these descriptions will show up in auto-complete diff --git a/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts index 5829be487ee..0f14810fc96 100644 --- a/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts +++ b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts @@ -7,7 +7,7 @@ import { clearHeadersFromTabs, STORAGE_KEY, } from '../tabs'; -import { storageStore } from '../../storage'; +import { storageStore } from '../../stores/storage'; describe('createTab', () => { it('creates with default title', () => { diff --git a/packages/graphiql-react/src/editor/completion.ts b/packages/graphiql-react/src/editor/completion.ts index f7330a06061..b9667277b0d 100644 --- a/packages/graphiql-react/src/editor/completion.ts +++ b/packages/graphiql-react/src/editor/completion.ts @@ -8,9 +8,8 @@ import { isNonNullType, } from 'graphql'; import { markdown } from '../utility'; -import { pluginStore } from '../plugin'; +import { pluginStore, schemaStore } from '../stores'; import { importCodeMirror } from './common'; -import { schemaStore } from '../schema'; /** * Render a custom UI for CodeMirror's hint which includes additional info diff --git a/packages/graphiql-react/src/editor/components/header-editor.tsx b/packages/graphiql-react/src/editor/components/header-editor.tsx deleted file mode 100644 index 3652b96f7e2..00000000000 --- a/packages/graphiql-react/src/editor/components/header-editor.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { FC, useEffect } from 'react'; -import { clsx } from 'clsx'; -import { useEditorStore } from '../context'; -import { useHeaderEditor, UseHeaderEditorArgs } from '../header-editor'; -import '../style/codemirror.css'; -import '../style/fold.css'; -import '../style/editor.css'; - -type HeaderEditorProps = UseHeaderEditorArgs & { - /** - * Visually hide the header editor. - * @default false - */ - isHidden?: boolean; -}; - -export const HeaderEditor: FC = ({ - isHidden, - ...hookArgs -}) => { - const headerEditor = useEditorStore(store => store.headerEditor); - const ref = useHeaderEditor(hookArgs); - - useEffect(() => { - if (!isHidden) { - headerEditor?.refresh(); - } - }, [headerEditor, isHidden]); - - return ( -
- ); -}; diff --git a/packages/graphiql-react/src/editor/components/index.ts b/packages/graphiql-react/src/editor/components/index.ts deleted file mode 100644 index 9fbe6db2a47..00000000000 --- a/packages/graphiql-react/src/editor/components/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { HeaderEditor } from './header-editor'; -export { ImagePreview } from './image-preview'; -export { QueryEditor } from './query-editor'; -export { ResponseEditor } from './response-editor'; -export { VariableEditor } from './variable-editor'; diff --git a/packages/graphiql-react/src/editor/components/query-editor.tsx b/packages/graphiql-react/src/editor/components/query-editor.tsx deleted file mode 100644 index c4dfead3a4f..00000000000 --- a/packages/graphiql-react/src/editor/components/query-editor.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { FC } from 'react'; -import { useQueryEditor, UseQueryEditorArgs } from '../query-editor'; -import '../style/codemirror.css'; -import '../style/fold.css'; -import '../style/lint.css'; -import '../style/hint.css'; -import '../style/info.css'; -import '../style/jump.css'; -import '../style/auto-insertion.css'; -import '../style/editor.css'; - -export const QueryEditor: FC = props => { - const ref = useQueryEditor(props); - return
; -}; diff --git a/packages/graphiql-react/src/editor/components/response-editor.tsx b/packages/graphiql-react/src/editor/components/response-editor.tsx deleted file mode 100644 index fb7c98ecc53..00000000000 --- a/packages/graphiql-react/src/editor/components/response-editor.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useResponseEditor, UseResponseEditorArgs } from '../response-editor'; -import { FC } from 'react'; -import '../style/codemirror.css'; -import '../style/fold.css'; -import '../style/info.css'; -import '../style/editor.css'; - -export const ResponseEditor: FC = props => { - const ref = useResponseEditor(props); - return ( -
- ); -}; diff --git a/packages/graphiql-react/src/editor/components/variable-editor.tsx b/packages/graphiql-react/src/editor/components/variable-editor.tsx deleted file mode 100644 index 6abb67ebba5..00000000000 --- a/packages/graphiql-react/src/editor/components/variable-editor.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { FC, useEffect } from 'react'; -import { clsx } from 'clsx'; - -import { useEditorStore } from '../context'; -import { useVariableEditor, UseVariableEditorArgs } from '../variable-editor'; - -import '../style/codemirror.css'; -import '../style/fold.css'; -import '../style/lint.css'; -import '../style/hint.css'; -import '../style/editor.css'; - -type VariableEditorProps = UseVariableEditorArgs & { - /** - * Visually hide the header editor. - * @default false - */ - isHidden?: boolean; -}; - -export const VariableEditor: FC = ({ - isHidden, - ...hookArgs -}) => { - const variableEditor = useEditorStore(store => store.variableEditor); - const ref = useVariableEditor(hookArgs); - - useEffect(() => { - if (!isHidden) { - variableEditor?.refresh(); - } - }, [variableEditor, isHidden]); - - return ( -
- ); -}; diff --git a/packages/graphiql-react/src/editor/header-editor.ts b/packages/graphiql-react/src/editor/header-editor.tsx similarity index 84% rename from packages/graphiql-react/src/editor/header-editor.ts rename to packages/graphiql-react/src/editor/header-editor.tsx index f2f1c8713b0..61449fd1cea 100644 --- a/packages/graphiql-react/src/editor/header-editor.ts +++ b/packages/graphiql-react/src/editor/header-editor.tsx @@ -6,7 +6,7 @@ import { DEFAULT_KEY_MAP, importCodeMirror, } from './common'; -import { useEditorStore } from './context'; +import { useEditorStore, useExecutionStore } from '../stores'; import { useChangeHandler, useKeyMap, @@ -15,15 +15,21 @@ import { useSynchronizeOption, } from './hooks'; import { WriteableEditorProps } from './types'; -import { useExecutionStore } from '../execution'; import { KEY_MAP } from '../constants'; +import { clsx } from 'clsx'; -export type UseHeaderEditorArgs = WriteableEditorProps & { +type HeaderEditorProps = WriteableEditorProps & { /** * Invoked when the contents of the headers editor change. * @param value The new contents of the editor. */ onEdit?(value: string): void; + + /** + * Visually hide the header editor. + * @default false + */ + isHidden?: boolean; }; // To make react-compiler happy, otherwise complains about using dynamic imports in Component @@ -34,12 +40,13 @@ function importCodeMirrorImports() { ]); } -export function useHeaderEditor({ +export function HeaderEditor({ editorTheme = DEFAULT_EDITOR_THEME, keyMap = DEFAULT_KEY_MAP, onEdit, readOnly = false, -}: UseHeaderEditorArgs = {}) { + isHidden = false, +}: HeaderEditorProps) { const { initialHeaders, headerEditor, @@ -47,7 +54,7 @@ export function useHeaderEditor({ shouldPersistHeaders, } = useEditorStore(); const run = useExecutionStore(store => store.run); - const ref = useRef(null); + const ref = useRef(null!); useEffect(() => { let isActive = true; @@ -59,10 +66,6 @@ export function useHeaderEditor({ } const container = ref.current; - if (!container) { - return; - } - const newEditor = CodeMirror(container, { value: initialHeaders, lineNumbers: true, @@ -119,7 +122,15 @@ export function useHeaderEditor({ useKeyMap(headerEditor, KEY_MAP.prettify, prettifyEditors); useKeyMap(headerEditor, KEY_MAP.mergeFragments, mergeQuery); - return ref; + useEffect(() => { + if (!isHidden) { + headerEditor?.refresh(); + } + }, [headerEditor, isHidden]); + + return ( +
+ ); } export const STORAGE_KEY = 'headers'; diff --git a/packages/graphiql-react/src/editor/hooks.ts b/packages/graphiql-react/src/editor/hooks.ts index 9442794dc91..2b21b39b199 100644 --- a/packages/graphiql-react/src/editor/hooks.ts +++ b/packages/graphiql-react/src/editor/hooks.ts @@ -1,17 +1,19 @@ 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 { 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 { schemaStore } from '../schema'; -import { storageStore } from '../storage'; +import { + schemaStore, + storageStore, + editorStore, + useEditorStore, + executionStore, +} from '../stores'; import { debounce } from '../utility'; import { onHasCompletion } from './completion'; -import { editorStore, useEditorStore } from './context'; -import { CodeMirrorEditor } from './types'; -import { executionStore } from '../execution'; +import { CodeMirrorEditor, SchemaReference } from './types'; export function useSynchronizeValue( editor: CodeMirrorEditor | null, diff --git a/packages/graphiql-react/src/editor/components/image-preview.tsx b/packages/graphiql-react/src/editor/image-preview.tsx similarity index 95% rename from packages/graphiql-react/src/editor/components/image-preview.tsx rename to packages/graphiql-react/src/editor/image-preview.tsx index 83a9c790629..3655d1d8d7b 100644 --- a/packages/graphiql-react/src/editor/components/image-preview.tsx +++ b/packages/graphiql-react/src/editor/image-preview.tsx @@ -15,14 +15,11 @@ const ImagePreview_: FC = props => { }); const [mime, setMime] = useState(null); - const ref = useRef(null); + const ref = useRef(null!); const src = tokenToURL(props.token)?.href; useEffect(() => { - if (!ref.current) { - return; - } if (!src) { setDimensions({ width: null, height: null }); setMime(null); diff --git a/packages/graphiql-react/src/editor/index.ts b/packages/graphiql-react/src/editor/index.ts index 1c548e52080..e77ba8be4e5 100644 --- a/packages/graphiql-react/src/editor/index.ts +++ b/packages/graphiql-react/src/editor/index.ts @@ -1,12 +1,9 @@ -export { - HeaderEditor, - ImagePreview, - QueryEditor, - ResponseEditor, - VariableEditor, -} from './components'; -export { EditorContextProvider, useEditorStore } from './context'; -export { useHeaderEditor } from './header-editor'; +export { HeaderEditor } from './header-editor'; +export { QueryEditor } from './query-editor'; +export { ResponseEditor, type ResponseTooltipType } from './response-editor'; +export { VariableEditor } from './variable-editor'; + +export { ImagePreview } from './image-preview'; export { getAutoCompleteLeafs, copyQuery, @@ -18,17 +15,10 @@ export { useVariablesEditorState, useHeadersEditorState, } from './hooks'; -export { useQueryEditor } from './query-editor'; -export { useResponseEditor } from './response-editor'; -export { useVariableEditor } from './variable-editor'; - -export type { UseHeaderEditorArgs } from './header-editor'; -export type { UseQueryEditorArgs } from './query-editor'; -export type { - ResponseTooltipType, - UseResponseEditorArgs, -} from './response-editor'; export type { TabsState } from './tabs'; -export type { UseVariableEditorArgs } from './variable-editor'; - -export type { CommonEditorProps, KeyMap, WriteableEditorProps } from './types'; +export type { + CommonEditorProps, + KeyMap, + WriteableEditorProps, + SchemaReference, +} from './types'; diff --git a/packages/graphiql-react/src/editor/query-editor.ts b/packages/graphiql-react/src/editor/query-editor.tsx similarity index 95% rename from packages/graphiql-react/src/editor/query-editor.ts rename to packages/graphiql-react/src/editor/query-editor.tsx index 04621a76091..715c12285d6 100644 --- a/packages/graphiql-react/src/editor/query-editor.ts +++ b/packages/graphiql-react/src/editor/query-editor.tsx @@ -1,5 +1,4 @@ import { getSelectedOperationName } from '@graphiql/toolkit'; -import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; import type { DocumentNode, FragmentDefinitionNode, @@ -12,18 +11,22 @@ import { OperationFacts, } from 'graphql-language-service'; import { RefObject, useEffect, useRef } from 'react'; -import { executionStore } from '../execution'; +import { + executionStore, + pluginStore, + schemaStore, + useSchemaStore, + useEditorStore, + useStorage, + CodeMirrorEditorWithOperationFacts, +} from '../stores'; import { markdown, debounce } from '../utility'; -import { pluginStore } from '../plugin'; -import { schemaStore, useSchemaStore } from '../schema'; -import { useStorage } from '../storage'; import { commonKeys, DEFAULT_EDITOR_THEME, DEFAULT_KEY_MAP, importCodeMirror, } from './common'; -import { CodeMirrorEditorWithOperationFacts, useEditorStore } from './context'; import { useCompletion, copyQuery, @@ -36,11 +39,12 @@ import { CodeMirrorEditor, CodeMirrorType, WriteableEditorProps, + SchemaReference, } from './types'; -import { normalizeWhitespace } from './whitespace'; +import { normalizeWhitespace } from '../utility/whitespace'; import { KEY_MAP } from '../constants'; -export type UseQueryEditorArgs = WriteableEditorProps & { +type QueryEditorProps = WriteableEditorProps & { /** * Invoked when a reference to the GraphQL schema (type or field) is clicked * as part of the editor or one of its tooltips. @@ -108,13 +112,13 @@ function updateEditorExternalFragments( editor.options.hintOptions.externalFragments = externalFragmentList; } -export function useQueryEditor({ +export function QueryEditor({ editorTheme = DEFAULT_EDITOR_THEME, keyMap = DEFAULT_KEY_MAP, onClickReference, onEdit, readOnly = false, -}: UseQueryEditorArgs = {}) { +}: QueryEditorProps) { const { initialQuery, queryEditor, @@ -124,11 +128,11 @@ export function useQueryEditor({ updateActiveTabValues, } = useEditorStore(); const storage = useStorage(); - const ref = useRef(null); + const ref = useRef(null!); const codeMirrorRef = useRef(undefined); const onClickReferenceRef = useRef< - NonNullable + NonNullable >(() => {}); useEffect(() => { @@ -156,10 +160,6 @@ export function useQueryEditor({ codeMirrorRef.current = CodeMirror; const container = ref.current; - if (!container) { - return; - } - const newEditor = CodeMirror(container, { value: initialQuery, lineNumbers: true, @@ -394,7 +394,7 @@ export function useQueryEditor({ useKeyMap(queryEditor, ['Shift-Ctrl-P', 'Shift-Ctrl-F'], prettifyEditors); useKeyMap(queryEditor, KEY_MAP.mergeFragments, mergeQuery); - return ref; + return
; } function useSynchronizeSchema( diff --git a/packages/graphiql-react/src/editor/response-editor.tsx b/packages/graphiql-react/src/editor/response-editor.tsx index c21bf96c643..07f37e8a012 100644 --- a/packages/graphiql-react/src/editor/response-editor.tsx +++ b/packages/graphiql-react/src/editor/response-editor.tsx @@ -2,7 +2,7 @@ import { formatError } from '@graphiql/toolkit'; import type { Position, Token } from 'codemirror'; import { ComponentType, useEffect, useRef, JSX } from 'react'; import { createRoot } from 'react-dom/client'; -import { useSchemaStore } from '../schema'; +import { useSchemaStore, useEditorStore } from '../stores'; import { commonKeys, @@ -10,8 +10,7 @@ import { DEFAULT_KEY_MAP, importCodeMirror, } from './common'; -import { ImagePreview } from './components'; -import { useEditorStore } from './context'; +import { ImagePreview } from './image-preview'; import { useSynchronizeOption } from './hooks'; import { CodeMirrorEditor, CommonEditorProps } from './types'; @@ -26,7 +25,7 @@ export type ResponseTooltipType = ComponentType<{ token: Token; }>; -export type UseResponseEditorArgs = CommonEditorProps & { +type ResponseEditorProps = CommonEditorProps & { /** * Customize the tooltip when hovering over properties in the response editor. */ @@ -52,15 +51,15 @@ function importCodeMirrorImports() { ); } -export function useResponseEditor({ +export function ResponseEditor({ responseTooltip, editorTheme = DEFAULT_EDITOR_THEME, keyMap = DEFAULT_KEY_MAP, -}: UseResponseEditorArgs = {}) { +}: ResponseEditorProps) { const { fetchError, validationErrors } = useSchemaStore(); const { initialResponse, responseEditor, setResponseEditor } = useEditorStore(); - const ref = useRef(null); + const ref = useRef(null!); const responseTooltipRef = useRef( responseTooltip, @@ -102,10 +101,6 @@ export function useResponseEditor({ ); const container = ref.current; - if (!container) { - return; - } - const newEditor = CodeMirror(container, { value: initialResponse, lineWrapping: true, @@ -138,5 +133,13 @@ export function useResponseEditor({ } }, [responseEditor, fetchError, validationErrors]); - return ref; + return ( +
+ ); } diff --git a/packages/graphiql-react/src/editor/tabs.ts b/packages/graphiql-react/src/editor/tabs.ts index e8e58f129b9..898fe1bfd21 100644 --- a/packages/graphiql-react/src/editor/tabs.ts +++ b/packages/graphiql-react/src/editor/tabs.ts @@ -1,8 +1,7 @@ 'use no memo'; // can't figure why it isn't optimized -import { storageStore } from '../storage'; -import { debounce } from '../utility/debounce'; -import { editorStore } from './context'; +import { storageStore, editorStore } from '../stores'; +import { debounce } from '../utility'; export type TabDefinition = { /** @@ -67,9 +66,15 @@ export function getDefaultTabState({ defaultQuery, defaultHeaders, headers, - defaultTabs, query, variables, + defaultTabs = [ + { + query: query ?? defaultQuery, + variables, + headers: headers ?? defaultHeaders, + }, + ], shouldPersistHeaders, }: { defaultQuery: string; @@ -133,15 +138,7 @@ export function getDefaultTabState({ } catch { return { activeTabIndex: 0, - tabs: ( - defaultTabs || [ - { - query: query ?? defaultQuery, - variables, - headers: headers ?? defaultHeaders, - }, - ] - ).map(createTab), + tabs: defaultTabs.map(createTab), }; } } diff --git a/packages/graphiql-react/src/editor/types.ts b/packages/graphiql-react/src/editor/types.ts index 3fca9f0d936..05b678aa504 100644 --- a/packages/graphiql-react/src/editor/types.ts +++ b/packages/graphiql-react/src/editor/types.ts @@ -27,3 +27,5 @@ export type WriteableEditorProps = CommonEditorProps & { */ readOnly?: boolean; }; + +export type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; diff --git a/packages/graphiql-react/src/editor/variable-editor.ts b/packages/graphiql-react/src/editor/variable-editor.tsx similarity index 84% rename from packages/graphiql-react/src/editor/variable-editor.ts rename to packages/graphiql-react/src/editor/variable-editor.tsx index 55ce95c2126..d93e300a5db 100644 --- a/packages/graphiql-react/src/editor/variable-editor.ts +++ b/packages/graphiql-react/src/editor/variable-editor.tsx @@ -1,14 +1,11 @@ -import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; import { useEffect, useRef } from 'react'; - -import { useExecutionStore } from '../execution'; +import { useExecutionStore, useEditorStore } from '../stores'; import { commonKeys, DEFAULT_EDITOR_THEME, DEFAULT_KEY_MAP, importCodeMirror, } from './common'; -import { useEditorStore } from './context'; import { useChangeHandler, useCompletion, @@ -17,10 +14,11 @@ import { prettifyEditors, useSynchronizeOption, } from './hooks'; -import { WriteableEditorProps } from './types'; +import { WriteableEditorProps, SchemaReference } from './types'; import { KEY_MAP } from '../constants'; +import { clsx } from 'clsx'; -export type UseVariableEditorArgs = WriteableEditorProps & { +type VariableEditorProps = WriteableEditorProps & { /** * Invoked when a reference to the GraphQL schema (type or field) is clicked * as part of the editor or one of its tooltips. @@ -32,6 +30,11 @@ export type UseVariableEditorArgs = WriteableEditorProps & { * @param value The new contents of the editor. */ onEdit?(value: string): void; + /** + * Visually hide the header editor. + * @default false + */ + isHidden?: boolean; }; // To make react-compiler happy, otherwise complains about using dynamic imports in Component @@ -43,17 +46,18 @@ function importCodeMirrorImports() { ]); } -export function useVariableEditor({ +export function VariableEditor({ editorTheme = DEFAULT_EDITOR_THEME, keyMap = DEFAULT_KEY_MAP, onClickReference, onEdit, readOnly = false, -}: UseVariableEditorArgs = {}) { + isHidden = false, +}: VariableEditorProps) { const { initialVariables, variableEditor, setVariableEditor } = useEditorStore(); const run = useExecutionStore(store => store.run); - const ref = useRef(null); + const ref = useRef(null!); useEffect(() => { let isActive = true; @@ -63,10 +67,6 @@ export function useVariableEditor({ return; } const container = ref.current; - if (!container) { - return; - } - const newEditor = CodeMirror(container, { value: initialVariables, lineNumbers: true, @@ -92,6 +92,7 @@ export function useVariableEditor({ gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], extraKeys: commonKeys, }); + function showHint() { newEditor.showHint({ completeSingle: false, container }); } @@ -130,7 +131,15 @@ export function useVariableEditor({ useKeyMap(variableEditor, KEY_MAP.prettify, prettifyEditors); useKeyMap(variableEditor, KEY_MAP.mergeFragments, mergeQuery); - return ref; + useEffect(() => { + if (!isHidden) { + variableEditor?.refresh(); + } + }, [variableEditor, isHidden]); + + return ( +
+ ); } export const STORAGE_KEY = 'variables'; diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts index 20010c6aa02..a1daa0729cb 100644 --- a/packages/graphiql-react/src/index.ts +++ b/packages/graphiql-react/src/index.ts @@ -1,22 +1,16 @@ import './style/root.css'; export { - useEditorStore, - // QueryEditor, - useQueryEditor, useOperationsEditorState, // VariableEditor, - useVariableEditor, useVariablesEditorState, // HeaderEditor, - useHeaderEditor, useHeadersEditorState, // ResponseEditor, - useResponseEditor, // copyQuery, prettifyEditors, @@ -27,11 +21,14 @@ export { useEditorState, useOptimisticState, } from './editor'; -export { useExecutionStore } from './execution'; -export { usePluginStore } from './plugin'; +export { + useEditorStore, + useExecutionStore, + usePluginStore, + useSchemaStore, + useStorage, +} from './stores'; export { GraphiQLProvider } from './provider'; -export { useSchemaStore } from './schema'; -export { useStorage } from './storage'; export { useTheme } from './theme'; export * from './utility'; @@ -44,14 +41,10 @@ export type { KeyMap, ResponseTooltipType, TabsState, - UseHeaderEditorArgs, - UseQueryEditorArgs, - UseResponseEditorArgs, - UseVariableEditorArgs, WriteableEditorProps, + SchemaReference, } from './editor'; -export type { GraphiQLPlugin } from './plugin'; -export type { SchemaContextType } from './schema'; +export type { GraphiQLPlugin } from './stores/plugin'; export type { Theme } from './theme'; export { clsx as cn } from 'clsx'; export { KEY_MAP } from './constants'; diff --git a/packages/graphiql-react/src/provider.tsx b/packages/graphiql-react/src/provider.tsx index 20290b36a8a..2ce69274e15 100644 --- a/packages/graphiql-react/src/provider.tsx +++ b/packages/graphiql-react/src/provider.tsx @@ -1,18 +1,18 @@ /* eslint sort-keys: "error" */ import type { ComponentPropsWithoutRef, FC } from 'react'; -import { EditorContextProvider } from './editor'; -import { ExecutionContextProvider } from './execution'; -import { PluginContextProvider } from './plugin'; -import { SchemaContextProvider } from './schema'; -import { StorageContextProvider } from './storage'; +import { EditorStore } from './stores/editor'; +import { ExecutionStore } from './stores/execution'; +import { PluginStore } from './stores/plugin'; +import { SchemaStore } from './stores/schema'; +import { StorageStore } from './stores/storage'; type GraphiQLProviderProps = // - ComponentPropsWithoutRef & - ComponentPropsWithoutRef & - ComponentPropsWithoutRef & - ComponentPropsWithoutRef & - ComponentPropsWithoutRef; + ComponentPropsWithoutRef & + ComponentPropsWithoutRef & + ComponentPropsWithoutRef & + ComponentPropsWithoutRef & + ComponentPropsWithoutRef; export const GraphiQLProvider: FC = ({ defaultHeaders, @@ -92,16 +92,14 @@ export const GraphiQLProvider: FC = ({ visiblePlugin, }; return ( - - - - - - {children} - - - - - + + + + + {children} + + + + ); }; diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/stores/editor.tsx similarity index 95% rename from packages/graphiql-react/src/editor/context.tsx rename to packages/graphiql-react/src/stores/editor.tsx index f4e35052f91..d1f4edacf4e 100644 --- a/packages/graphiql-react/src/editor/context.tsx +++ b/packages/graphiql-react/src/stores/editor.tsx @@ -12,10 +12,11 @@ import { VariableToType } from 'graphql-language-service'; 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'; -import { useSynchronizeValue } from './hooks'; -import { STORAGE_KEY_QUERY } from './query-editor'; +import { storageStore, useStorage } from './storage'; +import { executionStore } from './execution'; +import { STORAGE_KEY as STORAGE_KEY_HEADERS } from '../editor/header-editor'; +import { useSynchronizeValue } from '../editor/hooks'; +import { STORAGE_KEY_QUERY } from '../editor/query-editor'; import { createTab, getDefaultTabState, @@ -29,13 +30,12 @@ import { clearHeadersFromTabs, serializeTabState, STORAGE_KEY as STORAGE_KEY_TABS, -} from './tabs'; -import { CodeMirrorEditor } from './types'; -import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from './variable-editor'; +} from '../editor/tabs'; +import { CodeMirrorEditor } from '../editor/types'; +import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from '../editor/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; @@ -44,7 +44,7 @@ export type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { variableToType: VariableToType | null; }; -interface EditorStore extends TabsState { +interface EditorStoreType extends TabsState { /** * Add a new tab. */ @@ -225,8 +225,8 @@ interface EditorStore extends TabsState { onPrettifyQuery: (query: string) => MaybePromise; } -type EditorContextProviderProps = Pick< - EditorStore, +type EditorStoreProps = Pick< + EditorStoreType, | 'onTabChange' | 'onEditOperationName' | 'defaultHeaders' @@ -297,13 +297,13 @@ type EditorContextProviderProps = Pick< * typing in the editor. */ variables?: string; - onPrettifyQuery?: EditorStore['onPrettifyQuery']; + onPrettifyQuery?: EditorStoreType['onPrettifyQuery']; }; -const DEFAULT_PRETTIFY_QUERY: EditorStore['onPrettifyQuery'] = query => +const DEFAULT_PRETTIFY_QUERY: EditorStoreType['onPrettifyQuery'] = query => print(parse(query)); -export const editorStore = createStore((set, get) => ({ +export const editorStore = createStore((set, get) => ({ tabs: null!, activeTabIndex: null!, addTab() { @@ -444,7 +444,7 @@ export const editorStore = createStore((set, get) => ({ onPrettifyQuery: DEFAULT_PRETTIFY_QUERY, })); -export const EditorContextProvider: FC = ({ +export const EditorStore: FC = ({ externalFragments, onEditOperationName, defaultHeaders, diff --git a/packages/graphiql-react/src/execution.tsx b/packages/graphiql-react/src/stores/execution.tsx similarity index 96% rename from packages/graphiql-react/src/execution.tsx rename to packages/graphiql-react/src/stores/execution.tsx index fde831955ae..35fcb1d2cb0 100644 --- a/packages/graphiql-react/src/execution.tsx +++ b/packages/graphiql-react/src/stores/execution.tsx @@ -19,12 +19,12 @@ import { FC, ReactElement, ReactNode, useEffect } from 'react'; import setValue from 'set-value'; import getValue from 'get-value'; -import { getAutoCompleteLeafs } from './editor'; +import { getAutoCompleteLeafs } from '../editor'; import { createStore } from 'zustand'; -import { editorStore } from './editor/context'; -import { createBoundedUseStore } from './utility'; +import { editorStore } from './editor'; +import { createBoundedUseStore } from '../utility'; -type ExecutionContextType = { +type ExecutionStoreType = { /** * If there is currently a GraphQL request in-flight. For multipart * requests like subscriptions, this will be `true` while fetching the @@ -80,8 +80,8 @@ type ExecutionContextType = { fetcher: Fetcher; }; -type ExecutionContextProviderProps = Pick< - ExecutionContextType, +type ExecutionStoreProps = Pick< + ExecutionStoreType, 'getDefaultFieldNames' | 'fetcher' > & { children: ReactNode; @@ -92,8 +92,7 @@ type ExecutionContextProviderProps = Pick< }; export const executionStore = createStore< - ExecutionContextType & - Pick + ExecutionStoreType & Pick >((set, get) => ({ isFetching: false, subscription: null, @@ -268,7 +267,7 @@ export const executionStore = createStore< }, })); -export const ExecutionContextProvider: FC = ({ +export const ExecutionStore: FC = ({ fetcher, getDefaultFieldNames, children, diff --git a/packages/graphiql-react/src/stores/index.ts b/packages/graphiql-react/src/stores/index.ts new file mode 100644 index 00000000000..e27614c7a2c --- /dev/null +++ b/packages/graphiql-react/src/stores/index.ts @@ -0,0 +1,9 @@ +export { + editorStore, + useEditorStore, + type CodeMirrorEditorWithOperationFacts, +} from './editor'; +export { executionStore, useExecutionStore } from './execution'; +export { pluginStore, usePluginStore } from './plugin'; +export { schemaStore, useSchemaStore } from './schema'; +export { storageStore, useStorage } from './storage'; diff --git a/packages/graphiql-react/src/plugin.tsx b/packages/graphiql-react/src/stores/plugin.tsx similarity index 89% rename from packages/graphiql-react/src/plugin.tsx rename to packages/graphiql-react/src/stores/plugin.tsx index 6b4da222687..5a24e11f678 100644 --- a/packages/graphiql-react/src/plugin.tsx +++ b/packages/graphiql-react/src/stores/plugin.tsx @@ -1,7 +1,7 @@ // eslint-disable-next-line react/jsx-filename-extension -- TODO import { ComponentType, FC, ReactElement, ReactNode, useEffect } from 'react'; import { createStore } from 'zustand'; -import { createBoundedUseStore } from './utility'; +import { createBoundedUseStore } from '../utility'; export type GraphiQLPlugin = { /** @@ -20,7 +20,7 @@ export type GraphiQLPlugin = { title: string; }; -type PluginContextType = { +type PluginStoreType = { /** * A list of all current plugins, including the built-in ones (the doc * explorer and the history). @@ -48,10 +48,12 @@ type PluginContextType = { * is visible, the function will be invoked with `null`. */ onTogglePluginVisibility?(visiblePlugin: GraphiQLPlugin | null): void; + + setPlugins(plugins: GraphiQLPlugin[]): void; }; -type PluginContextProviderProps = Pick< - PluginContextType, +type PluginStoreProps = Pick< + PluginStoreType, 'referencePlugin' | 'onTogglePluginVisibility' > & { children: ReactNode; @@ -69,14 +71,14 @@ type PluginContextProviderProps = Pick< visiblePlugin?: GraphiQLPlugin | string; }; -export const pluginStore = createStore((set, get) => ({ +export const pluginStore = createStore((set, get) => ({ plugins: [], visiblePlugin: null, referencePlugin: undefined, setVisiblePlugin(plugin) { const { plugins, onTogglePluginVisibility } = get(); const byTitle = typeof plugin === 'string'; - const newVisiblePlugin: PluginContextType['visiblePlugin'] = + const newVisiblePlugin: PluginStoreType['visiblePlugin'] = (plugin && plugins.find(p => (byTitle ? p.title : p) === plugin)) || null; set(({ visiblePlugin }) => { if (newVisiblePlugin === visiblePlugin) { @@ -86,16 +88,7 @@ export const pluginStore = createStore((set, get) => ({ return { visiblePlugin: newVisiblePlugin }; }); }, -})); - -export const PluginContextProvider: FC = ({ - onTogglePluginVisibility, - children, - visiblePlugin, - plugins = [], - referencePlugin, -}) => { - useEffect(() => { + setPlugins(plugins) { const seenTitles = new Set(); const msg = 'All GraphiQL plugins must have a unique title'; for (const { title } of plugins) { @@ -107,6 +100,18 @@ export const PluginContextProvider: FC = ({ } seenTitles.add(title); } + set({ plugins }); + }, +})); + +export const PluginStore: FC = ({ + onTogglePluginVisibility, + children, + visiblePlugin, + plugins = [], + referencePlugin, +}) => { + useEffect(() => { // TODO: visiblePlugin initial data // const storedValue = storage.get(STORAGE_KEY); // const pluginForStoredValue = plugins.find( @@ -118,9 +123,8 @@ export const PluginContextProvider: FC = ({ // if (storedValue) { // storage.set(STORAGE_KEY, ''); // } - + pluginStore.getState().setPlugins(plugins); pluginStore.setState({ - plugins, onTogglePluginVisibility, referencePlugin, }); diff --git a/packages/graphiql-react/src/schema.ts b/packages/graphiql-react/src/stores/schema.ts similarity index 96% rename from packages/graphiql-react/src/schema.ts rename to packages/graphiql-react/src/stores/schema.ts index cd03053e5db..0a09ff3d455 100644 --- a/packages/graphiql-react/src/schema.ts +++ b/packages/graphiql-react/src/stores/schema.ts @@ -16,23 +16,14 @@ import { } from 'graphql'; import { Dispatch, FC, ReactElement, ReactNode, useEffect } from 'react'; import { createStore } from 'zustand'; -import { editorStore } from './editor/context'; -import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; -import { createBoundedUseStore } from './utility'; +import { editorStore } from './editor'; +import { SchemaReference } from '../editor/types'; +import { createBoundedUseStore } from '../utility'; import { executionStore, useExecutionStore } from './execution'; type MaybeGraphQLSchema = GraphQLSchema | null | undefined; -type SchemaStore = SchemaContextType & - Pick< - SchemaContextProviderProps, - | 'inputValueDeprecation' - | 'introspectionQueryName' - | 'schemaDescription' - | 'onSchemaChange' - >; - -export const schemaStore = createStore((set, get) => ({ +export const schemaStore = createStore((set, get) => ({ inputValueDeprecation: null!, introspectionQueryName: null!, schemaDescription: null!, @@ -170,12 +161,20 @@ export const schemaStore = createStore((set, get) => ({ }, })); -export type SchemaContextType = { +export interface SchemaStoreType + extends Pick< + SchemaStoreProps, + | 'inputValueDeprecation' + | 'introspectionQueryName' + | 'schemaDescription' + | 'onSchemaChange' + > { /** * Stores an error raised during introspecting or building the GraphQL schema * from the introspection result. */ fetchError: string | null; + /** * Trigger building the GraphQL schema. This might trigger an introspection * request if no schema is passed via props and if using a schema is not @@ -215,9 +214,9 @@ export type SchemaContextType = { * `false` when `schema` is provided via `props` as `GraphQLSchema | null` */ shouldIntrospect: boolean; -}; +} -type SchemaContextProviderProps = { +type SchemaStoreProps = { children: ReactNode; /** * This prop can be used to skip validating the GraphQL schema. This applies @@ -258,7 +257,7 @@ type SchemaContextProviderProps = { schema?: GraphQLSchema | IntrospectionQuery | null; } & IntrospectionArgs; -export const SchemaContextProvider: FC = ({ +export const SchemaStore: FC = ({ onSchemaChange, dangerouslyAssumeSchemaIsValid = false, children, diff --git a/packages/graphiql-react/src/storage.tsx b/packages/graphiql-react/src/stores/storage.tsx similarity index 77% rename from packages/graphiql-react/src/storage.tsx rename to packages/graphiql-react/src/stores/storage.tsx index 6defafdaf50..583cb09e57e 100644 --- a/packages/graphiql-react/src/storage.tsx +++ b/packages/graphiql-react/src/stores/storage.tsx @@ -2,13 +2,13 @@ import { Storage, StorageAPI } from '@graphiql/toolkit'; import { FC, ReactElement, ReactNode, useEffect } from 'react'; import { createStore } from 'zustand'; -import { createBoundedUseStore } from './utility'; +import { createBoundedUseStore } from '../utility'; -type StorageContextType = { +type StorageStoreType = { storage: StorageAPI; }; -type StorageContextProviderProps = { +type StorageStoreProps = { children: ReactNode; /** * Provide a custom storage API. @@ -19,14 +19,11 @@ type StorageContextProviderProps = { storage?: Storage; }; -export const storageStore = createStore(() => ({ +export const storageStore = createStore(() => ({ storage: null!, })); -export const StorageContextProvider: FC = ({ - storage, - children, -}) => { +export const StorageStore: FC = ({ storage, children }) => { const isMounted = useStorageStore(store => Boolean(store.storage)); useEffect(() => { diff --git a/packages/graphiql-react/src/editor/style/auto-insertion.css b/packages/graphiql-react/src/style/auto-insertion.css similarity index 100% rename from packages/graphiql-react/src/editor/style/auto-insertion.css rename to packages/graphiql-react/src/style/auto-insertion.css diff --git a/packages/graphiql-react/src/editor/style/codemirror.css b/packages/graphiql-react/src/style/codemirror.css similarity index 99% rename from packages/graphiql-react/src/editor/style/codemirror.css rename to packages/graphiql-react/src/style/codemirror.css index e1911ab2711..eb2066ef8eb 100644 --- a/packages/graphiql-react/src/editor/style/codemirror.css +++ b/packages/graphiql-react/src/style/codemirror.css @@ -168,7 +168,7 @@ .graphiql-container .cm-searching { background-color: hsla(var(--color-warning), var(--alpha-background-light)); /** - * When cycling through search results, CodeMirror overlays the current + * When cycling through search results, CodeMirror overlays the current * selection with another element that has the .CodeMirror-selected class * applied. This adds another background color (see above), but this extra * box does not quite match the height of this element. To match them up we diff --git a/packages/graphiql-react/src/editor/style/editor.css b/packages/graphiql-react/src/style/editor.css similarity index 100% rename from packages/graphiql-react/src/editor/style/editor.css rename to packages/graphiql-react/src/style/editor.css diff --git a/packages/graphiql-react/src/editor/style/fold.css b/packages/graphiql-react/src/style/fold.css similarity index 100% rename from packages/graphiql-react/src/editor/style/fold.css rename to packages/graphiql-react/src/style/fold.css diff --git a/packages/graphiql-react/src/editor/style/hint.css b/packages/graphiql-react/src/style/hint.css similarity index 100% rename from packages/graphiql-react/src/editor/style/hint.css rename to packages/graphiql-react/src/style/hint.css diff --git a/packages/graphiql-react/src/editor/style/info.css b/packages/graphiql-react/src/style/info.css similarity index 100% rename from packages/graphiql-react/src/editor/style/info.css rename to packages/graphiql-react/src/style/info.css diff --git a/packages/graphiql-react/src/editor/style/jump.css b/packages/graphiql-react/src/style/jump.css similarity index 100% rename from packages/graphiql-react/src/editor/style/jump.css rename to packages/graphiql-react/src/style/jump.css diff --git a/packages/graphiql-react/src/editor/style/lint.css b/packages/graphiql-react/src/style/lint.css similarity index 99% rename from packages/graphiql-react/src/editor/style/lint.css rename to packages/graphiql-react/src/style/lint.css index 7dc8a064bfc..5aaa1473112 100644 --- a/packages/graphiql-react/src/editor/style/lint.css +++ b/packages/graphiql-react/src/style/lint.css @@ -5,7 +5,7 @@ .CodeMirror-lint-mark-warning { background-repeat: repeat-x; /** - * The following two are very specific to the font size, so we use + * The following two are very specific to the font size, so we use * "magic values" instead of variables. */ background-size: 10px 3px; diff --git a/packages/graphiql-react/src/style/root.css b/packages/graphiql-react/src/style/root.css index 8c8dbcfa09f..ff1828fb554 100644 --- a/packages/graphiql-react/src/style/root.css +++ b/packages/graphiql-react/src/style/root.css @@ -1,3 +1,13 @@ +@import 'codemirror.css'; +@import 'fold.css'; +@import 'editor.css'; + +@import 'lint.css'; +@import 'hint.css'; +@import 'info.css'; +@import 'jump.css'; +@import 'auto-insertion.css'; + /* a very simple box-model reset, intentionally does not include pseudo elements */ .graphiql-container * { box-sizing: border-box; diff --git a/packages/graphiql-react/src/theme.ts b/packages/graphiql-react/src/theme.ts index 6e3cda415bb..f63efaa8acb 100644 --- a/packages/graphiql-react/src/theme.ts +++ b/packages/graphiql-react/src/theme.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useStorage } from './storage'; +import { useStorage } from './stores'; /** * The value `null` semantically means that the user does not explicitly choose diff --git a/packages/graphiql-react/src/toolbar/execute.tsx b/packages/graphiql-react/src/toolbar/execute.tsx index 9cd9c2bb87a..275e24e64fc 100644 --- a/packages/graphiql-react/src/toolbar/execute.tsx +++ b/packages/graphiql-react/src/toolbar/execute.tsx @@ -1,6 +1,5 @@ import { FC } from 'react'; -import { useEditorStore } from '../editor'; -import { useExecutionStore } from '../execution'; +import { useEditorStore, useExecutionStore } from '../stores'; import { PlayIcon, StopIcon } from '../icons'; import { DropdownMenu, Tooltip } from '../ui'; import { KEY_MAP } from '../constants'; diff --git a/packages/graphiql-react/src/utility/resize.ts b/packages/graphiql-react/src/utility/resize.ts index 97271d59ba8..0dacb8d0cb4 100644 --- a/packages/graphiql-react/src/utility/resize.ts +++ b/packages/graphiql-react/src/utility/resize.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { useStorage } from '../storage'; +import { useStorage } from '../stores'; import { debounce } from './debounce'; type ResizableElement = 'first' | 'second'; diff --git a/packages/graphiql-react/src/editor/__tests__/whitespace.spec.ts b/packages/graphiql-react/src/utility/whitespace.spec.ts similarity index 76% rename from packages/graphiql-react/src/editor/__tests__/whitespace.spec.ts rename to packages/graphiql-react/src/utility/whitespace.spec.ts index 4a44b6aab21..41ebc49f553 100644 --- a/packages/graphiql-react/src/editor/__tests__/whitespace.spec.ts +++ b/packages/graphiql-react/src/utility/whitespace.spec.ts @@ -1,4 +1,4 @@ -import { invalidCharacters, normalizeWhitespace } from '../whitespace'; +import { invalidCharacters, normalizeWhitespace } from './whitespace'; describe('normalizeWhitespace', () => { it('removes unicode characters', () => { diff --git a/packages/graphiql-react/src/editor/whitespace.ts b/packages/graphiql-react/src/utility/whitespace.ts similarity index 100% rename from packages/graphiql-react/src/editor/whitespace.ts rename to packages/graphiql-react/src/utility/whitespace.ts diff --git a/packages/graphiql/README.md b/packages/graphiql/README.md index d851dfee0c9..73f60e89d81 100644 --- a/packages/graphiql/README.md +++ b/packages/graphiql/README.md @@ -202,4 +202,4 @@ has to be loaded for the theme prop to work. You can also create your own theme in CSS. As a reference, the default `graphiql` theme definition can be found -[here](../graphiql-react/src/editor/style/codemirror.css). +[here](../graphiql-react/src/style/codemirror.css). diff --git a/packages/graphiql/src/GraphiQL.tsx b/packages/graphiql/src/GraphiQL.tsx index 806dd88db46..78fc0185cea 100644 --- a/packages/graphiql/src/GraphiQL.tsx +++ b/packages/graphiql/src/GraphiQL.tsx @@ -10,21 +10,18 @@ import type { FC, ComponentPropsWithoutRef, } from 'react'; -import { Fragment, useState, useEffect, Children } from 'react'; +import { useState, useEffect, Children } from 'react'; import { Button, ButtonGroup, ChevronDownIcon, ChevronUpIcon, - CopyIcon, Dialog, ExecuteButton, GraphiQLProvider, HeaderEditor, KeyboardShortcutIcon, - MergeIcon, PlusIcon, - PrettifyIcon, QueryEditor, ReloadIcon, ResponseEditor, @@ -33,49 +30,36 @@ import { Tab, Tabs, Theme, - ToolbarButton, Tooltip, UnStyledButton, - copyQuery, useDragResize, useEditorStore, useExecutionStore, - UseHeaderEditorArgs, - mergeQuery, usePluginStore, - prettifyEditors, - UseQueryEditorArgs, - UseResponseEditorArgs, useSchemaStore, useStorage, useTheme, - UseVariableEditorArgs, VariableEditor, WriteableEditorProps, - isMacOs, cn, - KEY_MAP, } from '@graphiql/react'; +import { HistoryStore, HISTORY_PLUGIN } from '@graphiql/plugin-history'; import { - HistoryContextProvider, - HISTORY_PLUGIN, -} from '@graphiql/plugin-history'; -import { - DocExplorerContextProvider, + DocExplorerStore, DOC_EXPLORER_PLUGIN, } from '@graphiql/plugin-doc-explorer'; +import { GraphiQLLogo, GraphiQLToolbar, GraphiQLFooter, ShortKeys } from './ui'; /** * API docs for this live here: * * https://graphiql-test.netlify.app/typedoc/modules/graphiql.html#graphiqlprops */ -export type GraphiQLProps = - // - Omit, 'children'> & - Omit, 'children'> & - // `children` prop should be optional - GraphiQLInterfaceProps; +export interface GraphiQLProps + // `children` prop should be optional + extends GraphiQLInterfaceProps, + Omit, 'children'>, + Omit, 'children'> {} /** * The top-level React component for GraphiQL, intended to encompass the entire @@ -143,11 +127,15 @@ const GraphiQL_: FC = ({ referencePlugin={referencePlugin} {...props} > - - - {children} - - + + + + + {children} + + + + ); }; @@ -156,52 +144,59 @@ type AddSuffix, Suffix extends string> = { [Key in keyof Obj as `${string & Key}${Suffix}`]: Obj[Key]; }; -export type GraphiQLInterfaceProps = WriteableEditorProps & - AddSuffix, 'Query'> & - AddSuffix, 'Variables'> & - AddSuffix, 'Headers'> & - Pick & { - children?: ReactNode; - /** - * Set the default state for the editor tools. - * - `false` hides the editor tools - * - `true` shows the editor tools - * - `'variables'` specifically shows the variables editor - * - `'headers'` specifically shows the headers editor - * By default, the editor tools are initially shown when at least one of the - * editors has contents. - */ - defaultEditorToolsVisibility?: boolean | 'variables' | 'headers'; - /** - * Toggle if the headers' editor should be shown inside the editor tools. - * @default true - */ - isHeadersEditorEnabled?: boolean; - /** - * Indicates if settings for persisting headers should appear in the - * settings modal. - */ - showPersistHeadersSettings?: boolean; - defaultTheme?: Theme; - /** - * `forcedTheme` allows enforcement of a specific theme for GraphiQL. - * This is useful when you want to make sure that GraphiQL is always - * rendered with a specific theme. - */ - forcedTheme?: (typeof THEMES)[number]; - /** - * Additional class names which will be appended to the container element. - */ - className?: string; - /** - * When the user clicks a close tab button, this function is invoked with - * the index of the tab that is about to be closed. It can return a promise - * that should resolve to `true` (meaning the tab may be closed) or `false` - * (meaning the tab may not be closed). - * @param index The index of the tab that should be closed. - */ - confirmCloseTab?(index: number): Promise | boolean; - }; +type QueryEditorProps = ComponentPropsWithoutRef; +type VariableEditorProps = ComponentPropsWithoutRef; +type HeaderEditorProps = ComponentPropsWithoutRef; +type ResponseEditorProps = ComponentPropsWithoutRef; + +export interface GraphiQLInterfaceProps + extends WriteableEditorProps, + AddSuffix, 'Query'>, + AddSuffix, 'Variables'>, + AddSuffix, 'Headers'>, + Pick { + children?: ReactNode; + /** + * Set the default state for the editor tools. + * - `false` hides the editor tools + * - `true` shows the editor tools + * - `'variables'` specifically shows the variables editor + * - `'headers'` specifically shows the headers editor + * By default, the editor tools are initially shown when at least one of the + * editors has contents. + */ + defaultEditorToolsVisibility?: boolean | 'variables' | 'headers'; + /** + * Toggle if the headers' editor should be shown inside the editor tools. + * @default true + */ + isHeadersEditorEnabled?: boolean; + /** + * Indicates if settings for persisting headers should appear in the + * settings modal. + */ + showPersistHeadersSettings?: boolean; + defaultTheme?: Theme; + /** + * `forcedTheme` allows enforcement of a specific theme for GraphiQL. + * This is useful when you want to make sure that GraphiQL is always + * rendered with a specific theme. + */ + forcedTheme?: (typeof THEMES)[number]; + /** + * Additional class names which will be appended to the container element. + */ + className?: string; + + /** + * When the user clicks a close tab button, this function is invoked with + * the index of the tab that is about to be closed. It can return a promise + * that should resolve to `true` (meaning the tab may be closed) or `false` + * (meaning the tab may not be closed). + * @param index The index of the tab that should be closed. + */ + confirmCloseTab?(index: number): Promise | boolean; +} const THEMES = ['light', 'dark', 'system'] as const; @@ -273,20 +268,15 @@ export const GraphiQLInterface: FC = ({ const editorToolsResize = useDragResize({ defaultSizeRelation: 3, direction: 'vertical', - initiallyHidden: (() => { - if ( - defaultEditorToolsVisibility === 'variables' || - defaultEditorToolsVisibility === 'headers' - ) { + initiallyHidden: ((d: typeof defaultEditorToolsVisibility) => { + if (d === 'variables' || d === 'headers') { return; } - - if (typeof defaultEditorToolsVisibility === 'boolean') { - return defaultEditorToolsVisibility ? undefined : 'second'; + if (typeof d === 'boolean') { + return d ? undefined : 'second'; } - return initialVariables || initialHeaders ? undefined : 'second'; - })(), + })(defaultEditorToolsVisibility), sizeThresholdSecond: 60, storageKey: 'secondaryEditorFlex', }); @@ -428,512 +418,362 @@ export const GraphiQLInterface: FC = ({ changeTab(index); }; - return ( - -
-
- {plugins.map((plugin, index) => { - const isVisible = plugin === visiblePlugin; - const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}`; - return ( - - - - - ); - })} - - - - - - - - - + const sidebar = ( +
+ {plugins.map((plugin, index) => { + const isVisible = plugin === visiblePlugin; + const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}`; + return ( + - + ); + })} + + + + + + + + + + + + + +
+ + Short Keys + +
-
-
- {PluginContent ? : null} -
- {visiblePlugin && ( -
- )} -
-
- - {tabs.map((tab, index, arr) => ( - - - {tab.title} - - {arr.length > 1 && } - - ))} - - - - - - {logo} -
-
-
-
- -
- - {toolbar} -
-
- -
- - Variables - - {isHeadersEditorEnabled && ( - - Headers - - )} - - - - {editorToolsResize.hiddenElement === 'second' ? ( - - -
- -
- - {isHeadersEditorEnabled && ( - - )} -
-
- -
- -
- {isExecutionFetching && } - - {footer} -
-
-
+
+
- -
- - Short Keys - - -
+
+ +
+ + Settings + + +
+ {showPersistHeadersSettings ? (
- -
-
- -
- - Settings - - -
- {showPersistHeadersSettings ? ( -
-
-
- Persist headers -
-
- Save headers upon reloading.{' '} - - Only enable if you trust this device. - -
+
+
+ Persist headers
- - - - -
- ) : null} - {!forcedTheme && ( -
-
-
Theme
-
- Adjust how the interface appears. -
+
+ Save headers upon reloading.{' '} + + Only enable if you trust this device. +
- - - - -
- )} + + + + +
+ ) : null} + {!forcedTheme && (
-
Clear storage
+
Theme
- Remove all locally stored data and start fresh. + Adjust how the interface appears.
- + + + + +
-
-
- + )} +
+
+
Clear storage
+
+ Remove all locally stored data and start fresh. +
+
+ +
+
+
); -}; - -function withMacOS(key: string) { - return isMacOs ? key.replace('Ctrl', '⌘') : key; -} -const SHORT_KEYS = Object.entries({ - 'Search in editor': withMacOS(KEY_MAP.searchInEditor[0]), - 'Search in documentation': withMacOS(KEY_MAP.searchInDocs[0]), - 'Execute query': withMacOS(KEY_MAP.runQuery[0]), - 'Prettify editors': KEY_MAP.prettify[0], - 'Merge fragments definitions into operation definition': - KEY_MAP.mergeFragments[0], - 'Copy query': KEY_MAP.copyQuery[0], - 'Re-fetch schema using introspection': KEY_MAP.refetchSchema[0], -}); + const editorToolsText = `${editorToolsResize.hiddenElement === 'second' ? 'Show' : 'Hide'} editor tools`; + + const EditorToolsIcon = + editorToolsResize.hiddenElement === 'second' + ? ChevronUpIcon + : ChevronDownIcon; + + const editors = ( +
+
+ +
+ + {toolbar} +
+
+ +
+ + Variables + + {isHeadersEditorEnabled && ( + + Headers + + )} + + + + + +
-interface ShortKeysProps { - /** @default 'sublime' */ - keyMap?: string; -} +
+ + {isHeadersEditorEnabled && ( + + )} +
+
+ ); -const ShortKeys: FC = ({ keyMap = 'sublime' }) => { return ( -
- - - - - - - - - {SHORT_KEYS.map(([title, keys]) => ( - - - - - ))} - -
Short KeyFunction
- {keys.split('-').map((key, index, array) => ( - - {key} - {index !== array.length - 1 && ' + '} - - ))} - {title}
-

- The editors use{' '} - + {sidebar} +

+
- CodeMirror Key Maps - {' '} - that add more short keys. This instance of GraphiQL uses{' '} - {keyMap}. -

+ {PluginContent ? : null} +
+ {visiblePlugin && ( +
+ )} +
+
+ + {tabs.map((tab, index, arr) => ( + + + {tab.title} + + {arr.length > 1 && } + + ))} + + + + + + {logo} +
+
+ {editors} +
+
+ {isExecutionFetching && } + + {footer} +
+
+
+
); }; -const defaultGraphiqlLogo = ( - - Graph - i - QL - -); - -// Configure the UI by providing this Component as a child of GraphiQL. -const GraphiQLLogo: FC<{ children?: ReactNode }> = ({ - children = defaultGraphiqlLogo, -}) => { - return
{children}
; -}; - -const DefaultToolbarRenderProps: FC<{ - prettify: ReactNode; - copy: ReactNode; - merge: ReactNode; -}> = ({ prettify, copy, merge }) => ( - <> - {prettify} - {merge} - {copy} - -); - -// Configure the UI by providing this Component as a child of GraphiQL. -const GraphiQLToolbar: FC<{ - children?: typeof DefaultToolbarRenderProps; -}> = ({ children = DefaultToolbarRenderProps }) => { - if (typeof children !== 'function') { - throw new TypeError( - 'The `GraphiQL.Toolbar` component requires a render prop function as its child.', - ); - } - - const prettify = ( - - - ); - - const merge = ( - - - ); - - const copy = ( - - - ); - - return children({ prettify, copy, merge }); -}; - -// Configure the UI by providing this Component as a child of GraphiQL. -const GraphiQLFooter: FC<{ children: ReactNode }> = ({ children }) => { - return
{children}
; -}; - function getChildComponentType(child: ReactNode) { if ( child && diff --git a/packages/graphiql/src/ui/footer.tsx b/packages/graphiql/src/ui/footer.tsx new file mode 100644 index 00000000000..5ab9db90063 --- /dev/null +++ b/packages/graphiql/src/ui/footer.tsx @@ -0,0 +1,6 @@ +import { FC, ReactNode } from 'react'; + +// Configure the UI by providing this Component as a child of GraphiQL. +export const GraphiQLFooter: FC<{ children: ReactNode }> = ({ children }) => { + return
{children}
; +}; diff --git a/packages/graphiql/src/ui/index.ts b/packages/graphiql/src/ui/index.ts new file mode 100644 index 00000000000..10b27973b62 --- /dev/null +++ b/packages/graphiql/src/ui/index.ts @@ -0,0 +1,4 @@ +export { GraphiQLFooter } from './footer'; +export { GraphiQLLogo } from './logo'; +export { ShortKeys } from './short-keys'; +export { GraphiQLToolbar } from './toolbar'; diff --git a/packages/graphiql/src/ui/logo.tsx b/packages/graphiql/src/ui/logo.tsx new file mode 100644 index 00000000000..dbee3f4c7e2 --- /dev/null +++ b/packages/graphiql/src/ui/logo.tsx @@ -0,0 +1,21 @@ +import type { FC, ReactNode } from 'react'; + +const defaultGraphiqlLogo = ( + + Graph + i + QL + +); + +// Configure the UI by providing this Component as a child of GraphiQL. +export const GraphiQLLogo: FC<{ children?: ReactNode }> = ({ + children = defaultGraphiqlLogo, +}) => { + return
{children}
; +}; diff --git a/packages/graphiql/src/ui/short-keys.tsx b/packages/graphiql/src/ui/short-keys.tsx new file mode 100644 index 00000000000..32f8cde7e68 --- /dev/null +++ b/packages/graphiql/src/ui/short-keys.tsx @@ -0,0 +1,64 @@ +import { FC, Fragment } from 'react'; +import { isMacOs, KEY_MAP } from '@graphiql/react'; + +function withMacOS(key: string) { + return isMacOs ? key.replace('Ctrl', '⌘') : key; +} + +const SHORT_KEYS = Object.entries({ + 'Search in editor': withMacOS(KEY_MAP.searchInEditor[0]), + 'Search in documentation': withMacOS(KEY_MAP.searchInDocs[0]), + 'Execute query': withMacOS(KEY_MAP.runQuery[0]), + 'Prettify editors': KEY_MAP.prettify[0], + 'Merge fragments definitions into operation definition': + KEY_MAP.mergeFragments[0], + 'Copy query': KEY_MAP.copyQuery[0], + 'Re-fetch schema using introspection': KEY_MAP.refetchSchema[0], +}); + +interface ShortKeysProps { + /** @default 'sublime' */ + keyMap?: string; +} + +export const ShortKeys: FC = ({ keyMap = 'sublime' }) => { + return ( +
+ + + + + + + + + {SHORT_KEYS.map(([title, keys]) => ( + + + + + ))} + +
Short KeyFunction
+ {keys.split('-').map((key, index, array) => ( + + {key} + {index !== array.length - 1 && ' + '} + + ))} + {title}
+

+ The editors use{' '} + + CodeMirror Key Maps + {' '} + that add more short keys. This instance of GraphiQL uses{' '} + {keyMap}. +

+
+ ); +}; diff --git a/packages/graphiql/src/ui/toolbar.tsx b/packages/graphiql/src/ui/toolbar.tsx new file mode 100644 index 00000000000..c35c5f4e8a0 --- /dev/null +++ b/packages/graphiql/src/ui/toolbar.tsx @@ -0,0 +1,65 @@ +import type { FC, ReactNode } from 'react'; +import { + CopyIcon, + copyQuery, + KEY_MAP, + MergeIcon, + mergeQuery, + prettifyEditors, + PrettifyIcon, + ToolbarButton, +} from '@graphiql/react'; + +const DefaultToolbarRenderProps: FC<{ + prettify: ReactNode; + copy: ReactNode; + merge: ReactNode; +}> = ({ prettify, copy, merge }) => ( + <> + {prettify} + {merge} + {copy} + +); + +/** + * Configure the UI by providing this Component as a child of GraphiQL. + */ +export const GraphiQLToolbar: FC<{ + children?: typeof DefaultToolbarRenderProps; +}> = ({ children = DefaultToolbarRenderProps }) => { + if (typeof children !== 'function') { + throw new TypeError( + 'The `GraphiQL.Toolbar` component requires a render prop function as its child.', + ); + } + + const prettify = ( + + + ); + + const merge = ( + + + ); + + const copy = ( + + + ); + + return children({ prettify, copy, merge }); +};