Files
windmill/frontend/src/lib/components/RepositorySelector.svelte
T
hugocasaandClaude Opus 5 78331fda8b fix(frontend): key the GitHub App installation selector on installation_id (#10831)
* fix(frontend): key the GitHub App installation selector on installation_id

The GitHub Account ID dropdown used `account_id` as both the option value
and the lookup key. A workspace can hold several installations for the same
org (re-installed, or added from another workspace), so `.find()` resolved to
whichever came first: picking the live installation could hand back a stale,
token-errored one whose `repositories` are empty, leaving the repository
dropdown blank. `RepositorySelector`'s pagination matched the same way and
appended the wrong installation's page.

Both now key on `installation_id`, and the dropdown appends the installation
id to the label only for orgs that appear more than once. Switching
installation remounts `RepositorySelector` and clears the selected
repository, so its loaded pages no longer carry over.

Fixes WIN-2448

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontend): shorten the duplicate-org installation label

Drop the "installation" word from the disambiguating suffix: the id alone
already tells the two entries apart, and it keeps the errored variant short
enough to read at a glance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 00:48:12 +02:00

129 lines
3.4 KiB
Svelte

<script lang="ts">
import { untrack } from 'svelte'
import { GitSyncService } from '$lib/gen'
import Select from './select/Select.svelte'
interface Repository {
name: string
url: string
}
interface Props {
disabled?: boolean
selectedRepository?: string | undefined
installationId: number
initialRepositories: Repository[]
totalCount: number
perPage: number
containerClass?: string
minWidth?: string
onError?: (error: Error) => void
}
let {
disabled = false,
selectedRepository = $bindable(),
installationId,
initialRepositories,
totalCount,
perPage,
containerClass = 'flex-1',
minWidth = '160px',
onError
}: Props = $props()
// Track all loaded repositories across pages
let loadedRepositories = $state<Repository[]>(untrack(() => initialRepositories))
let currentPage = $state(1)
let isLoadingMore = $state(false)
// Client-side filtered repositories based on search text
let filterText = $state('')
const filteredRepositories = $derived(() => {
if (!filterText) {
return loadedRepositories
}
const search = filterText.toLowerCase()
return loadedRepositories.filter((repo) => repo.name.toLowerCase().includes(search))
})
// Check if there are more repos to load
const hasMoreRepos = $derived(loadedRepositories.length < totalCount)
// Show the select when we have more repos than the initial load
const showSearchableSelect = $derived(totalCount > perPage)
async function loadMoreRepositories() {
if (isLoadingMore || !hasMoreRepos) {
return
}
isLoadingMore = true
const nextPage = currentPage + 1
try {
const installations = await GitSyncService.getGlobalConnectedRepositories({
page: nextPage
})
// Match on installation_id: several installations can share one account_id
const installation = installations.find((inst) => inst.installation_id === installationId)
if (installation?.repositories) {
// Append new repos to existing ones
loadedRepositories = [...loadedRepositories, ...installation.repositories]
currentPage = nextPage
}
} catch (error) {
onError?.(error as Error)
console.error('Error loading more repositories:', error)
} finally {
isLoadingMore = false
}
}
</script>
<div class={containerClass}>
{#if showSearchableSelect}
<div class="flex flex-col gap-1">
<Select
containerStyle={'min-width: ' + minWidth}
items={filteredRepositories().map((repo) => ({
label: repo.name,
value: repo.url
}))}
placeholder="Select repository..."
clearable
{disabled}
bind:filterText
bind:value={selectedRepository}
/>
{#if hasMoreRepos}
<div class="flex items-center gap-2 pl-1">
<span class="text-2xs text-tertiary">
Loaded {loadedRepositories.length} of {totalCount}
</span>
{#if hasMoreRepos}
<button
type="button"
class="text-2xs text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
onclick={loadMoreRepositories}
disabled={isLoadingMore}
>
{isLoadingMore ? 'loading...' : 'load more...'}
</button>
{/if}
</div>
{/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>