feat(select-menu): add optionMeta slot and show repo details in create dialog

Add an optional optionMeta snippet to SelectMenu to render richer,
right-aligned content per option (falls back to option.meta when not
provided). Update SelectMenu markup to render optionMeta if present.

Style adjustments for .select-menu-option-meta to align items, allow
inline SVGs, and preserve spacing/coloring.

Use the new slot in CreateReviewDialog:
- supply grouped repository options (owner + name) instead of raw names
- show a branch icon for each option and a lock icon + last-updated date
  in the option meta
- add helpers repositoryById and formatUpdatedAt to resolve repository
  data for the meta snippet

No breaking changes: legacy option.meta still works when optionMeta is
not supplied.
This commit is contained in:
2026-09-17 23:12:05 +02:00
parent af1c70d45d
commit b21fc93e10
3 changed files with 41 additions and 3 deletions
+29 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import SelectMenu from "./SelectMenu.svelte";
import { onMount } from "svelte";
import { GitPullRequest, X, LoaderCircle, ArrowRight, Sparkles } from "@lucide/svelte";
import { GitBranch, GitPullRequest, LockKeyhole, X, LoaderCircle, ArrowRight, Sparkles } from "@lucide/svelte";
import { credLoad, pullRequestAiGenerate, createIntegrationReviewRequest, listIntegrationRepositories, listIntegrationRepositoryBranches, listRemotes, listBranches } from "../git";
import { integrationCredentialKey } from "../integrations";
import type { AiSettings, GitIntegrationSource, GitIntegrationRepository, IntegrationReviewRequest, StoredCredential } from "../types";
@@ -15,6 +15,26 @@
let dialog: HTMLDialogElement;
let titleInput: HTMLInputElement;
let repositories = $state<GitIntegrationRepository[]>([]);
/** Repository options grouped by owner, like the clone dialog's list. */
const repositoryOptions = $derived(repositories.map((repository) => {
const separator = repository.fullName.lastIndexOf("/");
return {
value: repository.id,
label: separator > 0 ? repository.fullName.slice(separator + 1) : repository.fullName,
group: separator > 0 ? repository.fullName.slice(0, separator) : source.label,
};
}));
function repositoryById(id: string): GitIntegrationRepository | undefined {
return repositories.find((repository) => repository.id === id);
}
function formatUpdatedAt(value: string): string {
if (!value) return "";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? "" : new Intl.DateTimeFormat(de ? "de-DE" : "en-US", { dateStyle: "medium" }).format(date);
}
let repositoryId = $state("");
let sourceBranch = $state("");
let targetBranch = $state("");
@@ -129,7 +149,14 @@
<div class="body">
{#if error}<div class="error" role="alert">{error}{#if !repositories.length && !loading}<button type="button" onclick={loadRepositories}>{de ? "Erneut laden" : "Retry"}</button>{/if}</div>{/if}
<div class="repository-field"><div class="field-heading"><span>Repository</span><small>{loading ? "…" : `${repositories.length} ${de ? "verfügbar" : "available"}`}</small></div>
<SelectMenu value={repositoryId} options={repositories.map(repository => ({value:repository.id,label:repository.fullName}))} disabled={loading || busy} ariaLabel="Repository" placeholder={loading ? (de ? "Repositories werden geladen …" : "Loading repositories…") : (de ? "Repository auswählen" : "Select repository")} searchable searchPlaceholder={de ? "Repositories durchsuchen …" : "Search repositories…"} emptyText={de ? "Keine passenden Repositories" : "No matching repositories"} onChange={value => repositoryId = value}/>
<SelectMenu value={repositoryId} options={repositoryOptions} disabled={loading || busy} ariaLabel="Repository" placeholder={loading ? (de ? "Repositories werden geladen …" : "Loading repositories…") : (de ? "Repository auswählen" : "Select repository")} searchable searchPlaceholder={de ? "Repositories durchsuchen …" : "Search repositories…"} emptyText={de ? "Keine passenden Repositories" : "No matching repositories"} showSelectedGroup onChange={value => repositoryId = value}>
{#snippet optionIcon()}<GitBranch size={14} aria-hidden="true" />{/snippet}
{#snippet optionMeta(option)}
{@const repository = repositoryById(option.value)}
{#if repository?.private}<LockKeyhole size={11} aria-label={de ? "Privat" : "Private"} />{/if}
{formatUpdatedAt(repository?.updatedAt ?? "")}
{/snippet}
</SelectMenu>
</div>
{#if !loading && !error && !repositories.length}<p>{de ? "Keine Repositories für diese Integration gefunden." : "No repositories found for this integration."}</p>{/if}
<div class="branches">
+8 -1
View File
@@ -25,6 +25,8 @@
emptyText?: string;
/** Optional icon in front of every option, e.g. a branch or repository mark. */
optionIcon?: Snippet<[SelectMenuOption]>;
/** Optional right-aligned content per option, richer than `option.meta`. */
optionMeta?: Snippet<[SelectMenuOption]>;
onChange: (value: string) => void;
}
@@ -40,6 +42,7 @@
searchPlaceholder = "Search…",
emptyText = "No results",
optionIcon = undefined,
optionMeta = undefined,
onChange,
}: Props = $props();
@@ -195,7 +198,11 @@
>
{#if optionIcon}<span class="select-menu-option-icon">{@render optionIcon(option)}</span>{/if}
<span class="select-menu-option-label">{option.label}</span>
{#if option.meta}<small class="select-menu-option-meta">{option.meta}</small>{/if}
{#if optionMeta}
<small class="select-menu-option-meta">{@render optionMeta(option)}</small>
{:else if option.meta}
<small class="select-menu-option-meta">{option.meta}</small>
{/if}
{#if option.value === value}<Check size={14} aria-hidden="true" />{/if}
</button>
{/each}