@@ -126,6 +126,11 @@ export interface ResolvedResource {
126126
127127export type ResourceNameMap = Map < string , ResolvedResource > ;
128128
129+ export interface ResourcePage {
130+ resources : ResolvedResource [ ] ;
131+ hasMore : boolean ;
132+ }
133+
129134function idOnlyMap ( ids : string [ ] ) : ResourceNameMap {
130135 const map : ResourceNameMap = new Map ( ) ;
131136 for ( const id of ids ) {
@@ -205,68 +210,97 @@ export async function resolveOrganizationNames(ids: string[]): Promise<ResourceN
205210/* Type-to-search — narrowing a wildcard grant to specific resources */
206211/* -------------------------------------------------------------------------- */
207212
208- /** How many results a resource search returns. Keeps the dropdown bounded and
209- * sidesteps pagination — the user types to narrow rather than scrolling. */
210- const SEARCH_LIMIT = 8 ;
211213/** Cap on organizations swept per project search, to bound the request fan-out. */
212214const SEARCH_ORG_SCAN = 20 ;
213215
214216/**
215217 * Search the user's projects by name for the resource picker. Projects are only
216218 * reachable through their owning organization, so we sweep the user's orgs and
217- * ask each for name matches, merge, and cap. An empty term returns the first
218- * projects found ( so the picker can show something before the user types) .
219- * Never throws: returns an empty list on failure.
219+ * ask each for name matches. Results are exposed as a global paginated list
220+ * across those organizations so the picker can load more as the user scrolls .
221+ * Never throws: returns an empty page on failure.
220222 */
221- export async function searchProjects ( term : string ) : Promise < ResolvedResource [ ] > {
223+ export async function searchProjects (
224+ term : string ,
225+ offset : number ,
226+ limit : number
227+ ) : Promise < ResourcePage > {
222228 try {
223229 const orgs = await getTeamOrOrganizationList ( [ Query . limit ( SEARCH_ORG_SCAN ) ] ) ;
224230 const trimmed = term . trim ( ) ;
225- const results = await Promise . allSettled (
226- orgs . teams . map ( ( org ) => {
227- const queries = [
228- Query . select ( [ '$id' , 'name' , 'region' , 'teamId' ] ) ,
229- Query . limit ( SEARCH_LIMIT )
230- ] ;
231- if ( trimmed !== '' ) {
232- queries . unshift ( Query . startsWith ( 'name' , trimmed ) ) ;
233- }
234- return sdk . forConsole . organization ( org . $id ) . listProjects ( { queries } ) ;
235- } )
236- ) ;
231+ const resources : ResolvedResource [ ] = [ ] ;
232+ let skip = offset ;
237233
238- const seen = new Set < string > ( ) ;
239- const out : ResolvedResource [ ] = [ ] ;
240- for ( const result of results ) {
241- if ( result . status !== 'fulfilled' ) continue ;
242- for ( const project of result . value . projects ) {
243- if ( project . $id === RESERVED_CONSOLE_PROJECT || seen . has ( project . $id ) ) continue ;
244- seen . add ( project . $id ) ;
245- out . push ( {
234+ for ( const org of orgs . teams ) {
235+ const pageLimit = limit + 1 - resources . length ;
236+ const queries = [
237+ Query . notEqual ( '$id' , RESERVED_CONSOLE_PROJECT ) ,
238+ Query . offset ( skip ) ,
239+ Query . limit ( pageLimit ) ,
240+ Query . select ( [ '$id' , 'name' , 'region' , 'teamId' ] )
241+ ] ;
242+ if ( trimmed !== '' ) {
243+ queries . unshift ( Query . startsWith ( 'name' , trimmed ) ) ;
244+ }
245+
246+ const result = await sdk . forConsole
247+ . organization ( org . $id )
248+ . listProjects ( { queries } )
249+ . catch ( ( ) => null ) ;
250+ if ( ! result ) continue ;
251+
252+ if ( skip >= result . total ) {
253+ skip -= result . total ;
254+ continue ;
255+ }
256+ skip = 0 ;
257+
258+ for ( const project of result . projects ) {
259+ resources . push ( {
246260 id : project . $id ,
247261 name : project . name ,
248262 region : project . region ,
249263 resolved : true
250264 } ) ;
251- if ( out . length >= SEARCH_LIMIT ) return out ;
252265 }
266+ if ( resources . length > limit ) break ;
253267 }
254- return out ;
268+
269+ return {
270+ resources : resources . slice ( 0 , limit ) ,
271+ hasMore : resources . length > limit
272+ } ;
255273 } catch {
256- return [ ] ;
274+ return { resources : [ ] , hasMore : false } ;
257275 }
258276}
259277
260278/**
261- * Load the user's organizations as resources for the org picker. Organizations
262- * are few enough to load once and filter client-side, so this returns the full
263- * list; the picker searches within it. Never throws.
279+ * Search the user's organizations by name using offset pagination. Never
280+ * throws: returns an empty page on failure.
264281 */
265- export async function listOrganizationResources ( ) : Promise < ResolvedResource [ ] > {
282+ export async function searchOrganizations (
283+ term : string ,
284+ offset : number ,
285+ limit : number
286+ ) : Promise < ResourcePage > {
266287 try {
267- const orgs = await getTeamOrOrganizationList ( [ Query . limit ( 100 ) ] ) ;
268- return orgs . teams . map ( ( org ) => ( { id : org . $id , name : org . name , resolved : true } ) ) ;
288+ const queries = [ Query . offset ( offset ) , Query . limit ( limit ) ] ;
289+ const trimmed = term . trim ( ) ;
290+ // startsWith over search: search is full-text (word-boundary) matching,
291+ // which drops partial-fragment matches; this mirrors the project search.
292+ if ( trimmed !== '' ) queries . push ( Query . startsWith ( 'name' , trimmed ) ) ;
293+
294+ const orgs = await getTeamOrOrganizationList ( queries ) ;
295+ return {
296+ resources : orgs . teams . map ( ( org ) => ( {
297+ id : org . $id ,
298+ name : org . name ,
299+ resolved : true
300+ } ) ) ,
301+ hasMore : offset + orgs . teams . length < orgs . total
302+ } ;
269303 } catch {
270- return [ ] ;
304+ return { resources : [ ] , hasMore : false } ;
271305 }
272306}
0 commit comments