feat(dashboard): show PR badges and deep-link to Review Center
Add pull request badges to the repository dashboard and enable deep- linking to the Review Center with a repository and source pre-filter. Badges are populated by resolving remotes, matching them to configured integration sources, and querying providers for open PRs while handling loading, error, and unavailable states. The dashboard badge opens the Review Center pre-filled and the app stores initial query/source values to support the deep-link. - Resolve and normalize remotes to match integration sources reliably. - Query provider APIs using stored credentials with sensible timeouts. - Expose a badge action that navigates to the Review Center pre-filtered.
This commit is contained in:
+10
-2
@@ -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; }}
|
||||
/>
|
||||
|
||||
@@ -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))}}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user