feat: support search for gh repo when pagination needed (#6982)

* frontend changes gh app search repo

* feat: support search for gh repo when pagination needed

* backend

* ee repo ref
This commit is contained in:
Alexander Petric
2025-10-28 18:24:26 -04:00
committed by GitHub
parent e4a212ba82
commit e09a6b98ce
4 changed files with 167 additions and 12 deletions
+1 -1
View File
@@ -1 +1 @@
a81fe447c13c5dd59eb60594123f36984ccb7058
a2c83c51c9b501e3660fe82416a73e487356158d
+15
View File
@@ -603,6 +603,13 @@ paths:
operationId: getGlobalConnectedRepositories
tags:
- Git Sync
parameters:
- name: search
in: query
description: Search repositories by name
required: false
schema:
type: string
responses:
"200":
description: connected repositories
@@ -19537,10 +19544,18 @@ components:
required:
- name
- url
total_count:
type: number
description: Total number of repositories available for this installation
per_page:
type: number
description: Number of repositories loaded per page
required:
- installation_id
- account_id
- repositories
- total_count
- per_page
WorkspaceGithubInstallation:
type: object
@@ -9,7 +9,6 @@
loadGithubInstallations,
startInstallationCheck,
stopInstallationCheck,
getRepositories,
addInstallationToWorkspace,
deleteInstallation,
exportInstallation,
@@ -18,6 +17,7 @@
handleInstallClick,
type GitHubAppState
} from '$lib/githubApp'
import RepositorySelector from './RepositorySelector.svelte'
interface Props {
resourceType: string
@@ -215,17 +215,21 @@
</select>
</div>
{#if githubState.selectedGHAppAccountId}
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">Repository</p>
<div class="flex flex-row gap-2">
<select bind:value={githubState.selectedGHAppRepository}>
<option value="" disabled selected>Select Repository</option>
{#each getRepositories(githubState, githubState.selectedGHAppAccountId) as repository (repository.url)}
<option value={repository.url}>{repository.name}</option>
{/each}
</select>
{@const selectedInstallation = githubState.workspaceGithubInstallations.find(
(inst) => inst.account_id === githubState.selectedGHAppAccountId
)}
{#if selectedInstallation}
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">Repository</p>
<RepositorySelector
bind:selectedRepository={githubState.selectedGHAppRepository}
accountId={githubState.selectedGHAppAccountId}
initialRepositories={selectedInstallation.repositories}
totalCount={selectedInstallation.total_count}
perPage={selectedInstallation.per_page}
/>
</div>
</div>
{/if}
{/if}
<div class="pt-[26px]">
<Button
@@ -0,0 +1,136 @@
<script lang="ts">
import { GitSyncService } from '$lib/gen'
import Select from './select/Select.svelte'
import { debounce } from '$lib/utils'
interface Repository {
name: string
url: string
}
interface Props {
disabled?: boolean
selectedRepository?: string | undefined
accountId: string
initialRepositories: Repository[]
totalCount: number
perPage: number
containerClass?: string
minWidth?: string
onError?: (error: Error) => void
}
let {
disabled = false,
selectedRepository = $bindable(),
accountId,
initialRepositories,
totalCount,
perPage,
containerClass = 'flex-1',
minWidth = '160px',
onError
}: Props = $props()
let isFetching = $state(false)
let selectFilterText = $state('')
let lastSearchQuery = $state('')
// Use a derived value that always reflects current repos
let availableRepos = $derived(() => {
// If we have search results from backend, use those
// Otherwise use initial repositories
return searchResults.length > 0 ? searchResults : initialRepositories
})
// Track search results from backend
let searchResults = $state<Repository[]>([])
// Only enable search mode if total count exceeds per_page limit
const searchMode = $derived(totalCount > perPage)
// Debounced search function
const debouncedSearch = debounce(async (query: string) => {
await searchRepositories(query)
}, 500)
// Watch for filter text changes and trigger backend search
$effect(() => {
if (searchMode && selectFilterText !== undefined && selectFilterText !== lastSearchQuery) {
if (selectFilterText.length >= 1) {
// Only search backend if we have more repos than currently loaded
if (totalCount > initialRepositories.length) {
debouncedSearch.debounced(selectFilterText)
}
} else if (selectFilterText.length === 0) {
lastSearchQuery = ''
searchResults = []
}
}
})
async function searchRepositories(query: string) {
if (!query) {
searchResults = []
lastSearchQuery = ''
return
}
// Don't search if we already have all repos
if (totalCount <= initialRepositories.length) {
return
}
isFetching = true
try {
const installations = await GitSyncService.getGlobalConnectedRepositories({
search: query
})
// Find the matching installation and get its repositories
const installation = installations.find((inst) => inst.account_id === accountId)
searchResults = installation?.repositories || []
lastSearchQuery = query
isFetching = false
return searchResults
} catch (error) {
isFetching = false
onError?.(error)
console.error('Error searching repositories:', error)
searchResults = []
lastSearchQuery = ''
return []
}
}
</script>
<div class={containerClass}>
{#if searchMode}
<div class="flex flex-col gap-1">
<Select
containerStyle={'min-width: ' + minWidth}
items={availableRepos().map((repo) => ({
label: repo.name,
value: repo.url
}))}
placeholder={isFetching ? 'Searching...' : `Search all repositories...`}
clearable
disabled={disabled || isFetching}
bind:filterText={selectFilterText}
bind:value={selectedRepository}
/>
{#if totalCount > initialRepositories.length}
<span class="text-3xs pl-1 text-tertiary">
Loaded {initialRepositories.length} of {totalCount} repositories.
</span>
{/if}
</div>
{:else}
<select bind:value={selectedRepository} {disabled} class="w-full">
<option value="" disabled selected>Select repository</option>
{#each initialRepositories as repository (repository.url)}
<option value={repository.url}>{repository.name}</option>
{/each}
</select>
{/if}
</div>