Review center #35

Merged
Christoph merged 2 commits from review-center into main 2026-09-07 14:40:39 +00:00
3 changed files with 178 additions and 7 deletions
Showing only changes of commit c6553218a2 - Show all commits
+10 -2
View File
@@ -315,6 +315,8 @@
let unlistenStartupRepository: (() => void) | undefined;
let activeRepoPath = "";
let activeView: AppView = "management";
let reviewCenterInitialQuery = "";
let reviewCenterInitialSourceId = "";
let repoTabs: RepoTab[] = [];
let repoTabContextMenu: RepoTabContextMenu | null = null;
let recentRepoPaths: string[] = [];
@@ -2587,9 +2589,11 @@
void backgroundRepoStatusTick(false);
}
function openReviewCenter() {
function openReviewCenter(repository = "", sourceId = "") {
if (isBusy) return;
closeRepoTabContextMenu();
reviewCenterInitialQuery = repository;
reviewCenterInitialSourceId = sourceId;
activeView = "review-center";
trackEvent("review_center_opened", { integrations: Object.values(gitIntegrationSettings.providers).filter((provider) => provider.enabled && provider.tokenStored).length });
}
@@ -5178,7 +5182,7 @@
{isBusy}
language={appLanguage}
onOpenManagement={openRepoManagement}
onOpenReviewCenter={openReviewCenter}
onOpenReviewCenter={() => openReviewCenter()}
isActive={(path) => activeView === "repository" && sameRepoPath(activeRepoPath, path)}
onSelect={selectRepoTab}
onClose={closeRepoTab}
@@ -5343,15 +5347,19 @@
{#if activeView === "management"}
<RepositoryDashboard
repos={dashboardRepos} language={appLanguage} {isBusy}
integrations={gitIntegrationSettings} loadCredential={loadStoredCredential}
onOpen={selectRepoTab} onAdd={chooseRepositoryFolder} onClone={openCloneDialog}
onInit={initializeRepository}
onFavorite={toggleFavoriteRepo} onClose={closeDashboardRepository}
onRemoveRecent={removeRepoFromRecent}
onOpenPullRequests={(repository, sourceId) => openReviewCenter(repository, sourceId)}
/>
{:else if activeView === "review-center"}
<ReviewCenter
language={appLanguage}
integrations={gitIntegrationSettings}
initialQuery={reviewCenterInitialQuery}
initialSourceId={reviewCenterInitialSourceId}
loadCredential={loadStoredCredential}
onOpenSettings={() => { appSettingsOpen = true; }}
/>
+163 -3
View File
@@ -1,5 +1,8 @@
<script lang="ts">
import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, Circle, Download, FolderGit2, GitBranch, Plus, Search, Star, Trash2, X } from "@lucide/svelte";
import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, Circle, Download, FolderGit2, GitBranch, GitPullRequest, LoaderCircle, Plus, Search, Star, TriangleAlert, Trash2, X } from "@lucide/svelte";
import { listIntegrationReviewRequests, listRemotes } from "../git";
import { configuredIntegrationSources, integrationCredentialKey } from "../integrations";
import type { GitIntegrationSettings, GitIntegrationSource, IntegrationReviewRequest, StoredCredential } from "../types";
import SelectMenu from "./SelectMenu.svelte";
interface DashboardRepository {
@@ -17,6 +20,15 @@
}
interface Workspace { id: string; name: string; }
interface PullRequestBadgeState {
status: "loading" | "ready" | "error" | "unavailable";
count: number;
sourceId: string;
repository: string;
message: string;
}
interface RemoteIdentity { host: string; repository: string; organization: string; }
export let repos: DashboardRepository[] = [];
export let language: "de" | "en" = "en";
@@ -28,6 +40,9 @@
export let onInit: () => unknown;
export let onFavorite: (path: string) => unknown;
export let onRemoveRecent: (path: string) => unknown;
export let integrations: GitIntegrationSettings;
export let loadCredential: (key: string) => Promise<StoredCredential | null>;
export let onOpenPullRequests: (repository: string, sourceId: string) => unknown;
const STORAGE_KEY = "gitty.dashboard.v1";
let query = "";
@@ -40,6 +55,9 @@
let workspaceName = "";
let workspaceError = "";
let workspaceSelection = new Set<string>();
let pullRequestBadges: Record<string, PullRequestBadgeState> = {};
let pullRequestLoadGeneration = 0;
let lastPullRequestLoadKey = "";
$: de = language === "de";
$: normalizedQuery = query.trim().toLocaleLowerCase();
@@ -60,6 +78,15 @@
{ value: "", label: de ? "Alle Workspaces" : "All workspaces" },
...workspaces.map((workspace) => ({ value: workspace.id, label: workspace.name, group: "Workspaces" })),
];
$: integrationSources = configuredIntegrationSources(integrations);
$: pullRequestLoadKey = JSON.stringify({
repositories: repos.map((repo) => repo.path),
sources: integrationSources.map((source) => [source.id, source.baseUrl]),
});
$: if (pullRequestLoadKey !== lastPullRequestLoadKey) {
lastPullRequestLoadKey = pullRequestLoadKey;
void loadPullRequestBadges();
}
loadPreferences();
@@ -135,6 +162,135 @@
function closeCard(event: MouseEvent, path: string) { event.stopPropagation(); if (!isBusy) onClose(path); }
function favoriteCard(event: MouseEvent, path: string) { event.stopPropagation(); if (!isBusy) onFavorite(path); }
function removeRecentCard(event: MouseEvent, path: string) { event.stopPropagation(); if (!isBusy) onRemoveRecent(path); }
function openPullRequests(event: MouseEvent, repo: DashboardRepository) {
event.stopPropagation();
const badge = pullRequestBadges[repo.path];
if (!isBusy && badge?.status === "ready") onOpenPullRequests(badge.repository || repo.name, badge.sourceId);
}
function remoteIdentity(value: string): RemoteIdentity | null {
const trimmed = value.trim();
if (!trimmed) return null;
let host = "";
let path = "";
const scp = trimmed.match(/^[^@\s]+@([^:\s]+):(.+)$/);
if (scp) {
host = scp[1];
path = scp[2];
} else {
try {
const url = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
host = url.hostname;
path = url.pathname;
} catch {
return null;
}
}
const parts = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/").filter(Boolean);
const gitIndex = parts.findIndex((part) => part.toLocaleLowerCase() === "_git");
const normalizedHost = host.toLocaleLowerCase();
if (gitIndex > 0 && parts[gitIndex + 1]) return { host: normalizedHost, repository: `${parts[gitIndex - 1]}/${parts[gitIndex + 1]}`.toLocaleLowerCase(), organization: normalizedHost === "dev.azure.com" ? (parts[0] ?? "").toLocaleLowerCase() : "" };
if (parts[0]?.toLocaleLowerCase() === "v3" && parts.length >= 4) return { host: normalizedHost, repository: `${parts[2]}/${parts[3]}`.toLocaleLowerCase(), organization: parts[1].toLocaleLowerCase() };
return { host: normalizedHost, repository: parts.join("/").toLocaleLowerCase(), organization: "" };
}
function sourceMatchesRemote(source: GitIntegrationSource, identity: RemoteIdentity): boolean {
try {
const sourceUrl = new URL(source.baseUrl);
const sourceHost = sourceUrl.hostname.toLocaleLowerCase();
const remoteHost = identity.host;
if (source.provider === "azure-devops" ? sourceHost.replace(/^ssh\./, "") !== remoteHost.replace(/^ssh\./, "") : sourceHost !== remoteHost) return false;
if (source.provider !== "azure-devops") return true;
const organization = sourceUrl.hostname.toLocaleLowerCase() === "dev.azure.com"
? sourceUrl.pathname.split("/").filter(Boolean)[0]?.toLocaleLowerCase() ?? ""
: sourceUrl.hostname.split(".")[0]?.toLocaleLowerCase() ?? "";
return !organization || !identity.organization || organization === identity.organization;
} catch {
return false;
}
}
function requestMatchesRepository(request: IntegrationReviewRequest, repository: string): boolean {
const requestRepository = request.repositoryName.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").toLocaleLowerCase();
if (!requestRepository) return false;
if (requestRepository === repository || repository.endsWith(`/${requestRepository}`) || requestRepository.endsWith(`/${repository}`)) return true;
const requestParts = requestRepository.split("/");
const remoteParts = repository.split("/");
return requestParts[requestParts.length - 1] === remoteParts[remoteParts.length - 1];
}
function updatePullRequestBadge(path: string, state: PullRequestBadgeState, generation: number) {
if (generation !== pullRequestLoadGeneration) return;
pullRequestBadges = { ...pullRequestBadges, [path]: state };
}
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = window.setTimeout(() => reject(new Error(message)), timeoutMs);
promise.then((value) => { window.clearTimeout(timer); resolve(value); }, (error) => { window.clearTimeout(timer); reject(error); });
});
}
async function loadPullRequestBadges() {
const generation = ++pullRequestLoadGeneration;
const sources = integrationSources;
pullRequestBadges = Object.fromEntries(repos.map((repo) => [repo.path, {
status: sources.length ? "loading" : "unavailable",
count: 0,
sourceId: "",
repository: "",
message: sources.length ? "" : (de ? "Keine passende Integration eingerichtet." : "No matching integration configured."),
} satisfies PullRequestBadgeState]));
if (!repos.length || !sources.length) return;
const grouped = new Map<string, Array<{ repo: DashboardRepository; identity: RemoteIdentity }>>();
await Promise.all(repos.map(async (repo) => {
try {
const remotes = await listRemotes(repo.path);
const identities = remotes
.sort((left, right) => Number(right.name === "origin") - Number(left.name === "origin"))
.map((remote) => remoteIdentity(remote.fetch_url))
.filter((identity): identity is RemoteIdentity => Boolean(identity));
const match = identities.map((identity) => ({ identity, source: sources.find((source) => sourceMatchesRemote(source, identity)) })).find((item) => item.source);
if (!match?.source) {
updatePullRequestBadge(repo.path, { status: "unavailable", count: 0, sourceId: "", repository: "", message: de ? "Keine passende Integration für das Remote gefunden." : "No matching integration for this remote." }, generation);
return;
}
grouped.set(match.source.id, [...(grouped.get(match.source.id) ?? []), { repo, identity: match.identity }]);
} catch (error) {
updatePullRequestBadge(repo.path, { status: "error", count: 0, sourceId: "", repository: "", message: error instanceof Error ? error.message : String(error) }, generation);
}
}));
await Promise.all([...grouped.entries()].map(async ([sourceId, entries]) => {
const source = sources.find((candidate) => candidate.id === sourceId);
if (!source) return;
try {
const credential = await withTimeout(loadCredential(integrationCredentialKey(source.provider, source.accountId)), 15_000, de ? "Der System-Schlüsselbund hat nicht geantwortet." : "The system keychain did not respond.");
if (!credential?.password) throw new Error(de ? "Kein Integrationstoken gespeichert." : "No integration token stored.");
const requests = await withTimeout(listIntegrationReviewRequests(source.provider, source.baseUrl, credential.username, credential.password, "open"), 46_000, de ? `${source.label} hat nicht geantwortet.` : `${source.label} did not respond.`);
for (const { repo, identity } of entries) {
updatePullRequestBadge(repo.path, {
status: "ready",
count: requests.filter((request) => requestMatchesRepository(request, identity.repository)).length,
sourceId,
repository: identity.repository,
message: "",
}, generation);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
for (const { repo, identity } of entries) updatePullRequestBadge(repo.path, { status: "error", count: 0, sourceId, repository: identity.repository, message }, generation);
}
}));
}
function pullRequestTitle(repo: DashboardRepository): string {
const badge = pullRequestBadges[repo.path];
if (!badge || badge.status === "loading") return de ? "Offene Pull Requests werden geladen" : "Loading open pull requests";
if (badge.status === "error" || badge.status === "unavailable") return badge.message;
return de ? `${badge.count} offene Pull Requests` : `${badge.count} open pull requests`;
}
function toggleCategory(id: string) {
const next = new Set(collapsedCategories);
if (next.has(id)) next.delete(id);
@@ -188,6 +344,7 @@
{#if category.repos.length > 0}
<div class="repo-grid">
{#each category.repos as repo (repo.path)}
{@const pullRequests = pullRequestBadges[repo.path]}
<article class="repo-card" class:open={repo.isOpen}>
<button class="card-main" type="button" onclick={() => openCard(repo.path)} disabled={isBusy} title={repo.path}>
<span class="card-title"><FolderGit2 size={15} /><strong>{repo.name}</strong></span>
@@ -202,6 +359,9 @@
</span>
</button>
<div class="card-actions">
<button class:has-open-prs={pullRequests?.status === "ready" && pullRequests.count > 0} class:pr-error={pullRequests?.status === "error"} class="pr-badge" type="button" onclick={(event) => openPullRequests(event, repo)} disabled={isBusy || !pullRequests || pullRequests.status !== "ready"} aria-label={`${repo.name}: ${pullRequestTitle(repo)}`} title={pullRequestTitle(repo)}>
{#if !pullRequests || pullRequests.status === "loading"}<LoaderCircle class="spin" size={14} />{:else if pullRequests.status === "error"}<TriangleAlert size={14} />{:else}<GitPullRequest size={14} /><span>{pullRequests.status === "ready" ? pullRequests.count : ""}</span>{/if}
</button>
<button class:active={repo.favorite} type="button" onclick={(event) => favoriteCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Favorit umschalten" : "Toggle favorite"}`} aria-pressed={repo.favorite}><Star size={15} /></button>
{#if repo.isOpen}<button type="button" onclick={(event) => closeCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Schließen" : "Close"}`} title={de ? "Repository schließen" : "Close repository"}><X size={15} /></button>{/if}
{#if category.id === "recent"}<button type="button" onclick={(event) => removeRecentCard(event, repo.path)} disabled={isBusy} aria-label={`${repo.name}: ${de ? "Aus Recent entfernen" : "Remove from recent"}`} title={de ? "Aus Recent entfernen" : "Remove from recent"}><X size={15} /></button>{/if}
@@ -239,8 +399,8 @@
.dashboard-header{display:flex;min-height:58px;align-items:center;justify-content:space-between;gap:16px;padding:8px 20px;border-bottom:1px solid var(--color-border-subtle)}.dashboard-header h1{margin:0;font-size:18px;line-height:1.2;font-weight:700}.header-actions,.workspace-tools{display:flex;align-items:center;gap:8px}.header-actions button,.workspace-tools button{display:inline-flex;min-height:30px;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--color-border);background:var(--app-button-bg)}button.primary{border-color:var(--color-primary);background:var(--color-primary);color:#fff}
.dashboard-toolbar{display:flex;min-height:48px;align-items:center;gap:8px;padding:8px 20px;border-bottom:1px solid var(--color-border-subtle)}.dashboard-search{display:flex;width:300px;height:30px;align-items:center;gap:8px;padding:0 9px;border:1px solid var(--color-border-input);border-radius:var(--ui-radius-sm);color:var(--color-ink-faint);background:var(--app-input-bg)}.dashboard-search:focus-within{border-color:var(--color-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--color-primary) 14%,transparent)}.dashboard-search :global(svg){flex:0 0 auto}.dashboard-search input{width:100%;min-width:0;height:100%;padding:0;border:0;border-radius:0;outline:0;color:var(--color-ink);background:transparent;box-shadow:none}.dashboard-search input:focus,.dashboard-search input:focus-visible{border:0;outline:0;box-shadow:none}.workspace-tools{margin-left:auto}.workspace-tools :global(.workspace-select){width:240px}.workspace-tools :global(.workspace-select .select-menu-trigger){height:30px;min-height:30px;padding:0 9px;border-color:var(--color-border-input);background:var(--app-input-bg);font-size:12px;font-weight:500}.workspace-tools .workspace-delete{width:30px;min-width:30px;padding:0;border-color:color-mix(in srgb,#df626b 55%,var(--color-border));color:#df747b;background:color-mix(in srgb,#c92f3a 8%,var(--app-button-bg))}.workspace-tools .workspace-delete:hover:not(:disabled){border-color:#df626b;color:#fff;background:#c93b45;box-shadow:0 0 0 2px color-mix(in srgb,#c92f3a 18%,transparent)}
.dashboard-summary{padding:8px 20px;color:var(--color-ink-muted)}.dashboard-summary span{padding:0 6px;color:var(--color-ink-faint)}.dashboard-content{flex:1;min-height:0;overflow:auto;padding:0 20px 24px}.dashboard-section+.dashboard-section{margin-top:12px}.section-header{min-height:31px}.section-toggle{display:flex;width:100%;min-height:31px;align-items:center;justify-content:flex-start;gap:7px;padding:0;border:0;background:transparent;color:var(--color-ink);text-align:left}.section-toggle:hover:not(:disabled){border:0;background:transparent;color:var(--color-ink)}.section-toggle :global(svg){flex:0 0 auto;color:var(--color-ink-muted)}.section-toggle strong{font-size:13px;line-height:1.2;font-weight:750}.section-toggle span{color:var(--color-ink-faint);font-weight:400}.section-toggle i{height:1px;flex:1;background:var(--color-border-subtle)}.category-empty{margin:0;padding:9px 12px;border:1px solid var(--color-border-subtle);color:var(--color-ink-faint);background:var(--color-surface)}
.repo-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.repo-card{position:relative;min-width:0;height:138px;border:1px solid var(--color-border);background:var(--color-surface)}.repo-card:hover,.repo-card:focus-within{border-color:var(--color-border-input);background:var(--color-surface-hover)}.card-main{display:flex;width:100%;height:100%;flex-direction:column;align-items:flex-start;gap:7px;padding:12px 70px 11px 14px;border:0;background:transparent;text-align:left}.card-main:focus-visible{outline:2px solid var(--color-accent);outline-offset:-2px}.card-title{display:flex;max-width:100%;align-items:center;gap:8px}.card-title strong{overflow:hidden;font-size:14px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.card-path{width:100%;overflow:hidden;color:var(--color-ink-muted);text-overflow:ellipsis;white-space:nowrap}.card-branch,.card-status,.card-status>span{display:flex;align-items:center;gap:6px}.card-branch{max-width:100%;overflow:hidden;color:var(--color-ink-muted);text-overflow:ellipsis;white-space:nowrap}.card-status{flex-wrap:wrap;gap:8px 18px;margin-top:auto}.card-status :global(.filled){fill:currentColor}.changed,.behind{color:#eeb94e}.clean{color:#68c878}.ahead{color:var(--color-accent)}.muted{color:var(--color-ink-muted)}
.card-actions{position:absolute;top:8px;right:8px;z-index:2;display:flex;align-items:center;gap:2px}.card-actions button{display:grid;width:26px;height:26px;place-items:center;padding:0;border:0;color:var(--color-ink-muted);background:transparent}.card-actions button:hover:not(:disabled){color:var(--color-ink);background:var(--color-surface-dim)}.card-actions button.active{color:var(--color-accent)}.card-actions button.active :global(svg){fill:color-mix(in srgb,var(--color-accent) 22%,transparent)}
.repo-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.repo-card{position:relative;min-width:0;height:138px;border:1px solid var(--color-border);background:var(--color-surface)}.repo-card:hover,.repo-card:focus-within{border-color:var(--color-border-input);background:var(--color-surface-hover)}.card-main{display:flex;width:100%;height:100%;flex-direction:column;align-items:flex-start;gap:7px;padding:12px 104px 11px 14px;border:0;background:transparent;text-align:left}.card-main:focus-visible{outline:2px solid var(--color-accent);outline-offset:-2px}.card-title{display:flex;max-width:100%;align-items:center;gap:8px}.card-title strong{overflow:hidden;font-size:14px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.card-path{width:100%;overflow:hidden;color:var(--color-ink-muted);text-overflow:ellipsis;white-space:nowrap}.card-branch,.card-status,.card-status>span{display:flex;align-items:center;gap:6px}.card-branch{max-width:100%;overflow:hidden;color:var(--color-ink-muted);text-overflow:ellipsis;white-space:nowrap}.card-status{flex-wrap:wrap;gap:8px 18px;margin-top:auto}.card-status :global(.filled){fill:currentColor}.changed,.behind{color:#eeb94e}.clean{color:#68c878}.ahead{color:var(--color-accent)}.muted{color:var(--color-ink-muted)}
.card-actions{position:absolute;top:8px;right:8px;z-index:2;display:flex;align-items:center;gap:2px}.card-actions button{display:grid;width:26px;height:26px;place-items:center;padding:0;border:0;color:var(--color-ink-muted);background:transparent}.card-actions button:hover:not(:disabled){color:var(--color-ink);background:var(--color-surface-dim)}.card-actions button.active{color:var(--color-accent)}.card-actions button.active :global(svg){fill:color-mix(in srgb,var(--color-accent) 22%,transparent)}.card-actions .pr-badge{display:flex;width:auto;min-width:31px;grid-template-columns:none;align-items:center;justify-content:center;gap:4px;padding:0 5px;color:var(--color-ink-faint);font-size:11px}.card-actions .pr-badge:disabled{opacity:.72}.card-actions .pr-badge.has-open-prs{color:var(--color-accent)}.card-actions .pr-badge.pr-error{color:#e0a35b}.card-actions .pr-badge:hover:not(:disabled){color:var(--color-accent);background:color-mix(in srgb,var(--color-accent) 9%,transparent)}
.dashboard-footer{display:flex;min-height:30px;align-items:center;padding:0 20px;border-top:1px solid var(--color-border-subtle);color:var(--color-ink-muted)}
.workspace-dialog{width:min(420px,calc(100vw - 32px));margin:auto;padding:0;border:1px solid var(--color-border);color:var(--color-ink);background:var(--app-dialog-bg);box-shadow:var(--app-dialog-shadow);font-size:12px}.workspace-dialog::backdrop{background:var(--app-dialog-backdrop)}.workspace-dialog form>header,.workspace-dialog form>footer{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--app-dialog-chrome)}.workspace-dialog form>header{border-bottom:1px solid var(--color-border)}.workspace-dialog form>footer{justify-content:flex-end;gap:8px;border-top:1px solid var(--color-border)}.workspace-dialog h2{margin:0;font-size:13px}.workspace-dialog header button{width:26px;height:26px;padding:0;border:0;background:transparent}.workspace-dialog form>label,.workspace-dialog form>input,.workspace-dialog fieldset,.dialog-error{margin-right:12px;margin-left:12px}.workspace-dialog form>label{display:block;margin-top:12px;margin-bottom:5px;color:var(--color-ink-muted)}.workspace-dialog form>input{width:calc(100% - 24px);height:30px;padding:0 8px;border:1px solid var(--color-border-input);color:var(--color-ink);background:var(--app-input-bg)}.dialog-error{margin-top:7px;color:#ed9292}.workspace-dialog fieldset{margin-top:14px;margin-bottom:14px;padding:0;border:0}.workspace-dialog legend{margin-bottom:6px;color:var(--color-ink-muted)}.workspace-repos{max-height:190px;overflow:auto;border:1px solid var(--color-border-subtle)}.workspace-repos label{display:grid;grid-template-columns:auto minmax(100px,auto) 1fr;align-items:center;gap:8px;min-height:32px;padding:4px 8px;border-bottom:1px solid var(--color-border-subtle)}.workspace-repos label:last-child{border-bottom:0}.workspace-repos small{overflow:hidden;color:var(--color-ink-faint);text-overflow:ellipsis;white-space:nowrap}.workspace-dialog footer button{display:inline-flex;min-height:28px;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--color-border);background:var(--app-button-bg)}
@media(max-width:1000px){.dashboard-toolbar{flex-wrap:wrap;gap:8px}.dashboard-search{flex:1 1 260px}.workspace-tools{margin-left:0}.repo-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
+5 -2
View File
@@ -14,11 +14,13 @@
interface Props {
language: AppLanguage;
integrations: GitIntegrationSettings;
initialQuery?: string;
initialSourceId?: string;
loadCredential: (key: string) => Promise<StoredCredential | null>;
onOpenSettings: () => void;
}
let { language = "en", integrations, loadCredential, onOpenSettings = () => {} }: Props = $props();
let { language = "en", integrations, initialQuery = "", initialSourceId = "", loadCredential, onOpenSettings = () => {} }: Props = $props();
let requests = $state<IntegrationReviewRequest[]>([]);
let loading = $state(false);
let errors = $state<Array<{ source: string; message: string }>>([]);
@@ -60,7 +62,8 @@
onMount(() => {
const closeActionMenu = () => { actionMenuId = ""; };
window.addEventListener("click", closeActionMenu);
const source = sources[0];
query = initialQuery;
const source = sources.find((candidate) => candidate.id === initialSourceId) ?? sources[0];
if (source) {
selectedSourceId = source.id;
void loadRequests("open", source.id);