Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 0 additions & 2 deletions src/browser/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ function AppInner() {
selectedWorkspace,
setSelectedWorkspace,
pendingNewWorkspaceProject,
pendingNewWorkspaceSectionId,
pendingNewWorkspaceDraftId,
beginWorkspaceCreation,
} = useWorkspaceContext();
Expand Down Expand Up @@ -1116,7 +1115,6 @@ function AppInner() {
projectName={projectName}
leftSidebarCollapsed={sidebarCollapsed}
onToggleLeftSidebarCollapsed={handleToggleSidebar}
pendingSectionId={pendingNewWorkspaceSectionId}
pendingDraftId={pendingNewWorkspaceDraftId}
onWorkspaceCreated={(metadata, options) => {
// IMPORTANT: Add workspace to store FIRST (synchronous) to ensure
Expand Down
4 changes: 0 additions & 4 deletions src/browser/components/ProjectPage/ProjectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ interface ProjectPageProps {
onToggleLeftSidebarCollapsed: () => void;
/** Draft ID for UI-only workspace creation drafts (from URL) */
pendingDraftId?: string | null;
/** Section ID to pre-select when creating (from sidebar section "+" button) */
pendingSectionId?: string | null;
onWorkspaceCreated: (
metadata: FrontendWorkspaceMetadata,
options?: WorkspaceCreatedOptions
Expand Down Expand Up @@ -75,7 +73,6 @@ export const ProjectPage: React.FC<ProjectPageProps> = ({
leftSidebarCollapsed,
onToggleLeftSidebarCollapsed,
pendingDraftId,
pendingSectionId,
onWorkspaceCreated,
}) => {
const { api } = useAPI();
Expand Down Expand Up @@ -327,7 +324,6 @@ export const ProjectPage: React.FC<ProjectPageProps> = ({
variant="creation"
projectPath={projectPath}
projectName={projectName}
pendingSectionId={pendingSectionId}
pendingDraftId={pendingDraftId}
onReady={handleChatReady}
onWorkspaceCreated={onWorkspaceCreated}
Expand Down
238 changes: 23 additions & 215 deletions src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ let archivePopoverShowErrorMock = mock(
);

function createProjectContextValue(
overrides: Partial<ProjectContextModule.ProjectContext> = {}
overrides: Partial<ProjectContextModule.ProjectContext> & Record<string, unknown> = {}
): ProjectContextModule.ProjectContext {
return {
userProjects: new Map(),
Expand Down Expand Up @@ -162,12 +162,6 @@ function createProjectContextValue(
updateSecrets: () => Promise.resolve(),
updateDisplayName: () => resolveVoidResult(),
updateColor: () => resolveVoidResult(),
createSection: () =>
Promise.resolve({ success: true, data: { id: "section-1", name: "Section" } }),
updateSection: () => resolveVoidResult(),
removeSection: () => resolveVoidResult(),
reorderSections: () => resolveVoidResult(),
assignWorkspaceToSection: () => resolveVoidResult(),
hasAnyProject: false,
resolveNewChatProjectPath: () => null,
...overrides,
Expand Down Expand Up @@ -719,12 +713,6 @@ describe("ProjectSidebar multi-project completed-subagent toggles", () => {
updateSecrets: () => Promise.resolve(),
updateDisplayName: () => resolveVoidResult(),
updateColor: () => resolveVoidResult(),
createSection: () =>
Promise.resolve({ success: true, data: { id: "section-1", name: "Section" } }),
updateSection: () => resolveVoidResult(),
removeSection: () => resolveVoidResult(),
reorderSections: () => resolveVoidResult(),
assignWorkspaceToSection: () => resolveVoidResult(),
hasAnyProject: true,
resolveNewChatProjectPath: () => "/projects/demo-project",
}));
Expand Down Expand Up @@ -830,12 +818,6 @@ describe("ProjectSidebar multi-project completed-subagent toggles", () => {
updateSecrets: () => Promise.resolve(),
updateDisplayName: () => resolveVoidResult(),
updateColor: () => resolveVoidResult(),
createSection: () =>
Promise.resolve({ success: true, data: { id: "section-1", name: "Section" } }),
updateSection: () => resolveVoidResult(),
removeSection: () => resolveVoidResult(),
reorderSections: () => resolveVoidResult(),
assignWorkspaceToSection: () => resolveVoidResult(),
hasAnyProject: true,
resolveNewChatProjectPath: () => "/projects/demo-project",
}));
Expand Down Expand Up @@ -939,12 +921,6 @@ describe("ProjectSidebar multi-project completed-subagent toggles", () => {
updateSecrets: () => Promise.resolve(),
updateDisplayName: () => resolveVoidResult(),
updateColor: () => resolveVoidResult(),
createSection: () =>
Promise.resolve({ success: true, data: { id: "section-1", name: "Section" } }),
updateSection: () => resolveVoidResult(),
removeSection: () => resolveVoidResult(),
reorderSections: () => resolveVoidResult(),
assignWorkspaceToSection: () => resolveVoidResult(),
hasAnyProject: true,
resolveNewChatProjectPath: () => "/projects/demo-project",
}));
Expand Down Expand Up @@ -1456,7 +1432,6 @@ describe("ProjectSidebar project actions menu", () => {
const menuButtons = within(menu).getAllByRole("button");
expect(menuButtons.map((button) => button.textContent)).toEqual([
"Edit name",
"Add sub-folder",
"Manage secrets",
"Change color",
"Delete...",
Expand All @@ -1472,6 +1447,28 @@ describe("ProjectSidebar project actions menu", () => {
expect(view.getByRole("button", { name: "Edit name" })).toBeTruthy();
});

test("shows nested sub-project rows only when the parent project is expanded", () => {
const childProjectPath = `${demoProjectPath}/packages/payments`;
window.localStorage.setItem(EXPANDED_PROJECTS_KEY, JSON.stringify([]));
projectContextValue = createProjectContextValue({
userProjects: new Map([
[demoProjectPath, { workspaces: [] }],
[
childProjectPath,
{ workspaces: [{ path: `${childProjectPath}/ws-1` }], displayName: "payments" },
],
]),
});

const view = renderSidebar();

expect(view.queryByText("payments")).toBeNull();

fireEvent.click(view.getByRole("button", { name: "Expand project demo-project" }));

expect(view.getByText("payments")).toBeTruthy();
});

test("menu actions route to settings and delete confirmation", () => {
projectContextValue = createProjectContextValue({
userProjects: new Map([
Expand Down Expand Up @@ -1598,193 +1595,6 @@ describe("ProjectSidebar project actions menu", () => {
expect(inputAfterRefresh?.value).toBe("#123456");
});

test("Add sub-folder expands collapsed project before auto-editing", async () => {
window.localStorage.setItem(EXPANDED_PROJECTS_KEY, JSON.stringify([]));
const createSection = mock(() =>
Promise.resolve({
success: true as const,
data: { id: "new-section", name: "New sub-folder", color: "#6B7280", nextId: null },
})
);
projectContextValue = createProjectContextValue({
userProjects: new Map([
[
demoProjectPath,
{
workspaces: [],
sections: [
{ id: "new-section", name: "New sub-folder", color: "#6B7280", nextId: null },
],
},
],
]),
createSection,
});

const view = renderSidebar();
expect(view.getByRole("button", { name: "Expand project demo-project" })).toBeTruthy();

fireEvent.click(view.getByRole("button", { name: "Project options for demo-project" }));
fireEvent.click(view.getByRole("button", { name: "Add sub-folder" }));

await waitFor(() => {
expect(createSection).toHaveBeenCalledWith(demoProjectPath, "New sub-folder");
expect(view.getByRole("button", { name: "Collapse project demo-project" })).toBeTruthy();
});
});

test("Add sub-folder abandon reuses section-delete confirmation before removing", async () => {
const createSection = mock(() =>
Promise.resolve({
success: true as const,
data: { id: "new-section", name: "New sub-folder", color: "#6B7280", nextId: null },
})
);
const removeSection = mock(() => resolveVoidResult());
projectContextValue = createProjectContextValue({
userProjects: new Map([
[
demoProjectPath,
{
workspaces: [
{
path: `${demoProjectPath}/ws-in-section`,
sectionId: "new-section",
},
],
sections: [
{ id: "new-section", name: "New sub-folder", color: "#6B7280", nextId: null },
],
},
],
]),
createSection,
removeSection,
});

const view = renderSidebar();

fireEvent.click(view.getByRole("button", { name: "Project options for demo-project" }));
fireEvent.click(view.getByRole("button", { name: "Add sub-folder" }));

await waitFor(() => {
expect(createSection).toHaveBeenCalledWith(demoProjectPath, "New sub-folder");
});

let autoEditProps:
| (Parameters<typeof SectionHeaderModule.SectionHeader>[0] & {
onAutoCreateAbandon?: () => void;
autoStartEditing?: boolean;
})
| null = null;
const sectionHeaderCalls = (
SectionHeaderModule.SectionHeader as unknown as {
mock: {
calls: Array<[Parameters<typeof SectionHeaderModule.SectionHeader>[0]]>;
};
}
).mock.calls;
for (const [props] of sectionHeaderCalls) {
if (props.autoStartEditing) {
autoEditProps = props;
}
}

expect(autoEditProps?.autoStartEditing).toBe(true);
expect(typeof autoEditProps?.onAutoCreateAbandon).toBe("function");

autoEditProps?.onAutoCreateAbandon?.();

await waitFor(() => {
expect(confirmDialogMock).toHaveBeenCalledWith({
title: "Delete section?",
description: "1 workspace(s) in this section will be moved to unsectioned.",
confirmLabel: "Delete",
confirmVariant: "destructive",
});
expect(removeSection).toHaveBeenCalledWith(demoProjectPath, "new-section");
});
});

test("marks section attention when a promoted draft workspace needs attention", () => {
const promotedWorkspace = {
...createWorkspace("promoted-workspace", { title: "Promoted workspace" }),
sectionId: "section-1",
isInitializing: true,
};

projectContextValue = createProjectContextValue({
userProjects: new Map([
[
demoProjectPath,
{
workspaces: [
{
path: `${demoProjectPath}/promoted-workspace`,
sectionId: "section-1",
},
],
sections: [{ id: "section-1", name: "Section 1", color: "#6B7280", nextId: null }],
},
],
]),
});

spyOn(WorkspaceContextModule, "useWorkspaceActions").mockImplementation(
() =>
({
selectedWorkspace: null,
setSelectedWorkspace: () => undefined,
preflightArchiveWorkspace: preflightArchiveWorkspaceMock,
archiveWorkspace: archiveWorkspaceActionMock,
removeWorkspace: () => Promise.resolve({ success: true }),
updateWorkspaceTitle: () => Promise.resolve({ success: true }),
refreshWorkspaceMetadata: () => Promise.resolve(),
pendingNewWorkspaceProject: null,
pendingNewWorkspaceDraftId: null,
workspaceDraftsByProject: {
[demoProjectPath]: [
{
draftId: "draft-promoted",
sectionId: "section-1",
createdAt: Date.now(),
},
],
},
workspaceDraftPromotionsByProject: {
[demoProjectPath]: {
"draft-promoted": promotedWorkspace,
},
},
createWorkspaceDraft: () => undefined,
openWorkspaceDraft: () => undefined,
deleteWorkspaceDraft: () => undefined,
}) as unknown as ReturnType<typeof WorkspaceContextModule.useWorkspaceActions>
);

render(
<ProjectSidebar
collapsed={false}
onToggleCollapsed={() => undefined}
sortedWorkspacesByProject={new Map([[demoProjectPath, [promotedWorkspace]]])}
workspaceRecency={{}}
/>
);

const sectionHeaderCalls = (
SectionHeaderModule.SectionHeader as unknown as {
mock: {
calls: Array<[Parameters<typeof SectionHeaderModule.SectionHeader>[0]]>;
};
}
).mock.calls;
const sectionProps = sectionHeaderCalls
.map(([props]) => props)
.find((props) => props.section.id === "section-1");

expect(sectionProps?.hasAttention).toBe(true);
});

test("supports inline project name editing with Enter, Escape, and empty-to-null commit", async () => {
const updateDisplayName = mock(() => resolveVoidResult());
projectContextValue = createProjectContextValue({
Expand Down Expand Up @@ -1860,7 +1670,6 @@ describe("ProjectSidebar project actions menu", () => {
[demoProjectPath]: [
{
draftId: "draft-hidden-empty",
sectionId: null,
createdAt: Date.now(),
},
],
Expand Down Expand Up @@ -1896,7 +1705,6 @@ describe("ProjectSidebar project actions menu", () => {
[demoProjectPath]: [
{
draftId,
sectionId: null,
createdAt: Date.now(),
},
],
Expand Down
Loading
Loading