Skip to content
Merged
8 changes: 5 additions & 3 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2330,7 +2330,7 @@ export default function Sidebar() {
const activeEnvironmentId = useStore((store) => store.activeEnvironmentId);
const projectExpandedById = useUiStateStore((store) => store.projectExpandedById);
const projectOrder = useUiStateStore((store) => store.projectOrder);
const reorderProjects = useUiStateStore((store) => store.reorderProjects);
const reorderProjectGroup = useUiStateStore((store) => store.reorderProjectGroup);
const navigate = useNavigate();
const pathname = useLocation({ select: (loc) => loc.pathname });
const isOnSettings = pathname.startsWith("/settings");
Expand Down Expand Up @@ -2696,9 +2696,11 @@ export default function Sidebar() {
const activeProject = sidebarProjects.find((project) => project.projectKey === active.id);
const overProject = sidebarProjects.find((project) => project.projectKey === over.id);
if (!activeProject || !overProject) return;
reorderProjects(activeProject.projectKey, overProject.projectKey);
const activeMemberKeys = activeProject.memberProjectRefs.map(scopedProjectKey);
const overMemberKeys = overProject.memberProjectRefs.map(scopedProjectKey);
reorderProjectGroup(activeMemberKeys, overMemberKeys);
},
[sidebarProjectSortOrder, reorderProjects, sidebarProjects],
[sidebarProjectSortOrder, reorderProjectGroup, sidebarProjects],
);

const handleProjectDragStart = useCallback(
Expand Down
76 changes: 76 additions & 0 deletions apps/web/src/uiStateStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import {
clearThreadUi,
markThreadUnread,
reorderProjectGroup,
reorderProjects,
setProjectExpanded,
setThreadChangedFilesExpanded,
Expand Down Expand Up @@ -63,6 +64,81 @@ describe("uiStateStore pure functions", () => {
expect(next.projectOrder).toEqual([project2, project3, project1]);
});

it("reorderProjects is a no-op when dragged key is not in projectOrder", () => {
const project1 = ProjectId.make("project-1");
const project2 = ProjectId.make("project-2");
const initialState = makeUiState({
projectOrder: [project1, project2],
});

const next = reorderProjects(initialState, ProjectId.make("missing"), project2);

expect(next).toBe(initialState);
});

it("reorderProjectGroup moves a single-member group to a target position", () => {
const project1 = ProjectId.make("project-1");
const project2 = ProjectId.make("project-2");
const project3 = ProjectId.make("project-3");
const initialState = makeUiState({
projectOrder: [project1, project2, project3],
});

const next = reorderProjectGroup(initialState, [project1], [project3]);

expect(next.projectOrder).toEqual([project2, project3, project1]);
});

it("reorderProjectGroup moves all member keys of a multi-member group together", () => {
const keyALocal = "env-local:proj-a";
const keyARemote = "env-remote:proj-a";
const keyB = "env-local:proj-b";
const keyC = "env-local:proj-c";
const initialState = makeUiState({
projectOrder: [keyALocal, keyARemote, keyB, keyC],
});

const next = reorderProjectGroup(initialState, [keyALocal, keyARemote], [keyC]);

expect(next.projectOrder).toEqual([keyB, keyC, keyALocal, keyARemote]);
});

it("reorderProjectGroup handles member keys scattered across projectOrder", () => {
const keyALocal = "env-local:proj-a";
const keyB = "env-local:proj-b";
const keyARemote = "env-remote:proj-a";
const keyC = "env-local:proj-c";
const initialState = makeUiState({
projectOrder: [keyALocal, keyB, keyARemote, keyC],
});

const next = reorderProjectGroup(initialState, [keyALocal, keyARemote], [keyC]);

expect(next.projectOrder).toEqual([keyB, keyC, keyALocal, keyARemote]);
});

it("reorderProjectGroup is a no-op when dragged group equals target group", () => {
const key1 = "env-local:proj-a";
const key2 = "env-remote:proj-a";
const initialState = makeUiState({
projectOrder: [key1, key2, "env-local:proj-b"],
});

const next = reorderProjectGroup(initialState, [key1, key2], [key1, key2]);

expect(next).toBe(initialState);
});

it("reorderProjectGroup is a no-op when dragged keys are not in projectOrder", () => {
const initialState = makeUiState({
projectOrder: ["env-local:proj-a", "env-local:proj-b"],
});

const next = reorderProjectGroup(initialState, ["env-local:missing"], ["env-local:proj-b"]);

expect(next).toBe(initialState);
});

it("syncProjects preserves current project order during snapshot recovery", () => {
const project1 = ProjectId.make("project-1");
const project2 = ProjectId.make("project-2");
Expand Down
44 changes: 44 additions & 0 deletions apps/web/src/uiStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,44 @@ export function reorderProjects(
};
}

export function reorderProjectGroup(
state: UiState,
draggedProjectIds: readonly string[],
targetProjectIds: readonly string[],
): UiState {
if (draggedProjectIds.length === 0) {
return state;
}
const draggedSet = new Set(draggedProjectIds);
const targetSet = new Set(targetProjectIds);
if (draggedProjectIds.every((id) => targetSet.has(id))) {
return state;
}

const originalTargetIndex = state.projectOrder.findIndex((id) => targetSet.has(id));
if (originalTargetIndex < 0) {
return state;
}

const projectOrder = [...state.projectOrder];

const removed: string[] = [];
for (let i = projectOrder.length - 1; i >= 0; i--) {
if (draggedSet.has(projectOrder[i]!)) {
removed.unshift(projectOrder.splice(i, 1)[0]!);
}
}
if (removed.length === 0) {
return state;
}

projectOrder.splice(originalTargetIndex, 0, ...removed);
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
return {
...state,
projectOrder,
};
}

interface UiStateStore extends UiState {
syncProjects: (projects: readonly SyncProjectInput[]) => void;
syncThreads: (threads: readonly SyncThreadInput[]) => void;
Expand All @@ -515,6 +553,10 @@ interface UiStateStore extends UiState {
toggleProject: (projectId: string) => void;
setProjectExpanded: (projectId: string, expanded: boolean) => void;
reorderProjects: (draggedProjectId: string, targetProjectId: string) => void;
reorderProjectGroup: (
draggedProjectIds: readonly string[],
targetProjectIds: readonly string[],
) => void;
}

export const useUiStateStore = create<UiStateStore>((set) => ({
Expand All @@ -533,6 +575,8 @@ export const useUiStateStore = create<UiStateStore>((set) => ({
set((state) => setProjectExpanded(state, projectId, expanded)),
reorderProjects: (draggedProjectId, targetProjectId) =>
set((state) => reorderProjects(state, draggedProjectId, targetProjectId)),
reorderProjectGroup: (draggedProjectIds, targetProjectIds) =>
set((state) => reorderProjectGroup(state, draggedProjectIds, targetProjectIds)),
}));

useUiStateStore.subscribe((state) => debouncedPersistState.maybeExecute(state));
Expand Down
Loading