diff --git a/.changeset/beige-months-care.md b/.changeset/beige-months-care.md new file mode 100644 index 00000000000..a8d94cd063c --- /dev/null +++ b/.changeset/beige-months-care.md @@ -0,0 +1,14 @@ +--- +'@graphiql/plugin-doc-explorer': minor +'@graphiql/plugin-explorer': major +'@graphiql/plugin-history': minor +'@graphiql/react': minor +'graphiql': major +--- + +- allow multiple independent instances of GraphiQL on the same page +- store `onClickReference` in query editor in React `ref` +- remove `onClickReference` from variable editor +- fix shortcut text per OS for run query in execute query button's tooltip and in default query +- allow override all default GraphiQL plugins +- adjust operation argument color to be purple from GraphiQL v2 on dark/light theme diff --git a/.eslintrc.js b/.eslintrc.js index 490ba4abb5b..ccf48816e06 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -32,8 +32,11 @@ module.exports = { 'functions/*', 'packages/vscode-graphql-syntax/tests/__fixtures__/*', // symlinks + 'packages/graphiql-react/__mocks__/monaco-editor.ts', 'packages/graphiql-plugin-doc-explorer/__mocks__/zustand.ts', + 'packages/graphiql-plugin-doc-explorer/__mocks__/monaco-editor.ts', 'packages/graphiql-plugin-history/__mocks__/zustand.ts', + 'packages/graphiql-plugin-history/__mocks__/monaco-editor.ts', ], overrides: [ { @@ -147,6 +150,10 @@ module.exports = { property: 'getComputedStyle', message: 'Use `getComputedStyle` instead', }, + { + object: 'self', + message: 'Use `globalThis` instead', + }, ], 'no-return-assign': 'error', 'no-return-await': 'error', @@ -367,6 +374,7 @@ module.exports = { 'unicorn/no-length-as-slice-end': 'error', 'unicorn/prefer-string-replace-all': 'error', 'unicorn/prefer-array-some': 'error', + // '@typescript-eslint/prefer-for-of': 'error', TODO 'unicorn/no-hex-escape': 'off', // TODO: enable // doesn't catch a lot of cases; we use ESLint builtin `no-restricted-syntax` to forbid `.keyCode` 'unicorn/prefer-keyboard-event-key': 'off', @@ -379,6 +387,13 @@ module.exports = { 'import-x/no-named-as-default-member': 'off', }, }, + { + files: ['packages/{monaco-graphql,graphiql*}/**/*.{ts,tsx,mts,cts}'], + excludedFiles: ['packages/graphiql-toolkit/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-unnecessary-condition': 'error', + }, + }, { // Rules that requires type information files: ['**/*.{ts,tsx,mts,cts}'], @@ -515,9 +530,7 @@ module.exports = { rules: { '@typescript-eslint/no-restricted-imports': [ 'error', - ...RESTRICTED_IMPORTS - // TODO: enable when monaco-editor will be migrated over codemirror - .filter(({ name }) => name !== 'monaco-editor'), + ...RESTRICTED_IMPORTS, { name: 'react', importNames: ['memo', 'useCallback', 'useMemo'], @@ -572,6 +585,7 @@ module.exports = { 'react-hooks/rules-of-hooks': 'off', 'sonarjs/no-dead-store': 'off', '@typescript-eslint/no-restricted-imports': 'off', + '@typescript-eslint/no-unnecessary-condition': 'off', }, }, ], diff --git a/examples/graphiql-nextjs/package.json b/examples/graphiql-nextjs/package.json index 59b74707d20..9c1ab8db6d5 100644 --- a/examples/graphiql-nextjs/package.json +++ b/examples/graphiql-nextjs/package.json @@ -4,14 +4,14 @@ "private": true, "scripts": { "types:check": "tsc --noEmit", - "dev": "next --turbopack", - "build": "next build --turbopack", + "dev": "next", + "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "graphiql": "^5.0.0-rc.0", - "next": "15.3.3", + "next": "15.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/monaco-graphql-nextjs/package.json b/examples/monaco-graphql-nextjs/package.json index 45aa9a8cc54..68d0a642eea 100644 --- a/examples/monaco-graphql-nextjs/package.json +++ b/examples/monaco-graphql-nextjs/package.json @@ -5,17 +5,17 @@ "type": "module", "scripts": { "types:check": "tsc --noEmit", - "dev": "next --turbopack", - "build": "next build --turbopack", + "dev": "next", + "build": "next build", "start": "next start" }, "dependencies": { "@graphiql/toolkit": "^0.11.3", "graphql": "^16.9.0", "jsonc-parser": "^3.2.0", - "monaco-editor": "^0.39.0", + "monaco-editor": "^0.52.2", "monaco-graphql": "^1.7.0", - "next": "15.3.3", + "next": "15.4.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/examples/monaco-graphql-webpack/package.json b/examples/monaco-graphql-webpack/package.json index f6a2337f2fd..c09770cc097 100644 --- a/examples/monaco-graphql-webpack/package.json +++ b/examples/monaco-graphql-webpack/package.json @@ -14,7 +14,7 @@ "graphql-language-service": "^5.4.0", "json-schema": "^0.4.0", "jsonc-parser": "^3.2.0", - "monaco-editor": "^0.47.0", + "monaco-editor": "^0.52.2", "monaco-graphql": "^1.7.0", "prettier": "3.3.2" }, diff --git a/package.json b/package.json index 1f9d3bb5ff0..cca4802b942 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "examples/monaco-graphql-nextjs", "examples/monaco-graphql-react-vite", "examples/graphiql-vite", - "examples/graphiql-nextjs", "examples/graphiql-webpack" ] }, @@ -128,7 +127,6 @@ "wsrun": "^5.2.4" }, "resolutions": { - "monaco-editor": "0.47.0", "@babel/traverse": "^7.23.2", "vscode-languageserver-types": "3.17.3", "markdown-it": "14.1.0", diff --git a/packages/graphiql-plugin-doc-explorer/__mocks__/monaco-editor.ts b/packages/graphiql-plugin-doc-explorer/__mocks__/monaco-editor.ts new file mode 120000 index 00000000000..b1964294fb0 --- /dev/null +++ b/packages/graphiql-plugin-doc-explorer/__mocks__/monaco-editor.ts @@ -0,0 +1 @@ +../../graphiql/__mocks__/monaco-editor.ts \ No newline at end of file diff --git a/packages/graphiql-plugin-doc-explorer/setup-files.ts b/packages/graphiql-plugin-doc-explorer/setup-files.ts index 3079d90df15..da20690a55f 100644 --- a/packages/graphiql-plugin-doc-explorer/setup-files.ts +++ b/packages/graphiql-plugin-doc-explorer/setup-files.ts @@ -2,36 +2,6 @@ import '@testing-library/jest-dom'; -vi.mock('zustand'); // to make it works like Jest (auto-mocking) - -/** - * Fixes TypeError: document.queryCommandSupported is not a function - */ -if (!navigator.clipboard) { - Object.defineProperty(navigator, 'clipboard', { - writable: false, - value: { - write: async () => null, - }, - }); -} - -/** - * Fixes TypeError: mainWindow.matchMedia is not a function - * @see https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom - */ -if (!window.matchMedia) { - Object.defineProperty(window, 'matchMedia', { - writable: false, - value: vi.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), // deprecated - removeListener: vi.fn(), // deprecated - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), - }); -} +// to make it works like Jest (auto-mocking) +vi.mock('zustand'); +vi.mock('monaco-editor'); 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 71620efc856..f8854b0e597 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 @@ -1,4 +1,6 @@ -import { act, render } from '@testing-library/react'; +import { Mock } from 'vitest'; +import { useGraphiQL as $useGraphiQL } from '@graphiql/react'; +import { render } from '@testing-library/react'; import { GraphQLInt, GraphQLObjectType, GraphQLSchema } from 'graphql'; import { FC, useEffect } from 'react'; import { @@ -7,7 +9,17 @@ import { useDocExplorerActions, } from '../../context'; import { DocExplorer } from '../doc-explorer'; -import { schemaStore } from '../../../../graphiql-react/dist/stores/schema'; + +const useGraphiQL = $useGraphiQL as Mock; + +vi.mock('@graphiql/react', async () => { + const originalModule = + await vi.importActual('@graphiql/react'); + return { + ...originalModule, + useGraphiQL: vi.fn(), + }; +}); function makeSchema(fieldName = 'field') { return new GraphQLSchema({ @@ -29,15 +41,16 @@ function makeSchema(fieldName = 'field') { } const defaultSchemaContext = { - ...schemaStore.getInitialState(), - async introspect() {}, + fetchError: null, + introspect() {}, + isFetching: false, schema: makeSchema(), + validationErrors: [], }; const withErrorSchemaContext = { - ...schemaStore.getInitialState(), + ...defaultSchemaContext, fetchError: 'Error fetching schema', - async introspect() {}, schema: new GraphQLSchema({ description: 'GraphQL Schema for testing' }), }; @@ -50,21 +63,29 @@ const DocExplorerWithContext: FC = () => { }; describe('DocExplorer', () => { + beforeEach(() => { + vi.resetModules(); + }); + it('renders spinner when the schema is loading', () => { - schemaStore.setState({ isFetching: true }); + useGraphiQL.mockImplementation(cb => + cb({ ...defaultSchemaContext, isFetching: true }), + ); const { container } = render(); const spinner = container.querySelectorAll('.graphiql-spinner'); expect(spinner).toHaveLength(1); }); it('renders with null schema', () => { - schemaStore.setState({ ...defaultSchemaContext, schema: null }); + useGraphiQL.mockImplementation(cb => + cb({ ...defaultSchemaContext, schema: null }), + ); const { container } = render(); const error = container.querySelectorAll('.graphiql-doc-explorer-error'); expect(error).toHaveLength(1); expect(error[0]).toHaveTextContent('No GraphQL schema available'); }); it('renders with schema', () => { - schemaStore.setState(defaultSchemaContext); + useGraphiQL.mockImplementation(cb => cb(defaultSchemaContext)); const { container } = render(); const error = container.querySelectorAll('.graphiql-doc-explorer-error'); expect(error).toHaveLength(0); @@ -73,14 +94,11 @@ describe('DocExplorer', () => { ).toHaveTextContent('GraphQL Schema for testing'); }); it('renders correctly with schema error', () => { - schemaStore.setState(withErrorSchemaContext); + useGraphiQL.mockImplementation(cb => cb(withErrorSchemaContext)); const { rerender, container } = render(); const error = container.querySelector('.graphiql-doc-explorer-error'); expect(error).toHaveTextContent('Error fetching schema'); - - act(() => { - schemaStore.setState(defaultSchemaContext); - }); + useGraphiQL.mockImplementation(cb => cb(defaultSchemaContext)); rerender(); const errors = container.querySelectorAll('.graphiql-doc-explorer-error'); expect(errors).toHaveLength(0); @@ -104,10 +122,9 @@ describe('DocExplorer', () => { }; // Initial render, set initial state - schemaStore.setState({ - ...defaultSchemaContext, - schema: initialSchema, - }); + useGraphiQL.mockImplementation(cb => + cb({ ...defaultSchemaContext, schema: initialSchema }), + ); const { container, rerender } = render( @@ -115,34 +132,28 @@ describe('DocExplorer', () => { ); // First proper render of doc explorer - act(() => { - schemaStore.setState({ - ...defaultSchemaContext, - schema: initialSchema, - }); - }); rerender( , ); - const [title] = container.querySelectorAll('.graphiql-doc-explorer-title'); + const title = container.querySelector('.graphiql-doc-explorer-title')!; expect(title.textContent).toEqual('field'); // Second render of doc explorer, this time with a new schema, with _same_ field name - act(() => { - schemaStore.setState({ + useGraphiQL.mockImplementation(cb => + cb({ ...defaultSchemaContext, schema: makeSchema(), // <<< New, but equivalent, schema - }); - }); + }), + ); rerender( , ); - const [title2] = container.querySelectorAll('.graphiql-doc-explorer-title'); + const title2 = container.querySelector('.graphiql-doc-explorer-title')!; // Because `Query.field` still exists in the new schema, we can still render it expect(title2.textContent).toEqual('field'); }); @@ -166,10 +177,9 @@ describe('DocExplorer', () => { }; // Initial render, set initial state - schemaStore.setState({ - ...defaultSchemaContext, - schema: initialSchema, - }); + useGraphiQL.mockImplementation(cb => + cb({ ...defaultSchemaContext, schema: initialSchema }), + ); const { container, rerender } = render( @@ -177,12 +187,6 @@ describe('DocExplorer', () => { ); // First proper render of doc explorer - act(() => { - schemaStore.setState({ - ...defaultSchemaContext, - schema: initialSchema, - }); - }); rerender( @@ -193,12 +197,12 @@ describe('DocExplorer', () => { expect(title.textContent).toEqual('field'); // Second render of doc explorer, this time with a new schema, with a different field name - act(() => { - schemaStore.setState({ + useGraphiQL.mockImplementation(cb => + cb({ ...defaultSchemaContext, schema: makeSchema('field2'), // <<< New schema with a new field name - }); - }); + }), + ); rerender( diff --git a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/field-documentation.spec.tsx b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/field-documentation.spec.tsx index 9904a2c7aca..a8689eee9b3 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/__tests__/field-documentation.spec.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/__tests__/field-documentation.spec.tsx @@ -73,7 +73,7 @@ describe('FieldDocumentation', () => { it('should render a simple string field', () => { const { container } = render( , ); expect( @@ -90,7 +90,7 @@ describe('FieldDocumentation', () => { it('should re-render on field change', () => { const { container, rerender } = render( , ); expect( @@ -105,7 +105,7 @@ describe('FieldDocumentation', () => { rerender( , ); expect( @@ -119,7 +119,7 @@ describe('FieldDocumentation', () => { it('should render a string field with arguments', () => { const { container, getByText } = render( , ); expect( @@ -150,7 +150,7 @@ describe('FieldDocumentation', () => { it('should render a string field with directives', () => { const { container } = render( , ); expect( 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 0141a621080..43d7d4ff270 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,13 +10,24 @@ import { import { docExplorerStore } from '../../context'; import { TypeDocumentation } from '../type-documentation'; import { unwrapType } from './test-utils'; -import { schemaStore } from '../../../../graphiql-react/dist/stores/schema'; +import { AllSlices } from '@graphiql/react'; + +vi.mock('@graphiql/react', async () => { + const originalModule = + await vi.importActual('@graphiql/react'); + const useGraphiQL: (typeof originalModule)['useGraphiQL'] = cb => + cb({ schema: ExampleSchema } as AllSlices); + + return { + ...originalModule, + useGraphiQL, + }; +}); const TypeDocumentationWithContext: FC<{ type: GraphQLNamedType }> = ({ type, }) => { useEffect(() => { - schemaStore.setState({ schema: ExampleSchema }); docExplorerStore.setState({ explorerNavStack: [ { @@ -75,8 +86,8 @@ describe('TypeDocumentation', () => { ); const title = container.querySelector( '.graphiql-doc-explorer-section-title', - ); - title?.childNodes[0].remove(); + )!; + title.childNodes[0]!.remove(); expect(title).toHaveTextContent('Possible Types'); }); @@ -86,8 +97,8 @@ describe('TypeDocumentation', () => { ); const title = container.querySelector( '.graphiql-doc-explorer-section-title', - ); - title?.childNodes[0].remove(); + )!; + title.childNodes[0]!.remove(); expect(title).toHaveTextContent('Enum Values'); const enums = container.querySelectorAll( '.graphiql-doc-explorer-enum-value', @@ -105,8 +116,8 @@ describe('TypeDocumentation', () => { const title = container.querySelector( '.graphiql-doc-explorer-section-title', - ); - title?.childNodes[0].remove(); + )!; + title.childNodes[0]!.remove(); expect(title).toHaveTextContent('Enum Values'); let enums = container.querySelectorAll('.graphiql-doc-explorer-enum-value'); @@ -118,8 +129,8 @@ describe('TypeDocumentation', () => { const deprecatedTitle = container.querySelectorAll( '.graphiql-doc-explorer-section-title', - )[1]; - deprecatedTitle.childNodes[0].remove(); + )[1]!; + deprecatedTitle.childNodes[0]!.remove(); expect(deprecatedTitle).toHaveTextContent('Deprecated Enum Values'); enums = container.querySelectorAll('.graphiql-doc-explorer-enum-value'); diff --git a/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.tsx b/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.tsx index 5ecd33d0452..4ff0ad13296 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/doc-explorer.tsx @@ -1,6 +1,6 @@ import { isType } from 'graphql'; import { FC, ReactNode } from 'react'; -import { ChevronLeftIcon, Spinner, useSchemaStore } from '@graphiql/react'; +import { ChevronLeftIcon, Spinner, useGraphiQL, pick } from '@graphiql/react'; import { useDocExplorer, useDocExplorerActions } from '../context'; import { FieldDocumentation } from './field-documentation'; import { SchemaDocumentation } from './schema-documentation'; @@ -9,7 +9,9 @@ import { TypeDocumentation } from './type-documentation'; import './doc-explorer.css'; export const DocExplorer: FC = () => { - const { fetchError, isFetching, schema, validationErrors } = useSchemaStore(); + const { fetchError, isFetching, schema, validationErrors } = useGraphiQL( + pick('fetchError', 'isFetching', 'schema', 'validationErrors'), + ); const explorerNavStack = useDocExplorer(); const { pop } = useDocExplorerActions(); const navItem = explorerNavStack.at(-1)!; @@ -19,7 +21,7 @@ export const DocExplorer: FC = () => { content = (
Error fetching schema
); - } else if (validationErrors.length > 0) { + } else if (validationErrors[0]) { content = (
Schema is invalid: {validationErrors[0].message} diff --git a/packages/graphiql-plugin-doc-explorer/src/components/schema-documentation.tsx b/packages/graphiql-plugin-doc-explorer/src/components/schema-documentation.tsx index 545a178138e..24ae74e6e5f 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/schema-documentation.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/schema-documentation.tsx @@ -16,8 +16,8 @@ export const SchemaDocumentation: FC = ({ schema, }) => { const queryType = schema.getQueryType(); - const mutationType = schema.getMutationType?.(); - const subscriptionType = schema.getSubscriptionType?.(); + const mutationType = schema.getMutationType(); + const subscriptionType = schema.getSubscriptionType(); const typeMap = schema.getTypeMap(); const ignoreTypesInAllSchema = [ queryType?.name, @@ -57,24 +57,22 @@ export const SchemaDocumentation: FC = ({ )} - {typeMap && ( -
- {Object.values(typeMap).map(type => { - if ( - ignoreTypesInAllSchema.includes(type.name) || - type.name.startsWith('__') - ) { - return null; - } +
+ {Object.values(typeMap).map(type => { + if ( + ignoreTypesInAllSchema.includes(type.name) || + type.name.startsWith('__') + ) { + return null; + } - return ( -
- -
- ); - })} -
- )} + return ( +
+ +
+ ); + })} +
); diff --git a/packages/graphiql-plugin-doc-explorer/src/components/search.tsx b/packages/graphiql-plugin-doc-explorer/src/components/search.tsx index 4a3d9b923aa..97d5c46c899 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/search.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/search.tsx @@ -15,10 +15,11 @@ import { ComboboxOption, } from '@headlessui/react'; import { - isMacOs, - useSchemaStore, + formatShortcutForOS, + useGraphiQL, MagnifyingGlassIcon, debounce, + KEY_MAP, } from '@graphiql/react'; import { useDocExplorer, useDocExplorerActions } from '../context'; import { renderType } from './utils'; @@ -91,7 +92,9 @@ export const Search: FC = () => { setSearchValue(event.target.value)} - placeholder={`${isMacOs ? '⌘' : 'Ctrl'} K`} + placeholder={formatShortcutForOS( + KEY_MAP.searchInDocs.key.replace('-', ' '), + )} ref={inputRef} value={searchValue} data-cy="doc-explorer-input" @@ -158,7 +161,7 @@ type FieldMatch = { export function useSearchResults() { const explorerNavStack = useDocExplorer(); - const schema = useSchemaStore(store => store.schema); + const schema = useGraphiQL(state => state.schema); const navItem = explorerNavStack.at(-1)!; @@ -195,7 +198,7 @@ export function useSearchResults() { break; } - const type = typeMap[typeName]; + const type = typeMap[typeName]!; if (withinType !== type && isMatch(typeName, searchValue)) { matches.types.push({ type }); } @@ -210,7 +213,7 @@ export function useSearchResults() { const fields = type.getFields(); for (const fieldName in fields) { - const field = fields[fieldName]; + const field = fields[fieldName]!; let matchingArgs: GraphQLArgument[] | undefined; if (!isMatch(fieldName, searchValue)) { diff --git a/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx b/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx index b95867efbc0..905b045c530 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/type-documentation.tsx @@ -9,7 +9,7 @@ import { isNamedType, isObjectType, } from 'graphql'; -import { useSchemaStore, Button, MarkdownContent } from '@graphiql/react'; +import { useGraphiQL, Button, MarkdownContent } from '@graphiql/react'; import { DocExplorerFieldDef } from '../context'; import { Argument } from './argument'; import { DefaultValue } from './default-value'; @@ -75,7 +75,8 @@ const Fields: FC<{ type: GraphQLNamedType }> = ({ type }) => { const fields: DocExplorerFieldDef[] = []; const deprecatedFields: DocExplorerFieldDef[] = []; - for (const field of Object.keys(fieldMap).map(name => fieldMap[name])) { + // TODO: maybe can be refactored to Object.values(fieldMap) ? + for (const field of Object.keys(fieldMap).map(name => fieldMap[name]!)) { if (field.deprecationReason) { deprecatedFields.push(field); } else { @@ -172,15 +173,15 @@ const EnumValues: FC<{ type: GraphQLNamedType }> = ({ type }) => { return ( <> - {values.length > 0 ? ( + {values.length > 0 && ( {values.map(value => ( ))} - ) : null} - {deprecatedValues.length > 0 ? ( - showDeprecated || values.length === 0 ? ( + )} + {deprecatedValues.length > 0 && + (showDeprecated || !values.length ? ( {deprecatedValues.map(value => ( @@ -190,8 +191,7 @@ const EnumValues: FC<{ type: GraphQLNamedType }> = ({ type }) => { - ) - ) : null} + ))} ); }; @@ -215,7 +215,7 @@ const EnumValue: FC<{ value: GraphQLEnumValue }> = ({ value }) => { }; const PossibleTypes: FC<{ type: GraphQLNamedType }> = ({ type }) => { - const schema = useSchemaStore(store => store.schema); + const schema = useGraphiQL(state => state.schema); if (!schema || !isAbstractType(type)) { return null; } diff --git a/packages/graphiql-plugin-doc-explorer/src/components/type-link.tsx b/packages/graphiql-plugin-doc-explorer/src/components/type-link.tsx index a4acdb99741..4932a97e3ac 100644 --- a/packages/graphiql-plugin-doc-explorer/src/components/type-link.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/components/type-link.tsx @@ -14,20 +14,16 @@ type TypeLinkProps = { export const TypeLink: FC = ({ type }) => { const { push } = useDocExplorerActions(); - if (!type) { - return null; - } - - return renderType(type, namedType => ( + return renderType(type, def => ( { event.preventDefault(); - push({ name: namedType.name, def: namedType }); + push({ name: def.name, def }); }} href="#" > - {namedType.name} + {def.name} )); }; diff --git a/packages/graphiql-plugin-doc-explorer/src/context.ts b/packages/graphiql-plugin-doc-explorer/src/context.ts index 444e615bfee..fef982be199 100644 --- a/packages/graphiql-plugin-doc-explorer/src/context.ts +++ b/packages/graphiql-plugin-doc-explorer/src/context.ts @@ -17,7 +17,8 @@ import { import { FC, ReactElement, ReactNode, useEffect } from 'react'; import { SchemaReference, - useSchemaStore, + useGraphiQL, + pick, createBoundedUseStore, } from '@graphiql/react'; import { createStore } from 'zustand'; @@ -245,7 +246,9 @@ export const docExplorerStore = createStore( export const DocExplorerStore: FC<{ children: ReactNode; }> = ({ children }) => { - const { schema, validationErrors, schemaReference } = useSchemaStore(); + const { schema, validationErrors, schemaReference } = useGraphiQL( + pick('schema', 'validationErrors', 'schemaReference'), + ); useEffect(() => { const { resolveSchemaReferenceToNavItem } = diff --git a/packages/graphiql-plugin-doc-explorer/src/index.tsx b/packages/graphiql-plugin-doc-explorer/src/index.tsx index 37e5e47b849..253e3c48969 100644 --- a/packages/graphiql-plugin-doc-explorer/src/index.tsx +++ b/packages/graphiql-plugin-doc-explorer/src/index.tsx @@ -2,7 +2,7 @@ import { DocsFilledIcon, DocsIcon, GraphiQLPlugin, - usePluginStore, + useGraphiQL, } from '@graphiql/react'; import { DocExplorer } from './components'; @@ -23,7 +23,7 @@ export type { export const DOC_EXPLORER_PLUGIN: GraphiQLPlugin = { title: 'Documentation Explorer', icon: function Icon() { - const visiblePlugin = usePluginStore(store => store.visiblePlugin); + const visiblePlugin = useGraphiQL(state => state.visiblePlugin); return visiblePlugin === DOC_EXPLORER_PLUGIN ? ( ) : ( diff --git a/packages/graphiql-plugin-explorer/src/index.tsx b/packages/graphiql-plugin-explorer/src/index.tsx index 15e70f990ce..b4db982b8b3 100644 --- a/packages/graphiql-plugin-explorer/src/index.tsx +++ b/packages/graphiql-plugin-explorer/src/index.tsx @@ -1,9 +1,8 @@ import React, { CSSProperties, FC, useCallback } from 'react'; import { GraphiQLPlugin, - useEditorStore, - useExecutionStore, - useSchemaStore, + useGraphiQL, + pick, useOperationsEditorState, useOptimisticState, } from '@graphiql/react'; @@ -62,9 +61,9 @@ export type GraphiQLExplorerPluginProps = Omit< >; const ExplorerPlugin: FC = props => { - const setOperationName = useEditorStore(store => store.setOperationName); - const schema = useSchemaStore(store => store.schema); - const run = useExecutionStore(store => store.run); + const { setOperationName, schema, run } = useGraphiQL( + pick('setOperationName', 'schema', 'run'), + ); // handle running the current operation from the plugin const handleRunOperation = useCallback( diff --git a/packages/graphiql-plugin-history/__mocks__/monaco-editor.ts b/packages/graphiql-plugin-history/__mocks__/monaco-editor.ts new file mode 120000 index 00000000000..b1964294fb0 --- /dev/null +++ b/packages/graphiql-plugin-history/__mocks__/monaco-editor.ts @@ -0,0 +1 @@ +../../graphiql/__mocks__/monaco-editor.ts \ No newline at end of file diff --git a/packages/graphiql-plugin-history/setup-files.ts b/packages/graphiql-plugin-history/setup-files.ts index 3079d90df15..da20690a55f 100644 --- a/packages/graphiql-plugin-history/setup-files.ts +++ b/packages/graphiql-plugin-history/setup-files.ts @@ -2,36 +2,6 @@ import '@testing-library/jest-dom'; -vi.mock('zustand'); // to make it works like Jest (auto-mocking) - -/** - * Fixes TypeError: document.queryCommandSupported is not a function - */ -if (!navigator.clipboard) { - Object.defineProperty(navigator, 'clipboard', { - writable: false, - value: { - write: async () => null, - }, - }); -} - -/** - * Fixes TypeError: mainWindow.matchMedia is not a function - * @see https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom - */ -if (!window.matchMedia) { - Object.defineProperty(window, 'matchMedia', { - writable: false, - value: vi.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), // deprecated - removeListener: vi.fn(), // deprecated - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), - }); -} +// to make it works like Jest (auto-mocking) +vi.mock('zustand'); +vi.mock('monaco-editor'); diff --git a/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx b/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx index 89154e147da..caa55c9f916 100644 --- a/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx +++ b/packages/graphiql-plugin-history/src/__tests__/components.spec.tsx @@ -1,9 +1,27 @@ +import { Mock } from 'vitest'; import { fireEvent, render } from '@testing-library/react'; import type { ComponentProps } from 'react'; import { formatQuery, HistoryItem } from '../components'; import { HistoryStore } from '../context'; -import { Tooltip, GraphiQLProvider } from '@graphiql/react'; -import { editorStore } from '../../../graphiql-react/dist/stores/editor'; +import { Tooltip, GraphiQLProvider, useGraphiQL } from '@graphiql/react'; + +vi.mock('@graphiql/react', async () => { + const originalModule = await vi.importActual('@graphiql/react'); + const mockedSetQueryEditor = vi.fn(); + const mockedSetVariableEditor = vi.fn(); + const mockedSetHeaderEditor = vi.fn(); + return { + ...originalModule, + useGraphiQL() { + return { + queryEditor: { setValue: mockedSetQueryEditor }, + variableEditor: { setValue: mockedSetVariableEditor }, + headerEditor: { setValue: mockedSetHeaderEditor }, + tabs: [], + }; + }, + }; +}); const mockQuery = /* GraphQL */ ` query Test($string: String) { @@ -53,6 +71,19 @@ function getMockProps( } describe('QueryHistoryItem', () => { + const { queryEditor, variableEditor, headerEditor } = useGraphiQL( + state => state, + ); + const mockedSetQueryEditor = queryEditor!.setValue as Mock; + const mockedSetVariableEditor = variableEditor!.setValue as Mock; + const mockedSetHeaderEditor = headerEditor!.setValue as Mock; + + beforeEach(() => { + mockedSetQueryEditor.mockClear(); + mockedSetVariableEditor.mockClear(); + mockedSetHeaderEditor.mockClear(); + }); + it('renders operationName if label is not provided', () => { const otherMockProps = { item: { operationName: mockOperationName } }; const props = getMockProps(otherMockProps); @@ -74,26 +105,6 @@ describe('QueryHistoryItem', () => { }); it('selects the item when history label button is clicked', () => { - const mockedSetQueryEditor = vi.fn(); - const mockedSetVariableEditor = vi.fn(); - const mockedSetHeaderEditor = vi.fn(); - type MonacoEditor = NonNullable< - ReturnType['queryEditor'] - >; - - editorStore.setState({ - queryEditor: { - setValue: mockedSetQueryEditor, - } as unknown as MonacoEditor, - variableEditor: { - setValue: mockedSetVariableEditor, - } as unknown as MonacoEditor, - headerEditor: { - setValue: mockedSetHeaderEditor, - getValue: () => '', - } as unknown as MonacoEditor, - }); - const otherMockProps = { item: { operationName: mockOperationName } }; const mockProps = getMockProps(otherMockProps); const { container } = render( diff --git a/packages/graphiql-plugin-history/src/components.tsx b/packages/graphiql-plugin-history/src/components.tsx index bd379ca62ee..1710eaf6ee7 100644 --- a/packages/graphiql-plugin-history/src/components.tsx +++ b/packages/graphiql-plugin-history/src/components.tsx @@ -7,7 +7,8 @@ import { StarFilledIcon, StarIcon, TrashIcon, - useEditorStore, + useGraphiQL, + pick, Button, Tooltip, UnStyledButton, @@ -112,7 +113,9 @@ type QueryHistoryItemProps = { export const HistoryItem: FC = props => { const { editLabel, toggleFavorite, deleteFromHistory, setActive } = useHistoryActions(); - const { headerEditor, queryEditor, variableEditor } = useEditorStore(); + const { headerEditor, queryEditor, variableEditor } = useGraphiQL( + pick('headerEditor', 'queryEditor', 'variableEditor'), + ); const inputRef = useRef(null); const buttonRef = useRef(null); const [isEditable, setIsEditable] = useState(false); diff --git a/packages/graphiql-plugin-history/src/context.tsx b/packages/graphiql-plugin-history/src/context.tsx index 62aed7455c7..664599a14f0 100644 --- a/packages/graphiql-plugin-history/src/context.tsx +++ b/packages/graphiql-plugin-history/src/context.tsx @@ -6,8 +6,8 @@ import { QueryStoreItem, } from '@graphiql/toolkit'; import { - useExecutionStore, - useEditorStore, + useGraphiQL, + pick, useStorage, createBoundedUseStore, } from '@graphiql/react'; @@ -122,9 +122,10 @@ export const HistoryStore: FC = ({ maxHistoryLength = 20, children, }) => { - const isFetching = useExecutionStore(store => store.isFetching); - const { tabs, activeTabIndex } = useEditorStore(); - const activeTab = tabs[activeTabIndex]; + const { isFetching, tabs, activeTabIndex } = useGraphiQL( + pick('isFetching', 'tabs', 'activeTabIndex'), + ); + const activeTab = tabs[activeTabIndex]!; const storage = useStorage(); const historyStorage = // eslint-disable-line react-hooks/exhaustive-deps -- false positive, code is optimized by React Compiler diff --git a/packages/graphiql-react/__mocks__/monaco-editor.ts b/packages/graphiql-react/__mocks__/monaco-editor.ts new file mode 120000 index 00000000000..b1964294fb0 --- /dev/null +++ b/packages/graphiql-react/__mocks__/monaco-editor.ts @@ -0,0 +1 @@ +../../graphiql/__mocks__/monaco-editor.ts \ No newline at end of file diff --git a/packages/graphiql-react/package.json b/packages/graphiql-react/package.json index 44c467fefd2..939d206fed1 100644 --- a/packages/graphiql-react/package.json +++ b/packages/graphiql-react/package.json @@ -42,7 +42,7 @@ "types:check": "tsc --noEmit", "dev": "vite build --watch --emptyOutDir=false", "build": "vite build", - "test": "vitest" + "test": "vitest --typecheck" }, "peerDependencies": { "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", @@ -56,13 +56,12 @@ "@radix-ui/react-tooltip": "^1.2", "@radix-ui/react-visually-hidden": "^1.2", "clsx": "^1.2.1", - "copy-to-clipboard": "^3.2.0", "framer-motion": "^12.12", "get-value": "^3.0.1", "graphql-language-service": "^5.3.1", "jsonc-parser": "^3.3.1", "markdown-it": "^14.1.0", - "monaco-editor": "^0.47", + "monaco-editor": "^0.52.2", "monaco-graphql": "^1.6.1", "prettier": "^3.5.3", "react-compiler-runtime": "19.1.0-rc.1", diff --git a/packages/graphiql-react/setup-files.ts b/packages/graphiql-react/setup-files.ts index 28dc3015b58..b460f6b0ccc 100644 --- a/packages/graphiql-react/setup-files.ts +++ b/packages/graphiql-react/setup-files.ts @@ -1,33 +1,4 @@ -/** - * Fixes TypeError: document.queryCommandSupported is not a function - */ -if (!navigator.clipboard) { - Object.defineProperty(navigator, 'clipboard', { - writable: false, - value: { - write: async () => null, - }, - }); -} - -/** - * Fixes TypeError: mainWindow.matchMedia is not a function - * @see https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom - */ -if (!window.matchMedia) { - Object.defineProperty(window, 'matchMedia', { - writable: false, - value: vi.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), // deprecated - removeListener: vi.fn(), // deprecated - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), - }); -} +// to make it works like Jest (auto-mocking) +vi.mock('monaco-editor'); export {}; diff --git a/packages/graphiql-react/src/components/button-group/index.tsx b/packages/graphiql-react/src/components/button-group/index.tsx index 97d5e0a5952..3534425da4c 100644 --- a/packages/graphiql-react/src/components/button-group/index.tsx +++ b/packages/graphiql-react/src/components/button-group/index.tsx @@ -1,5 +1,5 @@ import { ComponentPropsWithoutRef, forwardRef } from 'react'; -import { clsx } from 'clsx'; +import { cn } from '../../utility'; import './index.css'; export const ButtonGroup = forwardRef< @@ -9,7 +9,7 @@ export const ButtonGroup = forwardRef<
)); ButtonGroup.displayName = 'ButtonGroup'; diff --git a/packages/graphiql-react/src/components/button/index.tsx b/packages/graphiql-react/src/components/button/index.tsx index 98160c4a39b..b5d94fab7f8 100644 --- a/packages/graphiql-react/src/components/button/index.tsx +++ b/packages/graphiql-react/src/components/button/index.tsx @@ -1,5 +1,5 @@ import { ComponentPropsWithoutRef, forwardRef } from 'react'; -import { clsx } from 'clsx'; +import { cn } from '../../utility'; import './index.css'; type UnStyledButtonProps = ComponentPropsWithoutRef<'button'>; @@ -11,7 +11,7 @@ export const UnStyledButton = forwardRef<