feat(management): extract repository management into dashboard component
Move the inline repository management UI into a new RepositoryDashboard component and wire it into the application. Refactor app state to compute a reactive dashboardRepos list and update tab-closing logic so tabs can be closed while preserving the management view when appropriate. - Add a full dashboard with search, categories, workspaces and persisted prefs. - Refactor close behavior to support keeping the management view open. - Improve UI/CSS and accessibility for the management tab (compact layout and label).
This commit is contained in:
+28
-177
@@ -10,6 +10,7 @@
|
||||
|
||||
import TitleBar from "./lib/TitleBar.svelte";
|
||||
import RepoToolbar from "./lib/RepoToolbar.svelte";
|
||||
import RepositoryDashboard from "./lib/components/RepositoryDashboard.svelte";
|
||||
import RepoTabs from "./lib/RepoTabs.svelte";
|
||||
import AiReviewDialog from "./lib/components/AiReviewDialog.svelte";
|
||||
import AiCommitSplitDialog from "./lib/components/AiCommitSplitDialog.svelte";
|
||||
@@ -314,7 +315,12 @@
|
||||
// Last-seen branch/ahead/behind/changed for repos that are known (recent/favorites)
|
||||
// but not currently open as a tab — keyed by normalized path (repoKey).
|
||||
let repoStatusCache: Record<string, RepoTab> = {};
|
||||
let repoSearch = "";
|
||||
$: dashboardRepos = uniqueRepoPaths([...repoTabs.map(tab => tab.path), ...recentRepoPaths, ...favoriteRepoPaths])
|
||||
.map(path => ({ ...repoRowFromPath(path, repoTabs, repoStatusCache),
|
||||
isOpen: repoTabs.some(tab => sameRepoPath(tab.path, path)),
|
||||
known: Boolean(repoStatusCache[repoKey(path)]),
|
||||
recent: recentRepoPaths.some(recent => sameRepoPath(recent, path)),
|
||||
favorite: favoriteRepoPaths.some(favorite => sameRepoPath(favorite, path)) }));
|
||||
let cloneDialogOpen = false;
|
||||
let initRepositoryDialogOpen = false;
|
||||
let pullStrategy: PullStrategy = (localStorage.getItem("gitlite.pullStrategy") as PullStrategy) || "merge";
|
||||
@@ -552,15 +558,6 @@
|
||||
) as Record<string, string>;
|
||||
$: remoteBranches = branches.filter((b) => b.remote);
|
||||
$: currentBranchIsLocalOnly = Boolean(status?.current_branch) && !status?.upstream;
|
||||
$: repoSearchTerm = repoSearch.trim().toLowerCase();
|
||||
$: openRepoRows = repoTabs.filter((repo) => repoMatchesSearch(repo, repoSearchTerm));
|
||||
$: recentRepoRows = recentRepoPaths
|
||||
.filter((path) => !repoTabs.some((tab) => sameRepoPath(tab.path, path)))
|
||||
.map((path) => repoRowFromPath(path, repoTabs, repoStatusCache))
|
||||
.filter((repo) => repoMatchesSearch(repo, repoSearchTerm));
|
||||
$: favoriteRepoRows = favoriteRepoPaths
|
||||
.map((path) => repoRowFromPath(path, repoTabs, repoStatusCache))
|
||||
.filter((repo) => repoMatchesSearch(repo, repoSearchTerm));
|
||||
$: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed);
|
||||
$: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
|
||||
$: editorToolName = externalToolDisplayName("editor", externalToolsSettings.editor, detectedExternalTools);
|
||||
@@ -987,7 +984,9 @@
|
||||
backgroundRepoStatusIndex += 1;
|
||||
|
||||
try {
|
||||
if (fetchFirst) await fetchRemote(path);
|
||||
if (fetchFirst) {
|
||||
await fetchRemote(path);
|
||||
}
|
||||
const nextStatus = await getStatus(path);
|
||||
updateRepoManagementStatus(path, nextStatus);
|
||||
} catch {
|
||||
@@ -1605,13 +1604,6 @@
|
||||
};
|
||||
}
|
||||
|
||||
function repoMatchesSearch(repo: RepoTab, searchTerm = repoSearchTerm): boolean {
|
||||
if (!searchTerm) return true;
|
||||
return repo.name.toLowerCase().includes(searchTerm)
|
||||
|| repo.path.toLowerCase().includes(searchTerm)
|
||||
|| (repo.branch ?? "").toLowerCase().includes(searchTerm);
|
||||
}
|
||||
|
||||
function loadRepoLists() {
|
||||
try {
|
||||
const cacheValue = JSON.parse(localStorage.getItem(REPO_STATUS_CACHE_KEY) ?? "{}") as unknown;
|
||||
@@ -2614,7 +2606,11 @@
|
||||
await closeRepoTab(path);
|
||||
}
|
||||
|
||||
async function closeRepoTab(path: string, event?: MouseEvent) {
|
||||
async function closeDashboardRepository(path: string) {
|
||||
await closeRepoTab(path, undefined, true);
|
||||
}
|
||||
|
||||
async function closeRepoTab(path: string, event?: MouseEvent, keepManagement = false) {
|
||||
event?.stopPropagation();
|
||||
if (isBusy) return;
|
||||
closeRepoTabContextMenu();
|
||||
@@ -2631,6 +2627,12 @@
|
||||
});
|
||||
|
||||
if (!wasActive) return;
|
||||
if (keepManagement) {
|
||||
repoOpenRequestId += 1;
|
||||
resetRepositoryState(true);
|
||||
activeView = "management";
|
||||
return;
|
||||
}
|
||||
if (next) {
|
||||
// Switch identity immediately. Otherwise a status/fetch request for the
|
||||
// just-closed repository can still finish while it remains active and
|
||||
@@ -5236,164 +5238,13 @@
|
||||
{/if}
|
||||
|
||||
{#if activeView === "management"}
|
||||
<section class="repo-management" aria-label="Repository Management">
|
||||
<div class="repo-management-head">
|
||||
<div>
|
||||
<span class="eyebrow">Repository Management</span>
|
||||
<h1>Repositories</h1>
|
||||
</div>
|
||||
<div class="repo-management-actions">
|
||||
<button class="btn-primary" type="button" onclick={openCloneDialog} disabled={isBusy}>
|
||||
<Download size={15} aria-hidden="true" />
|
||||
Clone
|
||||
</button>
|
||||
<button class="btn-secondary" type="button" onclick={initializeRepository} disabled={isBusy}>
|
||||
<Plus size={15} aria-hidden="true" /> Init
|
||||
</button>
|
||||
<button class="btn-secondary" type="button" onclick={chooseRepositoryFolder} disabled={isBusy}>
|
||||
<FolderOpen size={15} aria-hidden="true" />
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-management-tools">
|
||||
<div class="repo-search">
|
||||
<Search size={15} aria-hidden="true" />
|
||||
<input
|
||||
bind:value={repoSearch}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder="Search repositories"
|
||||
aria-label="Search repositories"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-sections">
|
||||
<section class="repo-section">
|
||||
<header>
|
||||
<h2>Open repositories</h2>
|
||||
<span>{openRepoRows.length}</span>
|
||||
</header>
|
||||
{#if openRepoRows.length === 0}
|
||||
<div class="repo-empty">No open repositories.</div>
|
||||
{:else}
|
||||
<div class="repo-table">
|
||||
{#each openRepoRows as repo (repo.path)}
|
||||
<div class="repo-row">
|
||||
<button class="repo-row-main" type="button" onclick={() => selectRepoTab(repo.path)} disabled={isBusy}>
|
||||
<span class="repo-row-name">{repo.name}</span>
|
||||
<span class="repo-row-path">{repo.path}</span>
|
||||
<span class="repo-row-meta">
|
||||
{#if repo.branch}<strong><GitBranch size={11} aria-hidden="true" />{repo.branch}</strong>{/if}
|
||||
{#if repo.ahead > 0}<em class="ahead">↑ {repo.ahead}</em>{/if}
|
||||
{#if repo.behind > 0}<em class="behind">↓ {repo.behind}</em>{/if}
|
||||
{#if repo.changed > 0}<em>{repo.changed} changed</em>{/if}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="repo-row-icon repo-row-favorite"
|
||||
class:active={isFavoriteRepo(repo.path)}
|
||||
type="button"
|
||||
onclick={(event) => toggleFavoriteRepo(repo.path, event)}
|
||||
disabled={isBusy}
|
||||
title={isFavoriteRepo(repo.path) ? "Remove from favorites" : "Add to favorites"}
|
||||
aria-label={isFavoriteRepo(repo.path) ? `Remove ${repo.name} from favorites` : `Add ${repo.name} to favorites`}
|
||||
>
|
||||
<Star size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button class="repo-row-icon" type="button" onclick={(event) => closeRepoTab(repo.path, event)} disabled={isBusy} title="Close tab" aria-label={`Close ${repo.name}`}>
|
||||
<X size={14} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="repo-section">
|
||||
<header>
|
||||
<h2>Recent repositories</h2>
|
||||
<span>{recentRepoRows.length}</span>
|
||||
</header>
|
||||
{#if recentRepoRows.length === 0}
|
||||
<div class="repo-empty">No recent repositories.</div>
|
||||
{:else}
|
||||
<div class="repo-table">
|
||||
{#each recentRepoRows as repo (repo.path)}
|
||||
<div class="repo-row">
|
||||
<button class="repo-row-main" type="button" onclick={() => openRepo(repo.path)} disabled={isBusy}>
|
||||
<span class="repo-row-name">{repo.name}</span>
|
||||
<span class="repo-row-path">{repo.path}</span>
|
||||
<span class="repo-row-meta">
|
||||
{#if repo.branch}<strong><GitBranch size={11} aria-hidden="true" />{repo.branch}</strong>{:else}<em class="quiet">recent</em>{/if}
|
||||
{#if repo.ahead > 0}<em class="ahead">↑ {repo.ahead}</em>{/if}
|
||||
{#if repo.behind > 0}<em class="behind">↓ {repo.behind}</em>{/if}
|
||||
{#if repo.changed > 0}<em>{repo.changed} changed</em>{/if}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="repo-row-icon repo-row-favorite"
|
||||
class:active={isFavoriteRepo(repo.path)}
|
||||
type="button"
|
||||
onclick={(event) => toggleFavoriteRepo(repo.path, event)}
|
||||
disabled={isBusy}
|
||||
title={isFavoriteRepo(repo.path) ? "Remove from favorites" : "Add to favorites"}
|
||||
aria-label={isFavoriteRepo(repo.path) ? `Remove ${repo.name} from favorites` : `Add ${repo.name} to favorites`}
|
||||
>
|
||||
<Star size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button class="repo-row-icon" type="button" onclick={(event) => removeRepoFromRecent(repo.path, event)} disabled={isBusy} title="Remove from recent" aria-label={`Remove ${repo.name}`}>
|
||||
<X size={14} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="repo-section">
|
||||
<header>
|
||||
<h2>Favorites</h2>
|
||||
<span>{favoriteRepoRows.length}</span>
|
||||
</header>
|
||||
{#if favoriteRepoRows.length === 0}
|
||||
<div class="repo-empty">Mark repositories with the star to keep them here.</div>
|
||||
{:else}
|
||||
<div class="repo-table">
|
||||
{#each favoriteRepoRows as repo (repo.path)}
|
||||
<div class="repo-row">
|
||||
<button class="repo-row-main" type="button" onclick={() => openRepo(repo.path)} disabled={isBusy}>
|
||||
<span class="repo-row-name">{repo.name}</span>
|
||||
<span class="repo-row-path">{repo.path}</span>
|
||||
<span class="repo-row-meta">
|
||||
{#if repo.branch}<strong><GitBranch size={11} aria-hidden="true" />{repo.branch}</strong>{:else}<em class="quiet">favorite</em>{/if}
|
||||
{#if repo.ahead > 0}<em class="ahead">↑ {repo.ahead}</em>{/if}
|
||||
{#if repo.behind > 0}<em class="behind">↓ {repo.behind}</em>{/if}
|
||||
{#if repo.changed > 0}<em>{repo.changed} changed</em>{/if}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="repo-row-icon repo-row-favorite active"
|
||||
type="button"
|
||||
onclick={(event) => toggleFavoriteRepo(repo.path, event)}
|
||||
disabled={isBusy}
|
||||
title="Remove from favorites"
|
||||
aria-label={`Remove ${repo.name} from favorites`}
|
||||
>
|
||||
<Star size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button class="repo-row-icon" type="button" onclick={(event) => removeRepoFromManagement(repo.path, event)} disabled={isBusy} title="Remove" aria-label={`Remove ${repo.name}`}>
|
||||
<X size={14} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<RepositoryDashboard
|
||||
repos={dashboardRepos} language={appLanguage} {isBusy}
|
||||
onOpen={selectRepoTab} onAdd={chooseRepositoryFolder} onClone={openCloneDialog}
|
||||
onInit={initializeRepository}
|
||||
onFavorite={toggleFavoriteRepo} onClose={closeDashboardRepository}
|
||||
onRemoveRecent={removeRepoFromRecent}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Workspace -->
|
||||
<section
|
||||
|
||||
Reference in New Issue
Block a user