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>
This commit is contained in:
hugocasa
2026-08-26 00:48:12 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 6b73145e72
commit 78331fda8b
3 changed files with 57 additions and 38 deletions
@@ -55,6 +55,19 @@
)
)
// Org names that appear on more than one installation, so the dropdown can
// tell those entries apart.
let duplicatedAccountIds = $derived(
new Set(
githubState.workspaceGithubInstallations
.filter(
(installation, _, array) =>
array.filter((other) => other.account_id === installation.account_id).length > 1
)
.map((installation) => installation.account_id)
)
)
let showGitHubApp = $derived(
resourceType === 'git_repository' &&
$workspaceStore &&
@@ -205,29 +218,47 @@
<div class="flex flex-row gap-2 w-full">
<div class="flex flex-col gap-1 flex-1">
<p class="text-sm font-semibold text-secondary">GitHub Account ID</p>
<select bind:value={githubState.selectedGHAppAccountId}>
<option value="" disabled>Select GitHub Account ID</option>
<select
bind:value={githubState.selectedGHAppInstallationId}
onchange={() => (githubState.selectedGHAppRepository = undefined)}
>
<option value={undefined} disabled>Select GitHub Account ID</option>
{#each githubState.workspaceGithubInstallations as installation (`select-${installation.installation_id}-${installation.workspace_id}`)}
<option value={installation.account_id} disabled={!!installation.error}>
{installation.account_id}{installation.error ? ' (token error)' : ''}
{@const details = [
duplicatedAccountIds.has(installation.account_id)
? `${installation.installation_id}`
: undefined,
installation.error ? 'token error' : undefined
].filter(Boolean)}
<option
value={installation.installation_id}
disabled={!!installation.error}
>
{installation.account_id}{details.length
? ` (${details.join(', ')})`
: ''}
</option>
{/each}
</select>
</div>
{#if githubState.selectedGHAppAccountId}
{#if githubState.selectedGHAppInstallationId !== undefined}
{@const selectedInstallation = githubState.workspaceGithubInstallations.find(
(inst) => inst.account_id === githubState.selectedGHAppAccountId
(inst) => inst.installation_id === githubState.selectedGHAppInstallationId
)}
{#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}
/>
<!-- RepositorySelector snapshots its repositories and page cursor at
mount, so switching installation has to remount it. -->
{#key selectedInstallation.installation_id}
<RepositorySelector
bind:selectedRepository={githubState.selectedGHAppRepository}
installationId={selectedInstallation.installation_id}
initialRepositories={selectedInstallation.repositories}
totalCount={selectedInstallation.total_count}
perPage={selectedInstallation.per_page}
/>
{/key}
</div>
{/if}
{/if}
@@ -11,7 +11,7 @@
interface Props {
disabled?: boolean
selectedRepository?: string | undefined
accountId: string
installationId: number
initialRepositories: Repository[]
totalCount: number
perPage: number
@@ -23,7 +23,7 @@
let {
disabled = false,
selectedRepository = $bindable(),
accountId,
installationId,
initialRepositories,
totalCount,
perPage,
@@ -67,8 +67,8 @@
page: nextPage
})
// Find the matching installation and get its repositories
const installation = installations.find((inst) => inst.account_id === accountId)
// 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
@@ -95,8 +95,8 @@
}))}
placeholder="Select repository..."
clearable
disabled={disabled}
bind:filterText={filterText}
{disabled}
bind:filterText
bind:value={selectedRepository}
/>
{#if hasMoreRepos}
+7 -19
View File
@@ -10,7 +10,12 @@ export interface GitHubAppState {
loadingGithubInstallations: boolean
githubInstallations: GetGlobalConnectedRepositoriesResponse
workspaceGithubInstallations: GetGlobalConnectedRepositoriesResponse
selectedGHAppAccountId: string | undefined
/**
* Installations are keyed by `installation_id`, never by `account_id`: an org
* can be installed more than once (re-installed, or added from another
* workspace), and matching on the org name resolves to the wrong one.
*/
selectedGHAppInstallationId: number | undefined
selectedGHAppRepository: string | undefined
githubInstallationUrl: string | undefined
installationCheckInterval: number | undefined
@@ -25,11 +30,6 @@ export interface GitHubAppState {
isGhesSelfManaged: boolean
}
export interface GitHubRepository {
name: string
url: string
}
export interface GitHubAppError extends Error {
code: 'VALIDATION_ERROR' | 'NETWORK_ERROR' | 'AUTH_ERROR' | 'UNKNOWN_ERROR'
details?: unknown
@@ -101,7 +101,7 @@ export function createGitHubAppState(): GitHubAppState {
loadingGithubInstallations: false,
githubInstallations: [],
workspaceGithubInstallations: [],
selectedGHAppAccountId: undefined,
selectedGHAppInstallationId: undefined,
selectedGHAppRepository: undefined,
githubInstallationUrl: undefined,
installationCheckInterval: undefined,
@@ -241,18 +241,6 @@ export function stopInstallationCheck(state: GitHubAppState): void {
state.isCheckingInstallation = false
}
/**
* Gets repositories for a specific GitHub account
*/
export function getRepositories(state: GitHubAppState, accountId: string): GitHubRepository[] {
if (!accountId) return []
return (
state.githubInstallations.find((installation) => installation.account_id === accountId)
?.repositories || []
)
}
/**
* Adds a GitHub installation to the current workspace
*/