From 80254d0d12b80ccbe675a19374c545558b123149 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 14 Apr 2026 14:27:48 +0200 Subject: [PATCH] ref(forms): Add customAsyncQueryOptions to BackendJsonSubmitForm Allow consumers to override the built-in async select query options on a per-field basis. This enables use cases where the async endpoint requires a different query shape than the default buildAsyncSelectQuery, such as the Sentry App external-requests endpoint. Co-Authored-By: Claude Opus 4.6 --- .../backendJsonSubmitForm.tsx | 88 +++++++++++-------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/static/app/components/backendJsonFormAdapter/backendJsonSubmitForm.tsx b/static/app/components/backendJsonFormAdapter/backendJsonSubmitForm.tsx index e4312b0ee33071..de35586d1ca4c4 100644 --- a/static/app/components/backendJsonFormAdapter/backendJsonSubmitForm.tsx +++ b/static/app/components/backendJsonFormAdapter/backendJsonSubmitForm.tsx @@ -36,6 +36,17 @@ interface BackendJsonSubmitFormProps { * resolves on success or rejects/throws on error. */ onSubmit: (values: Record) => Promise | void; + /** + * Override the built-in async query options for specific fields. Map from + * field name to a factory that returns query options for a given search input. + * When provided for a field, this is used instead of the default URL-based + * async loading. Useful when the async endpoint requires a different query + * shape than the built-in `buildAsyncSelectQuery`. + */ + customAsyncQueryOptions?: Record< + string, + (debouncedInput: string) => ReturnType + >; /** * Current values of dynamic fields, passed as query params to async select endpoints. */ @@ -158,6 +169,7 @@ export function BackendJsonSubmitForm({ dynamicFieldValues, onAsyncOptionsFetched, onFieldChange, + customAsyncQueryOptions, footer, }: BackendJsonSubmitFormProps) { // Ref to avoid including the callback in queryKey (would cause refetches) @@ -274,44 +286,50 @@ export function BackendJsonSubmitForm({ ); case 'select': case 'choice': { - if (field.url) { + if (field.url || customAsyncQueryOptions?.[field.name]) { // Async select: fetch options from URL as user types. // Show static choices as initial options before any search. const staticOptions = transformChoices(field.choices); - const asyncQueryOptions = (debouncedInput: string) => - queryOptions({ - queryKey: [ - 'backend-json-async-select', - field.name, - field.url, - debouncedInput, - dynamicFieldValues, - JSON.stringify(onAsyncOptionsFetchedRef), - ], - queryFn: async (): Promise< - Array> - > => { - if (!debouncedInput) { - return staticOptions; - } - const response = await API_CLIENT.requestPromise( - field.url!, - { - query: buildAsyncSelectQuery( - field.name, - debouncedInput, - dynamicFieldValues - ), - } - ); - // API may return non-array responses (e.g. error objects) - const results = Array.isArray(response) ? response : []; - if (results.length > 0) { - onAsyncOptionsFetchedRef.current?.(field.name, results); - } - return results; - }, - }); + const customQueryOptions = customAsyncQueryOptions?.[field.name]; + const asyncQueryOptions = customQueryOptions + ? customQueryOptions + : (debouncedInput: string) => + queryOptions({ + queryKey: [ + 'backend-json-async-select', + field.name, + field.url, + debouncedInput, + dynamicFieldValues, + JSON.stringify(onAsyncOptionsFetchedRef), + ], + queryFn: async (): Promise< + Array> + > => { + if (!debouncedInput) { + return staticOptions; + } + const response = await API_CLIENT.requestPromise( + field.url!, + { + query: buildAsyncSelectQuery( + field.name, + debouncedInput, + dynamicFieldValues + ), + } + ); + // API may return non-array responses (e.g. error objects) + const results = Array.isArray(response) ? response : []; + if (results.length > 0) { + onAsyncOptionsFetchedRef.current?.( + field.name, + results + ); + } + return results; + }, + }); if (field.multiple) { return (