v6: Add a visual query builder (@graphiql/plugin-query-builder)#4352
Merged
Conversation
🦋 Changeset detectedLatest commit: 071aeab The changes in this PR will be included in the next version bump. This PR includes changesets to release 7 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
New first-party plugin: pick fields from the schema with checkboxes and the operation in the editor stays in sync both ways. Every change runs through a parse, mutate, reprint cycle over the `graphql` AST, so the builder and the editor never drift apart. Covers a schema tree of collapsible root types (scalar fields are checkboxes, composites are expand-only), argument inputs typed per argument (scalars, enums, lists, and recursive input objects including the list-of-input-objects case), promoting arguments to and from variables with orphaned-variable cleanup, and union and interface type conditions via `... on TypeName` selectors. The builder reads and edits the operation the editor cursor sits in, and moving the cursor expands the tree down to the field it lands on. The plugin is default-installed in the `graphiql` meta-package; omit it from your `plugins` list to opt out. Also changes `@graphiql/react`: the active `operationName` now follows the editor cursor, so the Run button, the operation dropdown, and operation-aware plugins reflect the operation you are editing.
f66df5d to
daf647d
Compare
`replaceVariableInSelectionSet` only walked `Kind.FIELD` selections, so demoting a variable referenced inside an `... on Type` inline fragment removed the variable definition but left the dangling `$var` reference, producing an invalid document. Recurse into inline-fragment selection sets too.
`argValueToValueNode` always returned a `ListValueNode`/`ObjectValueNode` for list and input-object types, so emptying a list left `arg: []` and clearing every field of a nested input object left `arg: {}` in the document. Scalars already follow the documented "empty input removes the argument" contract; make lists and input objects consistent by returning `undefined` when nothing is left to emit.
`resolveSchemaArg` walked path segments through `getFields()`, so a `... on TypeName` segment failed the field lookup and returned undefined. Editing an argument on a field inside a union or interface type condition then silently did nothing. Switch the current type on `... on` segments, mirroring `resolveFieldNamedType`.
A number input let the user type a decimal or scientific value straight into the AST, so an `Int` field could hold `{ kind: INT, value: "1.5" }` and a `Float` could hold a literal with no integer part, both rejected at execution. `scalarToValueNode` now coerces `Int` values through `Math.trunc` (rejecting non-finite or unsafe values) and `Float` values through `Number`, and the `Int` input carries `step="1"`.
The teardown cancelled the debounced cursor sync but not the debounced `handleChange`, so a content change landing within the debounce window of unmount could fire `editor.getValue()` and `storage.set()` against a disposed editor. Cancel both.
`onDidChangeCursorPosition` fired the operation-name sync on every cursor change reason, including `ContentFlush` (a `setValue`) and programmatic `setPosition` calls. The query builder writing an edit back to the editor could therefore retarget the active operation. Guard the sync on `CursorChangeReason.Explicit`, matching the query builder`s own cursor handler.
`overrideOperationName` was set only at store creation, so pinning an operation by setting the `operationName` prop after mount (e.g. from a URL or app state) never took effect, and clearing it never restored cursor tracking. Sync it on prop change like the other prop syncs.
The expanded state was computed once at mount, so a type condition added to the document later left the section collapsed. Derive it reactively and open (never force-close) via an effect, so the section reflects the document while still allowing manual collapse.
The variable toggle fell through to `onPromote` whenever it was not able to demote, so clicking "Inline argument" on an already-promoted arg without an `onDemote` handler re-promoted it. Only promote when the arg is not already a variable.
Deprecated fields rendered identically to the rest. Strike through the field name and show a "deprecated" badge whose tooltip carries the deprecation reason.
Present the shipped capabilities directly instead of describing them as in-progress, since the plugin is default-installed in `graphiql`. Keep the real known limitations (aliases, explicit null).
Note the new plugin, how to opt out via the `plugins` prop, and that its stylesheet is bundled into `graphiql` regardless of the plugin list.
A brand-new package should enter the release flow as a patch rather than a minor; the `graphiql` bump stays a minor for the new default plugin.
Declare `@testing-library/user-event` (used across the tests but only resolved transitively), add the `main` field, and widen the `graphql` peer range to `^17.0.0-alpha.2` so it matches v17 prereleases, matching `@graphiql/plugin-explorer`.
bf4c723 to
8ec5c7a
Compare
The plugin was scaffolded without React Compiler, unlike the other first-party plugins (\`doc-explorer\`, \`history\`), so its components shipped un-memoized. Wire the compiler into the build and make every component compiler-clean so it optimizes them rather than relying on hand-written memoization: - Replace the prop-sync \`useEffect\` in \`ListArgInput\` with an adjust-state-during-render guard, removing the disabled \`exhaustive-deps\` rule the compiler rejects. Behavior is unchanged: local rows still survive an echoed write. - Replace the module-global \`nextListItemId\` counter with \`crypto.randomUUID()\` ids, dropping an unsupported global mutation (and the latent cross-instance id sharing). - Rewrite a computed-property destructure the compiler does not yet support.
The plugin replaced the whole \`@graphiql/react\` module with a hand-written stub via a vitest alias, which silently drifts from the real package. Match the sibling plugins (\`doc-explorer\`, \`history\`): import the real module and \`vi.mock\` only the hooks (\`useGraphiQL\`, \`useGraphiQLActions\`), so icons, \`PanelHeader\`, and types come from the real package and stay honest. A small \`graphiql-react-mock\` helper keeps the \`__state\` control surface the tests already used. Drops the module alias, adds the \`monaco-editor\` resolution alias and \`vi.mock(monaco-editor|zustand)\` the siblings use, and a \`use no memo\` directive in the test setup.
- Rename test files \`.test.*\` to \`.spec.*\` (the convention across the other plugins). - Move \`src/test-setup.ts\` to \`setup-files.ts\` at the package root, matching siblings. - Rename \`src/index.css\` to \`src/style.css\` (history`s naming) and update importers. - Drop the redundant \`main\` field (siblings rely on \`exports\`) and align the \`graphql\` peer range to \`^17.0.0\`.
Export \`plugins\` from \`vite.config.mts\` and consume them in \`vitest.config.mts\`, so the test run exercises compiler-optimized components rather than raw source, matching the sibling plugins.
Match the sibling plugins (\`HISTORY_PLUGIN\`, \`DOC_EXPLORER_PLUGIN\`): export a ready-made plugin constant instead of a \`queryBuilderPlugin()\` factory. The builder takes no per-instance configuration, so the factory added no value. Updates the \`graphiql\` meta-package wiring and the README usage.
Source the rail icon from the shared \`@graphiql/react\` icon set (\`QueryBuilderIcon\`) like the other plugins, instead of bundling a local SVG. Drops the \`vite-plugin-svgr\` dependency, the svgr build/test plugin, and the svgr type reference from the query-builder package.
The plugin shipped a stories file with no \`.storybook\` config, so nothing could serve or build it. Add \`main.ts\`/\`preview.tsx\` and the storybook deps/scripts mirroring \`doc-explorer\`, so the stories are runnable and the conventions match.
…gleton The `graphiql-react-mock` helper exposed a module-level mutable `__state` object that every spec mutated and had to remember to reset, which risked state leaking between tests. `installGraphiQLReactMock()` now builds a fresh state object per call and returns it; the mocks read that object live, so write-back tests still observe mutations on re-render. Each test owns its own state, so there is nothing to reset and nothing to leak.
775bcf7 to
f56ca63
Compare
… drop historical framing
The spec clicked `.view-line` nodes twice in sequence; Monaco repaints those on layout passes, so the second click could race a repaint and silently miss in headless runs. Drive the cursor through Monaco's textarea with keyboard navigation instead: it is DOM-stable and produces the `Explicit` cursor change that operation tracking keys off of, landing at deterministic offsets.
Introduce `cy.typeInEditor`, `cy.setCursorToLine`, and `cy.activateOperation` so editor input and cursor positioning have one documented, correct-by-default pattern for future specs to copy. `activateOperation` finds an operation's line from the `?query=` URL and navigates there by keyboard, replacing the `.view-line` clicks that race Monaco's layout repaints. Adopt the commands in the operation-cursor, query-builder, keyboard, and tabs specs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New package
@graphiql/plugin-query-builder, a first-party visual query builder for GraphiQL. People have asked for one since #734.Pick fields from the schema with checkboxes and the operation in the editor stays in sync, both ways. Every change runs through the same parse → mutate → reprint cycle using the
graphqlpackage's AST utilities, so the builder and the editor never drift apart.What it covers:
$namein the operation; an "Inline argument" toggle reverses it. Suggested names are unique across the whole document, and removing a field cleans up any variable it orphaned.... on TypeNameselector; checking a field inside one adds it within that inline fragment. Interface fields shared by every type can also be selected directly, without a type condition. Named fragments already in the document are listed.The plugin is default-installed in the
graphiqlmeta-package, so the rail icon is present with no extra configuration. Omit it from yourpluginslist to opt out.This also changes
@graphiql/react: the activeoperationNamenow follows the editor cursor, so the Run button, the operation dropdown, and operation-aware plugins reflect the operation you are editing. See thegraphiql@6migration guide for details.Test plan
$name, and the button now reads "Inline argument". Click it; confirm the literal returns and the Variables entry is gone.+Ncount, and editing an argument in one operation doesn't jump the builder to the other. Confirm only the active operation's root type is enabled.... on TypeName(with no prior selection on the field) and confirm... on TypeName { __typename }is added. Check a field inside it; confirm it lands inside the fragment, not as a sibling. For an interface, confirm its shared fields can be checked directly.$namedefinition and its Variables entry are both removed, leaving valid GraphQL.Refs: #734, #4219