diff --git a/packages/altair-app/src/app/modules/altair/components/query-editor/gql-extensions.ts b/packages/altair-app/src/app/modules/altair/components/query-editor/gql-extensions.ts index f1f6e70628..bf9fd0fb9f 100644 --- a/packages/altair-app/src/app/modules/altair/components/query-editor/gql-extensions.ts +++ b/packages/altair-app/src/app/modules/altair/components/query-editor/gql-extensions.ts @@ -124,7 +124,7 @@ export const getCodemirrorGraphqlExtensions = (opts: ExtensionsOptions) => { autocomplete: (context: CompletionContext) => { const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1); - // Only show if inside a field SelectionSet + // Show if inside a field SelectionSet if ( nodeBefore.name === 'SelectionSet' && nodeBefore.parent?.name === 'Field' @@ -145,6 +145,27 @@ export const getCodemirrorGraphqlExtensions = (opts: ExtensionsOptions) => { }; } + // Show if inside an argument ObjectValue (input object type) + if ( + nodeBefore.name === 'ObjectValue' || + (nodeBefore.name === '{' && nodeBefore.parent?.name === 'ObjectValue') + ) { + return { + from: context.pos, + options: [ + { + label: 'Fill all fields', + apply(view: EditorView) { + fillAllFieldsCommands(view); + }, + boost: 99, + type: 'function', + info: 'Automatically fill in all the fields for this input object argument (controlled by addQueryDepthLimit in settings)', + }, + ], + }; + } + return null; }, }), diff --git a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.spec.ts b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.spec.ts index ab3c738997..b15e704949 100644 --- a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.spec.ts +++ b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.spec.ts @@ -8,6 +8,11 @@ const testQuery = `{ GOTBooks { } }`; + +const testQueryWithArgument = `{ + withGOTCharacter(character: { }) +}`; + describe('fillAllFields', () => { it('generates expected query', () => { const schema = getTestSchema(); @@ -19,4 +24,16 @@ describe('fillAllFields', () => { }); expect(res).toMatchSnapshot(); }); + + it('generates fields for input object arguments', () => { + const schema = getTestSchema(); + // Position cursor inside the character argument object braces + const pos = new Position(1, 32); + const token = getTokenAtPosition(testQueryWithArgument, pos, 1); + const res = fillAllFields(schema, testQueryWithArgument, pos, token, { + maxDepth: 2, + }); + expect(res.result).toContain('id:'); + expect(res.result).toContain('book:'); + }); }); diff --git a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts index b5fe8d550e..f55a2b3107 100644 --- a/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts +++ b/packages/altair-app/src/app/modules/altair/services/gql/fillFields.ts @@ -1,4 +1,13 @@ -import { visit, print, TypeInfo, parse, GraphQLSchema, Kind } from 'graphql'; +import { + visit, + print, + TypeInfo, + parse, + GraphQLSchema, + Kind, + isInputObjectType, + GraphQLInputObjectType, +} from 'graphql'; import { debug } from '../../utils/logger'; import getTypeInfo from 'codemirror-graphql/utils/getTypeInfo'; import { ContextToken } from 'graphql-language-service'; @@ -57,6 +66,45 @@ export interface FillAllFieldsOptions { maxDepth?: number; } +const buildInputObjectFields = ( + inputType: GraphQLInputObjectType, + { maxDepth = 1, currentDepth = 0 } = {} +): string => { + if (currentDepth >= maxDepth) { + return ''; + } + + const fields = inputType.getFields(); + const fieldEntries = Object.entries(fields).map(([fieldName, field]) => { + // Unwrap the type to get to the base type (remove NonNull and List wrappers) + let unwrappedType = field.type; + while ( + unwrappedType && + ('ofType' in unwrappedType) && + unwrappedType.ofType + ) { + unwrappedType = unwrappedType.ofType as any; + } + + // For nested input objects, recursively build fields + if (isInputObjectType(unwrappedType)) { + if (currentDepth + 1 < maxDepth) { + const nestedFields = buildInputObjectFields(unwrappedType, { + maxDepth, + currentDepth: currentDepth + 1, + }); + return `${fieldName}: {${nestedFields ? `\n ${nestedFields}\n` : ''}}`; + } + return `${fieldName}: `; + } + + // For scalar types, just add the field name + return `${fieldName}: `; + }); + + return fieldEntries.join('\n'); +}; + // Improved version based on: // https://github.com/graphql/graphiql/blob/272e2371fc7715217739efd7817ce6343cb4fbec/src/utility/fillLeafs.js export const fillAllFields = ( @@ -72,11 +120,64 @@ export const fillAllFields = ( } let tokenState = token.state as any; + let isSelectionSetMode = false; + let isObjectValueMode = false; + if (tokenState.kind === Kind.SELECTION_SET) { tokenState.wasSelectionSet = true; tokenState = { ...tokenState, ...tokenState.prevState }; + isSelectionSetMode = true; + } + // Check if we're in an object value (argument) + // The token state kind for object values is typically 'ObjectValue' or the token itself is '{' + if (tokenState.kind === 'ObjectValue' || tokenState.kind === '{') { + tokenState.wasObjectValue = true; + tokenState = { ...tokenState, ...tokenState.prevState }; + isObjectValueMode = true; + } + + const typeInfoResult = getTypeInfo(schema, token.state); + const fieldType = typeInfoResult.type; + const inputType = typeInfoResult.inputType; + + // For object value mode (arguments), handle specially without stripping + if (isObjectValueMode && inputType && isInputObjectType(inputType)) { + // Don't strip, parse as-is since `{ }` is valid for arguments + const ast = parseQuery(query); + if (!ast) { + return { insertions, result: query }; + } + + const typeInfo = new TypeInfo(schema); + visit(ast, { + enter(node) { + typeInfo.enter(node); + // Find the OBJECT node at the cursor position + if (node.kind === Kind.OBJECT && node.loc && + node.loc.startToken.line - 1 === cursor.line) { + const currentInputType = typeInfo.getInputType(); + if (currentInputType && isInputObjectType(currentInputType)) { + const fieldsString = buildInputObjectFields(currentInputType, { maxDepth }); + const indent = getIndentation(query, node.loc.start); + if (fieldsString && node.fields.length === 0) { + // Only fill if the object is empty + insertions.push({ + index: node.loc.start + 1, + string: '\n' + indent + ' ' + fieldsString.replace(/\n/g, '\n' + indent + ' ') + '\n' + indent, + }); + } + } + } + }, + }); + + return { + insertions, + result: withInsertions(query, insertions), + }; } - const fieldType = getTypeInfo(schema, token.state).type; + + // Original logic for selection sets // Strip out empty selection sets since those throw errors while parsing query query = query.replace(/{\s*}/g, ''); const ast = parseQuery(query); diff --git a/packages/altair-docs/docs/features/autofill-fields-at-cursor.md b/packages/altair-docs/docs/features/autofill-fields-at-cursor.md index 09c61fd70d..1ef16e5fb2 100644 --- a/packages/altair-docs/docs/features/autofill-fields-at-cursor.md +++ b/packages/altair-docs/docs/features/autofill-fields-at-cursor.md @@ -14,6 +14,31 @@ Note: You can change the autocompletion depth limit using a [`addQueryDepthLimit ![Autofill fields](/assets/img/docs/autofill-fields.gif) -::: warning -Note: This only works for the query fields, and not for the arguments. You can still [generate whole queriea and fragments](/docs/features/add-queries-and-fragments) directly from the docs along with their arguments filled in. +## Works with arguments too! + +This feature now also works with query arguments that accept input object types. When you place your cursor inside an empty argument object (e.g., `character: { }`), you can use the same keyboard shortcut (`Ctrl+Shift+Enter`) or select "Fill all fields" from the autocomplete menu to automatically fill in all the fields for that input type. + +For example, given this query: +```graphql +{ + withGOTCharacter(character: { }) +} +``` + +Placing the cursor inside the empty braces and pressing `Ctrl+Shift+Enter` will automatically fill in the required fields: +```graphql +{ + withGOTCharacter(character: { + id: + book: { + id: + url: + name: + } + }) +} +``` + +::: tip +You can still [generate whole queries and fragments](/docs/features/add-queries-and-fragments) directly from the docs along with their arguments filled in. :::