diff --git a/.changeset/async-select-query-name.md b/.changeset/async-select-query-name.md
new file mode 100644
index 00000000000..21a9210fa60
--- /dev/null
+++ b/.changeset/async-select-query-name.md
@@ -0,0 +1,11 @@
+---
+"@comet/admin-generator": minor
+---
+
+asyncSelect: query name is now derived from field name instead of rootQuery
+
+Previously the GraphQL operation name was derived from `rootQuery` (e.g. `rootQuery: "people"` → `PeopleSelect`). When multiple fields shared the same `rootQuery`, this produced duplicate operation names, causing errors in `gql:types`.
+
+The operation name is now derived from the **field name** (e.g. field `host` → `HostSelect`, field `guest` → `GuestSelect`). This ensures uniqueness across all fields in a form automatically, without any extra configuration.
+
+An optional `queryName` setting was also added to both `asyncSelect` and `asyncSelectFilter` to manually override the generated operation name when needed.
diff --git a/demo/admin/src/products/generator/generated/CreateCapProductForm.tsx b/demo/admin/src/products/generator/generated/CreateCapProductForm.tsx
index 1973fe27152..bff3a753deb 100644
--- a/demo/admin/src/products/generator/generated/CreateCapProductForm.tsx
+++ b/demo/admin/src/products/generator/generated/CreateCapProductForm.tsx
@@ -20,8 +20,8 @@ import { FORM_ERROR } from "final-form";
import { GQLProductType } from "@src/graphql.generated";
import { DamImageBlock } from "@comet/cms-admin";
import { validateTitle } from "../validateTitle";
-import { GQLProductCategoriesSelectQuery } from "./CreateCapProductForm.generated";
-import { GQLProductCategoriesSelectQueryVariables } from "./CreateCapProductForm.generated";
+import { GQLCategorySelectQuery } from "./CreateCapProductForm.generated";
+import { GQLCategorySelectQueryVariables } from "./CreateCapProductForm.generated";
import { AsyncAutocompleteField } from "@comet/admin";
import { CalendarToday as CalendarTodayIcon } from "@comet/admin-icons";
import { Future_DatePickerField } from "@comet/admin";
@@ -135,9 +135,9 @@ export function CreateCapProductForm({ onCreate, type }: FormProps) {
name="category"
label={}
loadOptions={async (search?: string) => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql`
- query ProductCategoriesSelect($search: String) {
+ query CategorySelect($search: String) {
productCategories(search: $search) {
nodes {
id
diff --git a/demo/admin/src/products/generator/generated/ProductCategoryForm.tsx b/demo/admin/src/products/generator/generated/ProductCategoryForm.tsx
index 75b3af792ce..c77fc2876df 100644
--- a/demo/admin/src/products/generator/generated/ProductCategoryForm.tsx
+++ b/demo/admin/src/products/generator/generated/ProductCategoryForm.tsx
@@ -16,8 +16,8 @@ import { resolveHasSaveConflict } from "@comet/cms-admin";
import { useFormSaveConflict } from "@comet/cms-admin";
import { FormApi } from "final-form";
import { useMemo } from "react";
-import { GQLProductCategoryTypesSelectQuery } from "./ProductCategoryForm.generated";
-import { GQLProductCategoryTypesSelectQueryVariables } from "./ProductCategoryForm.generated";
+import { GQLTypeSelectQuery } from "./ProductCategoryForm.generated";
+import { GQLTypeSelectQueryVariables } from "./ProductCategoryForm.generated";
import { AsyncAutocompleteField } from "@comet/admin";
import { productCategoryFormFragment } from "./ProductCategoryForm.gql";
import { GQLProductCategoryFormFragment } from "./ProductCategoryForm.gql.generated";
@@ -130,9 +130,9 @@ export function ProductCategoryForm({ onCreate, id }: FormProps) {
name="type"
label={}
loadOptions={async (search?: string) => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql`
- query ProductCategoryTypesSelect($search: String) {
+ query TypeSelect($search: String) {
productCategoryTypes(search: $search) {
nodes {
id
diff --git a/demo/admin/src/products/generator/generated/ProductForm.tsx b/demo/admin/src/products/generator/generated/ProductForm.tsx
index b533678947b..89896170709 100644
--- a/demo/admin/src/products/generator/generated/ProductForm.tsx
+++ b/demo/admin/src/products/generator/generated/ProductForm.tsx
@@ -35,19 +35,19 @@ import { GQLFinalFormFileUploadDownloadableFragment } from "@comet/cms-admin";
import { validateProductSlug } from "../validateProductSlug";
import { Future_DatePickerField } from "@comet/admin";
import { SelectField } from "@comet/admin";
-import { GQLProductCategoriesSelectQuery } from "./ProductForm.generated";
-import { GQLProductCategoriesSelectQueryVariables } from "./ProductForm.generated";
+import { GQLCategorySelectQuery } from "./ProductForm.generated";
+import { GQLCategorySelectQueryVariables } from "./ProductForm.generated";
import { AsyncAutocompleteField } from "@comet/admin";
-import { GQLProductTagsSelectQuery } from "./ProductForm.generated";
-import { GQLProductTagsSelectQueryVariables } from "./ProductForm.generated";
+import { GQLTagsSelectQuery } from "./ProductForm.generated";
+import { GQLTagsSelectQueryVariables } from "./ProductForm.generated";
import { FinalFormSwitch } from "@comet/admin";
import { messages } from "@comet/admin";
import { FormControlLabel } from "@mui/material";
import { FieldSet } from "@comet/admin";
import { FormSpy } from "react-final-form";
import { Location as LocationIcon } from "@comet/admin-icons";
-import { GQLManufacturersSelectQuery } from "./ProductForm.generated";
-import { GQLManufacturersSelectQueryVariables } from "./ProductForm.generated";
+import { GQLManufacturerSelectQuery } from "./ProductForm.generated";
+import { GQLManufacturerSelectQueryVariables } from "./ProductForm.generated";
import { CalendarToday as CalendarTodayIcon } from "@comet/admin-icons";
import { FutureProductNotice } from "../../helpers/FutureProductNotice";
import { Future_DateTimePickerField as DateTimePickerField } from "@comet/admin";
@@ -360,9 +360,9 @@ export function ProductForm({ initialValues: passedInitialValues, onCreate, manu
name="category"
label={}
loadOptions={async (search?: string) => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql`
- query ProductCategoriesSelect($search: String) {
+ query CategorySelect($search: String) {
productCategories(search: $search) {
nodes {
id
@@ -387,9 +387,9 @@ export function ProductForm({ initialValues: passedInitialValues, onCreate, manu
name="tags"
label={}
loadOptions={async (search?: string) => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql`
- query ProductTagsSelect($search: String) {
+ query TagsSelect($search: String) {
productTags(search: $search) {
nodes {
id
@@ -476,9 +476,9 @@ export function ProductForm({ initialValues: passedInitialValues, onCreate, manu
}
loadOptions={async (search?: string) => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql`
- query ManufacturersSelect($search: String, $filter: ManufacturerFilter) {
+ query ManufacturerSelect($search: String, $filter: ManufacturerFilter) {
manufacturers(search: $search, filter: $filter) {
nodes {
id
diff --git a/demo/admin/src/products/generator/generated/ProductHighlightForm.tsx b/demo/admin/src/products/generator/generated/ProductHighlightForm.tsx
index 65b1dff81f0..d4d24ce8f93 100644
--- a/demo/admin/src/products/generator/generated/ProductHighlightForm.tsx
+++ b/demo/admin/src/products/generator/generated/ProductHighlightForm.tsx
@@ -16,14 +16,14 @@ import { resolveHasSaveConflict } from "@comet/cms-admin";
import { useFormSaveConflict } from "@comet/cms-admin";
import { FormApi } from "final-form";
import { useMemo } from "react";
-import { GQLProductCategoryTypesSelectQuery } from "./ProductHighlightForm.generated";
-import { GQLProductCategoryTypesSelectQueryVariables } from "./ProductHighlightForm.generated";
+import { GQLProductCategoryTypeSelectQuery } from "./ProductHighlightForm.generated";
+import { GQLProductCategoryTypeSelectQueryVariables } from "./ProductHighlightForm.generated";
import { AsyncAutocompleteField } from "@comet/admin";
import { OnChangeField } from "@comet/admin";
-import { GQLProductCategoriesSelectQuery } from "./ProductHighlightForm.generated";
-import { GQLProductCategoriesSelectQueryVariables } from "./ProductHighlightForm.generated";
-import { GQLProductsSelectQuery } from "./ProductHighlightForm.generated";
-import { GQLProductsSelectQueryVariables } from "./ProductHighlightForm.generated";
+import { GQLProductCategorySelectQuery } from "./ProductHighlightForm.generated";
+import { GQLProductCategorySelectQueryVariables } from "./ProductHighlightForm.generated";
+import { GQLProductSelectQuery } from "./ProductHighlightForm.generated";
+import { GQLProductSelectQueryVariables } from "./ProductHighlightForm.generated";
import { productHighlightFormFragment } from "./ProductHighlightForm.gql";
import { GQLProductHighlightFormDetailsFragment } from "./ProductHighlightForm.gql.generated";
import { productHighlightQuery } from "./ProductHighlightForm.gql";
@@ -143,9 +143,9 @@ export function ProductHighlightForm({ onCreate, id }: FormProps) {
name="productCategoryType"
label={}
loadOptions={async (search?: string) => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql`
- query ProductCategoryTypesSelect($search: String) {
+ query ProductCategoryTypeSelect($search: String) {
productCategoryTypes(search: $search) {
nodes {
id
@@ -169,9 +169,9 @@ export function ProductHighlightForm({ onCreate, id }: FormProps) {
name="productCategory"
label={}
loadOptions={async (search?: string) => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql`
- query ProductCategoriesSelect($search: String, $filter: ProductCategoryFilter) {
+ query ProductCategorySelect($search: String, $filter: ProductCategoryFilter) {
productCategories(search: $search, filter: $filter) {
nodes {
id
@@ -204,9 +204,9 @@ export function ProductHighlightForm({ onCreate, id }: FormProps) {
name="product"
label={}
loadOptions={async (search?: string) => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql`
- query ProductsSelect($search: String, $filter: ProductFilter) {
+ query ProductSelect($search: String, $filter: ProductFilter) {
products(search: $search, filter: $filter) {
nodes {
id
diff --git a/packages/admin/admin-generator/src/commands/generate/generate-command.ts b/packages/admin/admin-generator/src/commands/generate/generate-command.ts
index c56fa300875..b476a0f990d 100644
--- a/packages/admin/admin-generator/src/commands/generate/generate-command.ts
+++ b/packages/admin/admin-generator/src/commands/generate/generate-command.ts
@@ -113,6 +113,12 @@ export type FormFieldConfig = (
type: "asyncSelect";
name: UsableFormFields;
rootQuery: string;
+ /**
+ * Override the generated GraphQL query name.
+ * Defaults to the field name with an uppercased first letter and "Select" appended (e.g. field "host" → "HostSelect").
+ * Useful when the auto-generated name (based on the field name) is not descriptive enough.
+ */
+ queryName?: string;
labelField?: string;
/** Whether Autocomplete or Select should be used.
*
@@ -129,6 +135,12 @@ export type FormFieldConfig = (
name: string;
loadValueQueryField: string; //TODO improve typing, use something similar to UsableFormFields;
rootQuery: string;
+ /**
+ * Override the generated GraphQL query name.
+ * Defaults to the field name with an uppercased first letter and "Select" appended (e.g. field "productCategory" → "ProductCategorySelect").
+ * Useful when the auto-generated name (based on the field name) is not descriptive enough.
+ */
+ queryName?: string;
labelField?: string;
/** Whether Autocomplete or Select should be used.
*
diff --git a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelect.test.ts.snap b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelect.test.ts.snap
index eb256e1fde9..89845d70197 100644
--- a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelect.test.ts.snap
+++ b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelect.test.ts.snap
@@ -11,9 +11,9 @@ exports[`AsyncSelect filter > generates field without filter 1`] = `
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect {
+ query CategorySelect {
categories {
nodes { id name }
}
@@ -42,9 +42,9 @@ exports[`AsyncSelect filter > generates filter with value dependent on other fie
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($title: String) {
+ query CategorySelect($title: String) {
categories(title: $title) {
nodes { id name }
}
@@ -106,8 +106,8 @@ import { FormApi } from "final-form";
import { useMemo } from "react";
import { GQLCategoryType } from "@src/graphql.generated";
import { OnChangeField } from "@comet/admin";
-import { GQLCategoriesSelectQuery } from "./ProductForm.generated";
-import { GQLCategoriesSelectQueryVariables } from "./ProductForm.generated";
+import { GQLCategorySelectQuery } from "./ProductForm.generated";
+import { GQLCategorySelectQueryVariables } from "./ProductForm.generated";
import { AsyncSelectField } from "@comet/admin";
import { productFormFragment } from "./ProductForm.gql";
import { GQLProductFormFragment } from "./ProductForm.gql.generated";
@@ -240,9 +240,9 @@ id?: string;
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($type: CategoryType) {
+ query CategorySelect($type: CategoryType) {
categories(type: $type) {
nodes { id name }
}
@@ -305,8 +305,8 @@ import { InputAdornment } from "@mui/material";
import { FormApi } from "final-form";
import { useMemo } from "react";
import { OnChangeField } from "@comet/admin";
-import { GQLCategoriesSelectQuery } from "./ProductForm.generated";
-import { GQLCategoriesSelectQueryVariables } from "./ProductForm.generated";
+import { GQLCategorySelectQuery } from "./ProductForm.generated";
+import { GQLCategorySelectQueryVariables } from "./ProductForm.generated";
import { AsyncSelectField } from "@comet/admin";
import { productFormFragment } from "./ProductForm.gql";
import { GQLProductFormFragment } from "./ProductForm.gql.generated";
@@ -439,9 +439,9 @@ id?: string;
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($foo: String) {
+ query CategorySelect($foo: String) {
categories(foo: $foo) {
nodes { id name }
}
@@ -504,8 +504,8 @@ import { InputAdornment } from "@mui/material";
import { FormApi } from "final-form";
import { useMemo } from "react";
import { OnChangeField } from "@comet/admin";
-import { GQLCategoriesSelectQuery } from "./ProductForm.generated";
-import { GQLCategoriesSelectQueryVariables } from "./ProductForm.generated";
+import { GQLCategorySelectQuery } from "./ProductForm.generated";
+import { GQLCategorySelectQueryVariables } from "./ProductForm.generated";
import { AsyncSelectField } from "@comet/admin";
import { productFormFragment } from "./ProductForm.gql";
import { GQLProductFormFragment } from "./ProductForm.gql.generated";
@@ -638,9 +638,9 @@ id?: string;
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($filter: CategoryFilter) {
+ query CategorySelect($filter: CategoryFilter) {
categories(filter: $filter) {
nodes { id name }
}
@@ -704,8 +704,8 @@ import { FormApi } from "final-form";
import { useMemo } from "react";
import { GQLCategoryType } from "@src/graphql.generated";
import { OnChangeField } from "@comet/admin";
-import { GQLCategoriesSelectQuery } from "./ProductForm.generated";
-import { GQLCategoriesSelectQueryVariables } from "./ProductForm.generated";
+import { GQLCategorySelectQuery } from "./ProductForm.generated";
+import { GQLCategorySelectQueryVariables } from "./ProductForm.generated";
import { AsyncSelectField } from "@comet/admin";
import { productFormFragment } from "./ProductForm.gql";
import { GQLProductFormFragment } from "./ProductForm.gql.generated";
@@ -838,9 +838,9 @@ id?: string;
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($filter: CategoryFilter) {
+ query CategorySelect($filter: CategoryFilter) {
categories(filter: $filter) {
nodes { id name }
}
diff --git a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectAutocomplete.test.ts.snap b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectAutocomplete.test.ts.snap
index 71a624ae68a..f26c8a702af 100644
--- a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectAutocomplete.test.ts.snap
+++ b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectAutocomplete.test.ts.snap
@@ -11,9 +11,9 @@ exports[`AsyncSelect autocomplete > generates field with default autocomplete be
label={}
loadOptions={async (search?: string) => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($search: String) {
+ query CategorySelect($search: String) {
categories(search: $search) {
nodes { id name }
}
@@ -42,9 +42,9 @@ exports[`AsyncSelect autocomplete > generates field without autocomplete explici
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect {
+ query CategorySelect {
categories {
nodes { id name }
}
diff --git a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectFilter.test.ts.snap b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectFilter.test.ts.snap
index 8c7220d4f99..ef6d4e2b3b3 100644
--- a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectFilter.test.ts.snap
+++ b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectFilter.test.ts.snap
@@ -36,12 +36,12 @@ import { MenuItem } from "@mui/material";
import { InputAdornment } from "@mui/material";
import { FormApi } from "final-form";
import { useMemo } from "react";
-import { GQLProductCategoriesSelectQuery } from "./ProductHighlightForm.generated";
-import { GQLProductCategoriesSelectQueryVariables } from "./ProductHighlightForm.generated";
+import { GQLProductCategorySelectQuery } from "./ProductHighlightForm.generated";
+import { GQLProductCategorySelectQueryVariables } from "./ProductHighlightForm.generated";
import { AsyncSelectField } from "@comet/admin";
import { OnChangeField } from "@comet/admin";
-import { GQLProductsSelectQuery } from "./ProductHighlightForm.generated";
-import { GQLProductsSelectQueryVariables } from "./ProductHighlightForm.generated";
+import { GQLProductSelectQuery } from "./ProductHighlightForm.generated";
+import { GQLProductSelectQueryVariables } from "./ProductHighlightForm.generated";
import { productHighlightFormFragment } from "./ProductHighlightForm.gql";
import { GQLProductHighlightFormFragment } from "./ProductHighlightForm.gql.generated";
import { productHighlightQuery } from "./ProductHighlightForm.gql";
@@ -172,9 +172,9 @@ id?: string;
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query ProductCategoriesSelect {
+ query ProductCategorySelect {
productCategories {
nodes { id title }
}
@@ -200,9 +200,9 @@ id?: string;
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query ProductsSelect($filter: ProductFilter) {
+ query ProductSelect($filter: ProductFilter) {
products(filter: $filter) {
nodes { id title }
}
@@ -245,9 +245,9 @@ exports[`AsyncSelectFilter > generates filter with value dependent on other fiel
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($title: String) {
+ query CategorySelect($title: String) {
categories(title: $title) {
nodes { id name }
}
@@ -309,8 +309,8 @@ import { FormApi } from "final-form";
import { useMemo } from "react";
import { GQLCategoryType } from "@src/graphql.generated";
import { OnChangeField } from "@comet/admin";
-import { GQLCategoriesSelectQuery } from "./ProductForm.generated";
-import { GQLCategoriesSelectQueryVariables } from "./ProductForm.generated";
+import { GQLCategorySelectQuery } from "./ProductForm.generated";
+import { GQLCategorySelectQueryVariables } from "./ProductForm.generated";
import { AsyncSelectField } from "@comet/admin";
import { productFormFragment } from "./ProductForm.gql";
import { GQLProductFormFragment } from "./ProductForm.gql.generated";
@@ -443,9 +443,9 @@ id?: string;
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($type: CategoryType) {
+ query CategorySelect($type: CategoryType) {
categories(type: $type) {
nodes { id name }
}
@@ -508,8 +508,8 @@ import { InputAdornment } from "@mui/material";
import { FormApi } from "final-form";
import { useMemo } from "react";
import { OnChangeField } from "@comet/admin";
-import { GQLCategoriesSelectQuery } from "./ProductForm.generated";
-import { GQLCategoriesSelectQueryVariables } from "./ProductForm.generated";
+import { GQLCategorySelectQuery } from "./ProductForm.generated";
+import { GQLCategorySelectQueryVariables } from "./ProductForm.generated";
import { AsyncSelectField } from "@comet/admin";
import { productFormFragment } from "./ProductForm.gql";
import { GQLProductFormFragment } from "./ProductForm.gql.generated";
@@ -642,9 +642,9 @@ id?: string;
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($filter: CategoryFilter) {
+ query CategorySelect($filter: CategoryFilter) {
categories(filter: $filter) {
nodes { id name }
}
@@ -708,8 +708,8 @@ import { FormApi } from "final-form";
import { useMemo } from "react";
import { GQLCategoryType } from "@src/graphql.generated";
import { OnChangeField } from "@comet/admin";
-import { GQLCategoriesSelectQuery } from "./ProductForm.generated";
-import { GQLCategoriesSelectQueryVariables } from "./ProductForm.generated";
+import { GQLCategorySelectQuery } from "./ProductForm.generated";
+import { GQLCategorySelectQueryVariables } from "./ProductForm.generated";
import { AsyncSelectField } from "@comet/admin";
import { productFormFragment } from "./ProductForm.gql";
import { GQLProductFormFragment } from "./ProductForm.gql.generated";
@@ -842,9 +842,9 @@ id?: string;
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect($filter: CategoryFilter) {
+ query CategorySelect($filter: CategoryFilter) {
categories(filter: $filter) {
nodes { id name }
}
diff --git a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectRenderedProps.test.ts.snap b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectRenderedProps.test.ts.snap
index 17b4baf5065..dd549d60d55 100644
--- a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectRenderedProps.test.ts.snap
+++ b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/__snapshots__/asyncSelectRenderedProps.test.ts.snap
@@ -11,9 +11,9 @@ exports[`AsyncSelect rendered props > generates readonly async select 1`] = `
label={}
loadOptions={async () => {
- const { data } = await client.query({
+ const { data } = await client.query({
query: gql\`
- query CategoriesSelect {
+ query CategorySelect {
categories {
nodes { id name }
}
diff --git a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/asyncSelect.test.ts b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/asyncSelect.test.ts
index 58126bf9cb5..a9f9a7fcdae 100644
--- a/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/asyncSelect.test.ts
+++ b/packages/admin/admin-generator/src/commands/generate/generateForm/__tests__/asyncSelect.test.ts
@@ -477,4 +477,232 @@ describe("AsyncSelect filter", () => {
expect(formOutput.code).toMatchSnapshot();
});
});
+
+ it("uses field name for query name by default (multiple fields with same rootQuery)", async () => {
+ const schema = buildSchema(`
+ type Query {
+ people(search: String): PeopleConnection!
+ }
+
+ type PeopleConnection {
+ nodes: [Person!]!
+ }
+
+ type Visit {
+ id: ID!
+ host: Person!
+ guest: Person!
+ }
+ type Person {
+ id: ID!
+ name: String!
+ }
+
+ type Mutation {
+ createVisit(input: VisitInput!): Visit!
+ updateVisit(id: ID!, input: VisitInput!): Visit!
+ }
+
+ input VisitInput {
+ host: ID
+ guest: ID
+ }
+ `);
+ type GQLPerson = {
+ __typename?: "Person";
+ id: string;
+ name: string;
+ };
+ type GQLVisit = {
+ __typename?: "Visit";
+ id: string;
+ host: GQLPerson;
+ guest: GQLPerson;
+ };
+
+ const introspection = introspectionFromSchema(schema);
+
+ const hostFieldConfig: FormFieldConfig = {
+ type: "asyncSelect",
+ rootQuery: "people",
+ name: "host",
+ };
+ const guestFieldConfig: FormFieldConfig = {
+ type: "asyncSelect",
+ rootQuery: "people",
+ name: "guest",
+ };
+ const formConfig: FormConfig = {
+ type: "form",
+ gqlType: "Visit",
+ fields: [hostFieldConfig, guestFieldConfig],
+ };
+
+ const hostOutput = generateFormField({
+ gqlIntrospection: introspection,
+ baseOutputFilename: "VisitForm",
+ formFragmentName: "VisitFormFragment",
+ config: hostFieldConfig,
+ formConfig,
+ gqlType: "Visit",
+ });
+ const guestOutput = generateFormField({
+ gqlIntrospection: introspection,
+ baseOutputFilename: "VisitForm",
+ formFragmentName: "VisitFormFragment",
+ config: guestFieldConfig,
+ formConfig,
+ gqlType: "Visit",
+ });
+
+ // Each field gets a unique query name derived from the field name, not rootQuery
+ expect(hostOutput.code).toContain("HostSelect");
+ expect(hostOutput.code).not.toContain("PeopleSelect");
+ expect(guestOutput.code).toContain("GuestSelect");
+ expect(guestOutput.code).not.toContain("PeopleSelect");
+ });
+
+ it("uses custom queryName when provided", async () => {
+ const schema = buildSchema(`
+ type Query {
+ people(search: String): PeopleConnection!
+ }
+
+ type PeopleConnection {
+ nodes: [Person!]!
+ }
+
+ type Visit {
+ id: ID!
+ host: Person!
+ guest: Person!
+ }
+ type Person {
+ id: ID!
+ name: String!
+ }
+
+ type Mutation {
+ createVisit(input: VisitInput!): Visit!
+ updateVisit(id: ID!, input: VisitInput!): Visit!
+ }
+
+ input VisitInput {
+ host: ID
+ guest: ID
+ }
+ `);
+ type GQLPerson = {
+ __typename?: "Person";
+ id: string;
+ name: string;
+ };
+ type GQLVisit = {
+ __typename?: "Visit";
+ id: string;
+ host: GQLPerson;
+ guest: GQLPerson;
+ };
+
+ const introspection = introspectionFromSchema(schema);
+
+ const hostFieldConfig: FormFieldConfig = {
+ type: "asyncSelect",
+ rootQuery: "people",
+ queryName: "VisitHostSelect",
+ name: "host",
+ };
+
+ const formConfig: FormConfig = {
+ type: "form",
+ gqlType: "Visit",
+ fields: [hostFieldConfig],
+ };
+
+ const hostOutput = generateFormField({
+ gqlIntrospection: introspection,
+ baseOutputFilename: "VisitForm",
+ formFragmentName: "VisitFormFragment",
+ config: hostFieldConfig,
+ formConfig,
+ gqlType: "Visit",
+ });
+
+ // Custom queryName overrides the default field-name-based name
+ expect(hostOutput.code).toContain("VisitHostSelect");
+ expect(hostOutput.code).not.toContain("query HostSelect");
+ });
+
+ it("throws error for empty queryName", () => {
+ const schema = buildSchema(`
+ type Query {
+ people: PeopleConnection!
+ }
+ type PeopleConnection { nodes: [Person!]! }
+ type Visit { id: ID! host: Person! }
+ type Person { id: ID! name: String! }
+ type Mutation {
+ createVisit(input: VisitInput!): Visit!
+ updateVisit(id: ID!, input: VisitInput!): Visit!
+ }
+ input VisitInput { host: ID }
+ `);
+ type GQLPerson = { __typename?: "Person"; id: string; name: string };
+ type GQLVisit = { __typename?: "Visit"; id: string; host: GQLPerson };
+ const introspection = introspectionFromSchema(schema);
+ const fieldConfig: FormFieldConfig = {
+ type: "asyncSelect",
+ rootQuery: "people",
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ queryName: "" as any,
+ name: "host",
+ };
+ const formConfig: FormConfig = { type: "form", gqlType: "Visit", fields: [fieldConfig] };
+ expect(() =>
+ generateFormField({
+ gqlIntrospection: introspection,
+ baseOutputFilename: "VisitForm",
+ formFragmentName: "VisitFormFragment",
+ config: fieldConfig,
+ formConfig,
+ gqlType: "Visit",
+ }),
+ ).toThrow(/queryName/);
+ });
+
+ it("throws error for queryName with invalid characters", () => {
+ const schema = buildSchema(`
+ type Query {
+ people: PeopleConnection!
+ }
+ type PeopleConnection { nodes: [Person!]! }
+ type Visit { id: ID! host: Person! }
+ type Person { id: ID! name: String! }
+ type Mutation {
+ createVisit(input: VisitInput!): Visit!
+ updateVisit(id: ID!, input: VisitInput!): Visit!
+ }
+ input VisitInput { host: ID }
+ `);
+ type GQLPerson = { __typename?: "Person"; id: string; name: string };
+ type GQLVisit = { __typename?: "Visit"; id: string; host: GQLPerson };
+ const introspection = introspectionFromSchema(schema);
+ const fieldConfig: FormFieldConfig = {
+ type: "asyncSelect",
+ rootQuery: "people",
+ queryName: "Invalid Name!",
+ name: "host",
+ };
+ const formConfig: FormConfig = { type: "form", gqlType: "Visit", fields: [fieldConfig] };
+ expect(() =>
+ generateFormField({
+ gqlIntrospection: introspection,
+ baseOutputFilename: "VisitForm",
+ formFragmentName: "VisitFormFragment",
+ config: fieldConfig,
+ formConfig,
+ gqlType: "Visit",
+ }),
+ ).toThrow(/queryName/);
+ });
});
diff --git a/packages/admin/admin-generator/src/commands/generate/generateForm/asyncSelect/generateAsyncSelect.ts b/packages/admin/admin-generator/src/commands/generate/generateForm/asyncSelect/generateAsyncSelect.ts
index 9910fa46886..5faa852c6b7 100644
--- a/packages/admin/admin-generator/src/commands/generate/generateForm/asyncSelect/generateAsyncSelect.ts
+++ b/packages/admin/admin-generator/src/commands/generate/generateForm/asyncSelect/generateAsyncSelect.ts
@@ -5,6 +5,7 @@ import { findQueryTypeOrThrow } from "../../utils/findQueryType";
import { generateGqlOperation } from "../../utils/generateGqlOperation";
import { type Imports } from "../../utils/generateImportsCode";
import { isFieldOptional } from "../../utils/isFieldOptional";
+import { isValidGraphQLOperationName } from "../../utils/isValidGraphQLOperationName";
import { buildFormFieldOptions } from "../formField/options";
import { findFieldByName, type GenerateFieldsReturn } from "../generateFields";
import { type Prop } from "../generateForm";
@@ -189,7 +190,14 @@ export function generateAsyncSelect({
}
const rootQuery = config.rootQuery; //TODO we should infer a default value from the gql schema
- const queryName = `${rootQuery[0].toUpperCase() + rootQuery.substring(1)}Select`;
+ if (config.queryName !== undefined) {
+ if (!isValidGraphQLOperationName(config.queryName)) {
+ throw new Error(
+ `Field ${String(config.name)}: "queryName" must be a valid GraphQL operation name (non-empty, starting with a letter or underscore, containing only letters, digits, or underscores), got "${config.queryName}".`,
+ );
+ }
+ }
+ const queryName = config.queryName ?? `${name[0].toUpperCase() + name.substring(1)}Select`;
const rootQueryType = findQueryTypeOrThrow(rootQuery, gqlIntrospection);
let formFragmentFields: string[];
diff --git a/packages/admin/admin-generator/src/commands/generate/utils/isValidGraphQLOperationName.ts b/packages/admin/admin-generator/src/commands/generate/utils/isValidGraphQLOperationName.ts
new file mode 100644
index 00000000000..21ca3be55f9
--- /dev/null
+++ b/packages/admin/admin-generator/src/commands/generate/utils/isValidGraphQLOperationName.ts
@@ -0,0 +1,3 @@
+export function isValidGraphQLOperationName(name: string): boolean {
+ return name.length > 0 && /^[_A-Za-z][_0-9A-Za-z]*$/.test(name);
+}