From ea4d63b9333ef5a1735d014bb2163d8fc6353949 Mon Sep 17 00:00:00 2001 From: Mucheen <1528136628@qq.com> Date: Mon, 17 Aug 2026 20:03:55 +0800 Subject: [PATCH] feat(windows): add project tab close controls --- .../window/components/project-tab-bar.test.ts | 11 +- .../window/components/project-tab-bar.tsx | 114 ++++++++++++------ windows/tauri/src/i18n/locale.test.ts | 4 + windows/tauri/src/i18n/locale.ts | 2 + 4 files changed, 93 insertions(+), 38 deletions(-) diff --git a/windows/tauri/src/features/window/components/project-tab-bar.test.ts b/windows/tauri/src/features/window/components/project-tab-bar.test.ts index 2700997e..8903c0d2 100644 --- a/windows/tauri/src/features/window/components/project-tab-bar.test.ts +++ b/windows/tauri/src/features/window/components/project-tab-bar.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; -test("project tab bar exposes accessible switchable project tabs", async () => { +test("project tab bar exposes accessible switchable and closable project tabs", async () => { const source = await Bun.file(new URL("./project-tab-bar.tsx", import.meta.url)).text(); expect(source).toContain('role="tablist"'); @@ -8,4 +8,13 @@ test("project tab bar exposes accessible switchable project tabs", async () => { expect(source).toContain("aria-selected"); expect(source).toContain("isSwitchingProject"); expect(source).toContain("switchToProject"); + expect(source).toContain("closeProject"); + expect(source).toContain("XIcon as X"); + expect(source).toContain("isProjectActionPending"); + expect(source).toContain("event.stopPropagation()"); + expect(source).toContain("await closeProject(projectId)"); + expect(source).toContain("finally"); + expect(source).toContain('t("titleProject.closeProject", { name: project.name })'); + expect(source).toContain("group-hover:opacity-100"); + expect(source).toContain("group-focus-within:opacity-100"); }); diff --git a/windows/tauri/src/features/window/components/project-tab-bar.tsx b/windows/tauri/src/features/window/components/project-tab-bar.tsx index 08a7d8f7..4ee4d426 100644 --- a/windows/tauri/src/features/window/components/project-tab-bar.tsx +++ b/windows/tauri/src/features/window/components/project-tab-bar.tsx @@ -1,17 +1,32 @@ -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { useFileSystemStore } from "@/features/file-system/stores/file-system.store"; import { useTranslation } from "@/i18n/locale-provider"; import { useWorkspaceTabsStore } from "@/features/window/stores/workspace-tabs.store"; -import { FolderIcon } from "@/ui/icons"; +import { Button } from "@/ui/button"; +import { FolderIcon, XIcon as X } from "@/ui/icons"; import { cn } from "@/utils/cn"; import { getProjectTabBarItems } from "../utils/project-tab-bar-model"; export function ProjectTabBar() { const { t } = useTranslation(); + const [closingProjectId, setClosingProjectId] = useState(null); const projectTabs = useWorkspaceTabsStore.use.projectTabs(); const switchToProject = useFileSystemStore((state) => state.switchToProject); + const closeProject = useFileSystemStore((state) => state.closeProject); const isSwitchingProject = useFileSystemStore((state) => state.isSwitchingProject); const projects = useMemo(() => getProjectTabBarItems(projectTabs), [projectTabs]); + const isProjectActionPending = isSwitchingProject || closingProjectId !== null; + + const handleCloseProject = async (projectId: string) => { + if (isProjectActionPending) return; + + setClosingProjectId(projectId); + try { + await closeProject(projectId); + } finally { + setClosingProjectId(null); + } + }; if (projects.length === 0) return null; @@ -23,41 +38,66 @@ export function ProjectTabBar() { aria-orientation="horizontal" >
- {projects.map((project) => ( - - ))} + {projects.map((project) => { + const closeLabel = t("titleProject.closeProject", { name: project.name }); + + return ( +
+ + +
+ ); + })}
); diff --git a/windows/tauri/src/i18n/locale.test.ts b/windows/tauri/src/i18n/locale.test.ts index ca9b9a9b..a666c5c3 100644 --- a/windows/tauri/src/i18n/locale.test.ts +++ b/windows/tauri/src/i18n/locale.test.ts @@ -37,6 +37,10 @@ describe("Windows display language", () => { expect(translate("git.log.headCurrentBranch")).toBe("HEAD(当前分支)"); expect(translate("run.identifyAndGenerate")).toBe("识别并生成"); expect(createTranslator("en-US")("workbench.run")).toBe("Run"); + expect(createTranslator("en-US")("titleProject.closeProject", { name: "Lithe" })).toBe( + "Close project Lithe", + ); + expect(translate("titleProject.closeProject", { name: "Lithe" })).toBe("关闭项目 Lithe"); expect(translate("welcome.openProject")).toBe("打开项目"); expect(translate("workbench.diagnostics")).toBe("诊断"); expect(translate("diagnostics.empty")).toBe("未检测到问题"); diff --git a/windows/tauri/src/i18n/locale.ts b/windows/tauri/src/i18n/locale.ts index e7089751..69013613 100644 --- a/windows/tauri/src/i18n/locale.ts +++ b/windows/tauri/src/i18n/locale.ts @@ -113,6 +113,7 @@ const catalogs = { "titleProject.open": "Open…", "titleProject.cloneRepository": "Clone Repository…", "titleProject.openProjects": "Open Projects", + "titleProject.closeProject": "Close project {name}", "titleProject.recentProjects": "Recent Projects", "titleProject.noRecentProjects": "No recent projects", "titleProject.trigger": "Project: {project}", @@ -1179,6 +1180,7 @@ const catalogs = { "titleProject.open": "打开…", "titleProject.cloneRepository": "克隆仓库…", "titleProject.openProjects": "打开的项目", + "titleProject.closeProject": "关闭项目 {name}", "titleProject.recentProjects": "最近项目", "titleProject.noRecentProjects": "没有最近项目", "titleProject.trigger": "项目:{project}",