From fcd884ee0b97443155a43db452ce52e0b16e0f46 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Sat, 5 Sep 2026 23:45:23 +0200 Subject: [PATCH] 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). --- src/App.svelte | 205 ++------------- src/app.css | 9 + src/lib/RepoTabs.svelte | 1 + src/lib/components/RepositoryDashboard.svelte | 248 ++++++++++++++++++ src/lib/components/SelectMenu.svelte | 3 +- 5 files changed, 288 insertions(+), 178 deletions(-) create mode 100644 src/lib/components/RepositoryDashboard.svelte diff --git a/src/App.svelte b/src/App.svelte index 71aa218..34ca14b 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -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 = {}; - 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; $: 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"} -
-
-
- Repository Management -

Repositories

-
-
- - - -
-
- -
- -
- -
-
-
-

Open repositories

- {openRepoRows.length} -
- {#if openRepoRows.length === 0} -
No open repositories.
- {:else} -
- {#each openRepoRows as repo (repo.path)} -
- - - -
- {/each} -
- {/if} -
- -
-
-

Recent repositories

- {recentRepoRows.length} -
- {#if recentRepoRows.length === 0} -
No recent repositories.
- {:else} -
- {#each recentRepoRows as repo (repo.path)} -
- - - -
- {/each} -
- {/if} -
- -
-
-

Favorites

- {favoriteRepoRows.length} -
- {#if favoriteRepoRows.length === 0} -
Mark repositories with the star to keep them here.
- {:else} -
- {#each favoriteRepoRows as repo (repo.path)} -
- - - -
- {/each} -
- {/if} -
-
-
+ {:else}