11import type {
2+ IntegrationAccount ,
23 SourceConfig ,
34 SourceFieldConfig ,
45 SourceFieldInputConfig ,
6+ SourceFieldOauthAccountSelectConfig ,
57 SourceFieldOauthConfig ,
68} from "@posthog/api-client/posthog-client" ;
79import { useHostTRPC } from "@posthog/host-router/react" ;
@@ -98,6 +100,14 @@ function missingRequiredFields(
98100 if ( field . required && ! values [ field . name ] ) {
99101 missing . push ( field . name ) ;
100102 }
103+ } else if ( field . type === "oauth-account-select" ) {
104+ const value = values [ field . name ] ;
105+ if (
106+ field . required &&
107+ ( typeof value !== "string" || value . trim ( ) . length === 0 )
108+ ) {
109+ missing . push ( field . name ) ;
110+ }
101111 } else if ( isInputField ( field ) && field . required ) {
102112 const value = values [ field . name ] ;
103113 if ( typeof value !== "string" || value . trim ( ) . length === 0 ) {
@@ -134,6 +144,11 @@ function buildPayload(
134144 } else if ( field . type === "oauth" ) {
135145 const value = values [ field . name ] ;
136146 if ( value !== undefined && value !== "" ) out [ field . name ] = value ;
147+ } else if ( field . type === "oauth-account-select" ) {
148+ const value = values [ field . name ] ;
149+ if ( typeof value === "string" && value . trim ( ) !== "" ) {
150+ out [ field . name ] = value . trim ( ) ;
151+ }
137152 } else if ( isInputField ( field ) ) {
138153 const value = values [ field . name ] ;
139154 if ( typeof value === "string" ) out [ field . name ] = value . trim ( ) ;
@@ -219,6 +234,7 @@ export function DynamicSourceSetup({
219234 values = { values }
220235 setValue = { setValue }
221236 providerName = { title . replace ( / ^ C o n n e c t \s + / i, "" ) }
237+ sourceType = { sourceType }
222238 />
223239 ) ) }
224240 { hasUnsupportedField && (
@@ -257,11 +273,13 @@ function SourceField({
257273 values,
258274 setValue,
259275 providerName,
276+ sourceType,
260277} : {
261278 field : SourceFieldConfig ;
262279 values : FieldValues ;
263280 setValue : ( name : string , value : FieldValue ) => void ;
264281 providerName : string ;
282+ sourceType : string ;
265283} ) {
266284 if ( field . type === "switch-group" ) {
267285 const enabled = ! ! values [ field . name ] ;
@@ -285,6 +303,7 @@ function SourceField({
285303 values = { values }
286304 setValue = { setValue }
287305 providerName = { providerName }
306+ sourceType = { sourceType }
288307 />
289308 ) ) }
290309 </ Flex >
@@ -302,6 +321,18 @@ function SourceField({
302321 ) ;
303322 }
304323
324+ if ( field . type === "oauth-account-select" ) {
325+ return (
326+ < AccountSelectField
327+ field = { field }
328+ value = { values [ field . name ] }
329+ setValue = { setValue }
330+ sourceType = { sourceType }
331+ integrationId = { values [ field . integrationField ] }
332+ />
333+ ) ;
334+ }
335+
305336 if ( field . type === "select" ) {
306337 const selected = ( values [ field . name ] as string ) ?? field . defaultValue ?? "" ;
307338 const option = field . options . find ( ( o ) => o . value === selected ) ;
@@ -328,6 +359,7 @@ function SourceField({
328359 values = { values }
329360 setValue = { setValue }
330361 providerName = { providerName }
362+ sourceType = { sourceType }
331363 />
332364 ) ) }
333365 </ Flex >
@@ -484,6 +516,141 @@ function OAuthSourceField({
484516 ) ;
485517}
486518
519+ /**
520+ * Renders an `oauth-account-select` field: a searchable picker whose options are the accounts/
521+ * resources a connected OAuth integration exposes (e.g. GitHub repositories), fetched from the
522+ * backend using the integration's server-side token (the client only passes the integration id).
523+ * Search is server-side (debounced) so large lists work. Falls back to a free-text input until a
524+ * valid integration id is present in the form.
525+ */
526+ function AccountSelectField ( {
527+ field,
528+ value,
529+ setValue,
530+ sourceType,
531+ integrationId,
532+ } : {
533+ field : SourceFieldOauthAccountSelectConfig ;
534+ value : FieldValue | undefined ;
535+ setValue : ( name : string , value : FieldValue ) => void ;
536+ sourceType : string ;
537+ integrationId : FieldValue | undefined ;
538+ } ) {
539+ const projectId = useAuthStateValue ( ( state ) => state . currentProjectId ) ;
540+ const client = useAuthenticatedClient ( ) ;
541+ const [ query , setQuery ] = useState ( typeof value === "string" ? value : "" ) ;
542+ const [ accounts , setAccounts ] = useState < IntegrationAccount [ ] > ( [ ] ) ;
543+ const [ loading , setLoading ] = useState ( false ) ;
544+ const [ open , setOpen ] = useState ( false ) ;
545+
546+ const hasIntegration =
547+ integrationId !== undefined &&
548+ integrationId !== "" &&
549+ integrationId !== false ;
550+
551+ // Reset any prior selection when the backing integration changes. An account
552+ // chosen (or fallback text typed) for one integration must not survive into
553+ // another — otherwise the form could submit an account that was never
554+ // selected for the active integration.
555+ const prevIntegrationId = useRef ( integrationId ) ;
556+ useEffect ( ( ) => {
557+ if ( prevIntegrationId . current === integrationId ) return ;
558+ prevIntegrationId . current = integrationId ;
559+ setQuery ( "" ) ;
560+ setValue ( field . name , "" ) ;
561+ } , [ integrationId , field . name , setValue ] ) ;
562+
563+ useEffect ( ( ) => {
564+ if ( ! projectId || ! client || ! hasIntegration ) return ;
565+ let cancelled = false ;
566+ const handle = setTimeout ( async ( ) => {
567+ setLoading ( true ) ;
568+ try {
569+ const results = await client . getOauthAccounts (
570+ projectId ,
571+ sourceType ,
572+ integrationId as number | string ,
573+ query ,
574+ ) ;
575+ if ( ! cancelled ) setAccounts ( results ) ;
576+ } catch {
577+ if ( ! cancelled ) setAccounts ( [ ] ) ;
578+ } finally {
579+ if ( ! cancelled ) setLoading ( false ) ;
580+ }
581+ } , 300 ) ;
582+ return ( ) => {
583+ cancelled = true ;
584+ clearTimeout ( handle ) ;
585+ } ;
586+ } , [ projectId , client , sourceType , integrationId , hasIntegration , query ] ) ;
587+
588+ if ( ! hasIntegration ) {
589+ return (
590+ < Flex direction = "column" gap = "1" >
591+ < Text className = "text-gray-12 text-sm" > { field . label } </ Text >
592+ < TextField . Root
593+ placeholder = { field . placeholder || field . label }
594+ value = { typeof value === "string" ? value : "" }
595+ onChange = { ( e ) => setValue ( field . name , e . target . value ) }
596+ />
597+ { field . caption && (
598+ < Text className = "text-[13px] text-gray-11" > { field . caption } </ Text >
599+ ) }
600+ </ Flex >
601+ ) ;
602+ }
603+
604+ return (
605+ < Flex direction = "column" gap = "1" >
606+ < Text className = "text-gray-12 text-sm" > { field . label } </ Text >
607+ < TextField . Root
608+ placeholder = { field . placeholder || field . label }
609+ value = { query }
610+ onChange = { ( e ) => {
611+ // Typing only filters the list; it does not commit a value. The
612+ // submitted account is set solely by picking an option, so editing
613+ // the text after a selection clears it rather than silently mutating
614+ // the underlying (possibly opaque) account id.
615+ setQuery ( e . target . value ) ;
616+ setValue ( field . name , "" ) ;
617+ setOpen ( true ) ;
618+ } }
619+ onFocus = { ( ) => setOpen ( true ) }
620+ />
621+ { open && ( loading || accounts . length > 0 ) && (
622+ < Box className = "max-h-48 overflow-y-auto rounded-(--radius-2) border border-border bg-(--color-panel-solid)" >
623+ { loading ? (
624+ < Text className = "block px-2 py-1 text-[13px] text-gray-11" >
625+ Loading…
626+ </ Text >
627+ ) : (
628+ accounts . map ( ( account ) => (
629+ < button
630+ key = { account . value }
631+ type = "button"
632+ className = "block w-full px-2 py-1 text-left text-gray-12 text-sm hover:bg-(--gray-3)"
633+ onClick = { ( ) => {
634+ // Commit the account's opaque value to the form, but show the
635+ // human-readable name in the input.
636+ setValue ( field . name , account . value ) ;
637+ setQuery ( account . display_name ) ;
638+ setOpen ( false ) ;
639+ } }
640+ >
641+ { account . display_name }
642+ </ button >
643+ ) )
644+ ) }
645+ </ Box >
646+ ) }
647+ { field . caption && (
648+ < Text className = "text-[13px] text-gray-11" > { field . caption } </ Text >
649+ ) }
650+ </ Flex >
651+ ) ;
652+ }
653+
487654function SetupFormContainer ( {
488655 title,
489656 children,
0 commit comments