Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { access } from "node:fs/promises";
import { basename, join } from "node:path";
import type { BrowserWindow } from "electron";
import { dialog } from "electron";
import { track } from "main/lib/analytics";
import { db } from "main/lib/db";
import type { Project } from "main/lib/db/schemas";
import { terminalManager } from "main/lib/terminal";
import { nanoid } from "nanoid";
import { PROJECT_COLOR_VALUES } from "shared/constants/project-colors";
import simpleGit from "simple-git";
Expand Down Expand Up @@ -548,6 +550,67 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {

return { success: true };
}),

close: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
const project = db.data.projects.find((p) => p.id === input.id);

if (!project) {
throw new Error("Project not found");
}

// Find all workspaces for this project
const projectWorkspaces = db.data.workspaces.filter(
(w) => w.projectId === input.id,
);

// Kill all terminal processes in all workspaces of this project
let totalFailed = 0;
for (const workspace of projectWorkspaces) {
const terminalResult = await terminalManager.killByWorkspaceId(
workspace.id,
);
totalFailed += terminalResult.failed;
}

// Remove all workspace records and hide the project
await db.update((data) => {
// Remove all workspaces for this project
data.workspaces = data.workspaces.filter(
(w) => w.projectId !== input.id,
);

// Hide the project by setting tabOrder to null
const p = data.projects.find((p) => p.id === input.id);
if (p) {
p.tabOrder = null;
}

// Update active workspace if it was in this project
const closedWorkspaceIds = new Set(
projectWorkspaces.map((w) => w.id),
);
if (
data.settings.lastActiveWorkspaceId &&
closedWorkspaceIds.has(data.settings.lastActiveWorkspaceId)
) {
const sorted = data.workspaces
.slice()
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
data.settings.lastActiveWorkspaceId = sorted[0]?.id || undefined;
}
});

const terminalWarning =
totalFailed > 0
? `${totalFailed} terminal process(es) may still be running`
: undefined;

track("project_closed", { project_id: input.id });

return { success: true, terminalWarning };
}),
});
};

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/renderer/react-query/projects/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { useCloseProject } from "./useCloseProject";
export { useOpenNew } from "./useOpenNew";
export { useReorderProjects } from "./useReorderProjects";
export { useUpdateProject } from "./useUpdateProject";
24 changes: 24 additions & 0 deletions apps/desktop/src/renderer/react-query/projects/useCloseProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { trpc } from "renderer/lib/trpc";

/**
* Mutation hook for closing a project (hides from tabs, keeps worktrees on disk)
* Automatically invalidates all workspace and project queries on success
*/
export function useCloseProject(
options?: Parameters<typeof trpc.projects.close.useMutation>[0],
) {
const utils = trpc.useUtils();

return trpc.projects.close.useMutation({
...options,
onSuccess: async (...args) => {
// Auto-invalidate all workspace queries
await utils.workspaces.invalidate();
// Invalidate project queries since close updates project metadata
await utils.projects.getRecents.invalidate();

// Call user's onSuccess if provided
await options?.onSuccess?.(...args);
},
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
} from "@superset/ui/context-menu";
import type { KeyboardEvent, ReactNode } from "react";
import { useEffect, useRef, useState } from "react";
import { useUpdateProject } from "renderer/react-query/projects";
import {
useCloseProject,
useUpdateProject,
} from "renderer/react-query/projects";
import { PROJECT_COLORS } from "shared/constants/project-colors";

interface WorkspaceGroupContextMenuProps {
Expand All @@ -26,6 +29,7 @@ export function WorkspaceGroupContextMenu({
const inputRef = useRef<HTMLInputElement | null>(null);
const skipBlurSubmit = useRef(false);
const updateProject = useUpdateProject();
const closeProject = useCloseProject();

useEffect(() => {
setName(projectName);
Expand Down Expand Up @@ -146,12 +150,11 @@ export function WorkspaceGroupContextMenu({
<button
type="button"
onClick={() => {
// TODO: Implement worktree configuration
inputRef.current?.focus();
closeProject.mutate({ id: projectId });
}}
className="w-full px-2 py-1.5 text-left text-sm text-foreground transition-colors hover:bg-accent"
className="w-full px-2 py-1.5 text-left text-sm text-destructive transition-colors hover:bg-destructive/10"
>
Configure worktree setup
Close Project
</button>
</ContextMenuContent>
</ContextMenu>
Expand Down
Loading