Skip to content
Closed
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
235 changes: 234 additions & 1 deletion apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { workspaceSections, workspaces } from "@superset/local-db";
import { eq, inArray } from "drizzle-orm";
import { and, eq, inArray, isNull } from "drizzle-orm";
import { localDb } from "main/lib/local-db";
import {
PROJECT_COLOR_DEFAULT,
Expand All @@ -8,6 +8,7 @@ import {
import { z } from "zod";
import { publicProcedure, router } from "../../..";
import { getMaxProjectChildTabOrder } from "../utils/db-helpers";
import { getProjectChildItems } from "../utils/project-children-order";
import { reorderItems } from "../utils/reorder";

const SECTION_COLORS = PROJECT_COLORS.filter(
Expand All @@ -19,6 +20,150 @@ function randomSectionColor(): string {
.value;
}

function normalizeSectionWorkspaceOrder(sectionId: string): void {
const sectionWorkspaces = localDb
.select()
.from(workspaces)
.where(eq(workspaces.sectionId, sectionId))
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Filter out soft-deleting workspaces when reordering section items; otherwise targetIndex can map to the wrong visible position.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/workspaces/procedures/sections.ts, line 27:

<comment>Filter out soft-deleting workspaces when reordering section items; otherwise `targetIndex` can map to the wrong visible position.</comment>

<file context>
@@ -19,6 +20,150 @@ function randomSectionColor(): string {
+	const sectionWorkspaces = localDb
+		.select()
+		.from(workspaces)
+		.where(eq(workspaces.sectionId, sectionId))
+		.all()
+		.sort((a, b) => a.tabOrder - b.tabOrder);
</file context>
Suggested change
.where(eq(workspaces.sectionId, sectionId))
.where(
and(eq(workspaces.sectionId, sectionId), isNull(workspaces.deletingAt)),
)
Fix with Cubic

.all()
.sort((a, b) => a.tabOrder - b.tabOrder);

for (const [index, workspace] of sectionWorkspaces.entries()) {
localDb
.update(workspaces)
.set({ tabOrder: index })
.where(eq(workspaces.id, workspace.id))
.run();
}
}

function persistProjectChildOrder(
items: ReturnType<typeof getProjectChildItems>,
): void {
for (const item of items) {
if (item.kind === "workspace") {
localDb
.update(workspaces)
.set({ tabOrder: item.tabOrder })
.where(eq(workspaces.id, item.id))
.run();
continue;
}

localDb
.update(workspaceSections)
.set({ tabOrder: item.tabOrder })
.where(eq(workspaceSections.id, item.id))
.run();
}
}

function normalizeProjectChildOrder(projectId: string): void {
const projectWorkspaces = localDb
.select()
.from(workspaces)
.where(
and(eq(workspaces.projectId, projectId), isNull(workspaces.deletingAt)),
)
.all();
const projectSections = localDb
.select()
.from(workspaceSections)
.where(eq(workspaceSections.projectId, projectId))
.all();
const items = getProjectChildItems(
projectId,
projectWorkspaces,
projectSections,
);

for (const [index, item] of items.entries()) {
item.tabOrder = index;
}

persistProjectChildOrder(items);
}

function reorderProjectChildOrderWithWorkspace(
projectId: string,
workspaceId: string,
targetIndex: number,
): void {
const projectWorkspaces = localDb
.select()
.from(workspaces)
.where(
and(eq(workspaces.projectId, projectId), isNull(workspaces.deletingAt)),
)
.all();
const projectSections = localDb
.select()
.from(workspaceSections)
.where(eq(workspaceSections.projectId, projectId))
.all();
const items = getProjectChildItems(
projectId,
projectWorkspaces,
projectSections,
);
const currentIndex = items.findIndex(
(item) => item.kind === "workspace" && item.id === workspaceId,
);

if (currentIndex === -1) {
throw new Error(
`Workspace ${workspaceId} not found in project ${projectId}`,
);
}

const [moved] = items.splice(currentIndex, 1);
const clampedTargetIndex = Math.max(0, Math.min(targetIndex, items.length));
items.splice(clampedTargetIndex, 0, moved);

for (const [index, item] of items.entries()) {
item.tabOrder = index;
}

persistProjectChildOrder(items);
}

function reorderSectionWithWorkspace(
sectionId: string,
workspaceId: string,
targetIndex: number,
): void {
const sectionWorkspaces = localDb
.select()
.from(workspaces)
.where(eq(workspaces.sectionId, sectionId))
.all()
.sort((a, b) => a.tabOrder - b.tabOrder);
const currentIndex = sectionWorkspaces.findIndex(
(workspace) => workspace.id === workspaceId,
);

if (currentIndex === -1) {
throw new Error(
`Workspace ${workspaceId} not found in section ${sectionId}`,
);
}

const [moved] = sectionWorkspaces.splice(currentIndex, 1);
const clampedTargetIndex = Math.max(
0,
Math.min(targetIndex, sectionWorkspaces.length),
);
sectionWorkspaces.splice(clampedTargetIndex, 0, moved);

for (const [index, workspace] of sectionWorkspaces.entries()) {
localDb
.update(workspaces)
.set({ tabOrder: index })
.where(eq(workspaces.id, workspace.id))
.run();
}
}

export const createSectionsProcedures = () => {
return router({
createSection: publicProcedure
Expand Down Expand Up @@ -267,5 +412,93 @@ export const createSectionsProcedures = () => {

return { success: true };
}),

moveWorkspaceToSectionAtIndex: publicProcedure
.input(
z.object({
workspaceId: z.string(),
sectionId: z.string().nullable(),
targetIndex: z.number().int().nonnegative(),
}),
)
.mutation(({ input }) => {
const workspace = localDb
.select()
.from(workspaces)
.where(eq(workspaces.id, input.workspaceId))
.get();

if (!workspace) {
throw new Error(`Workspace ${input.workspaceId} not found`);
}

if (input.sectionId) {
const section = localDb
.select()
.from(workspaceSections)
.where(eq(workspaceSections.id, input.sectionId))
.get();

if (!section) {
throw new Error(`Section ${input.sectionId} not found`);
}

if (section.projectId !== workspace.projectId) {
throw new Error(
"Cannot move workspace to a section in a different project",
);
}
}

const sourceSectionId = workspace.sectionId ?? null;

if (sourceSectionId === input.sectionId) {
if (input.sectionId === null) {
reorderProjectChildOrderWithWorkspace(
workspace.projectId,
workspace.id,
input.targetIndex,
);
} else {
reorderSectionWithWorkspace(
input.sectionId,
workspace.id,
input.targetIndex,
);
}

return { success: true };
}

localDb
.update(workspaces)
.set({ sectionId: input.sectionId })
.where(eq(workspaces.id, input.workspaceId))
.run();

if (sourceSectionId === null && input.sectionId !== null) {
normalizeProjectChildOrder(workspace.projectId);
}

if (sourceSectionId !== null && sourceSectionId !== input.sectionId) {
normalizeSectionWorkspaceOrder(sourceSectionId);
}

if (input.sectionId === null) {
reorderProjectChildOrderWithWorkspace(
workspace.projectId,
workspace.id,
input.targetIndex,
);
} else {
reorderSectionWithWorkspace(
input.sectionId,
workspace.id,
input.targetIndex,
);
}

return { success: true };
}),
});
};
1 change: 1 addition & 0 deletions apps/desktop/src/renderer/react-query/workspaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { useHandleOpenedWorktree } from "./useHandleOpenedWorktree";
export { useImportAllWorktrees } from "./useImportAllWorktrees";
export { useMoveWorkspacesToSection } from "./useMoveWorkspacesToSection";
export { useMoveWorkspaceToSection } from "./useMoveWorkspaceToSection";
export { useMoveWorkspaceToSectionAtIndex } from "./useMoveWorkspaceToSectionAtIndex";
export { useOpenExternalWorktree } from "./useOpenExternalWorktree";
export { useOpenMainRepoWorkspace } from "./useOpenMainRepoWorkspace";
export { useOpenTrackedWorktree } from "./useOpenTrackedWorktree";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { toast } from "@superset/ui/sonner";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { invalidateWorkspaceQueries } from "./invalidateWorkspaceQueries";

export function useMoveWorkspaceToSectionAtIndex() {
const utils = electronTrpc.useUtils();

return electronTrpc.workspaces.moveWorkspaceToSectionAtIndex.useMutation({
onSuccess: () => invalidateWorkspaceQueries(utils),
onError: (error) =>
toast.error(`Failed to move workspace: ${error.message}`),
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export function ProjectSection({
canAccept: (item) =>
item.sectionId !== null && item.projectId === projectId,
targetSectionId: null,
getTargetIndex: () => topLevelChildren.length,
});

const handleNewWorkspace = () => {
Expand Down Expand Up @@ -312,19 +313,21 @@ export function ProjectSection({
className="overflow-hidden"
>
<div className="pb-1">
{topLevelChildren.length === 0 && (
<div
{...ungroupedDropZone.handlers}
className={cn(
"transition-colors rounded-sm min-h-8",
ungroupedDropZone.isDropTarget &&
!ungroupedDropZone.isDragOver &&
"border border-dashed border-primary/20",
ungroupedDropZone.isDragOver &&
"bg-primary/5 border-solid border-primary/30",
)}
/>
)}
<div
{...ungroupedDropZone.handlers}
className={cn(
"transition-colors rounded-sm",
(topLevelChildren.length === 0 ||
ungroupedDropZone.isDropTarget ||
ungroupedDropZone.isDragOver) &&
"min-h-8",
ungroupedDropZone.isDropTarget &&
!ungroupedDropZone.isDragOver &&
"border border-dashed border-primary/20",
ungroupedDropZone.isDragOver &&
"bg-primary/5 border-solid border-primary/30",
Comment on lines +327 to +328
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit border width in drag-over state

At Line 326-327, the drag-over classes set border style/color but not width, so the border cue may not render. Add border to make the drop feedback consistently visible.

Suggested patch
  ungroupedDropZone.isDragOver &&
-   "bg-primary/5 border-solid border-primary/30",
+   "bg-primary/5 border border-solid border-primary/30",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ungroupedDropZone.isDragOver &&
"bg-primary/5 border-solid border-primary/30",
ungroupedDropZone.isDragOver &&
"bg-primary/5 border border-solid border-primary/30",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx`
around lines 326 - 327, The drag-over classes on the ungrouped drop zone set
border style and color but omit border width, so the visual cue can be
invisible; update the conditional class string where
ungroupedDropZone.isDragOver is used in ProjectSection (the JSX that builds the
drop-zone classes) to include the "border" utility (e.g., add "border" alongside
"border-solid border-primary/30") so the border appears when isDragOver is true.

)}
/>
{topLevelChildren.map((item) =>
item.kind === "workspace" ? (
<WorkspaceListItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function WorkspaceListItem({
}
}, [isActive]);

const { isDragging, drag, drop } = useWorkspaceDnD({
const { isDragging, setNodeRef } = useWorkspaceDnD({
id,
projectId,
sectionId,
Expand Down Expand Up @@ -254,7 +254,7 @@ export function WorkspaceListItem({
tabIndex={0}
ref={(node) => {
itemRef.current = node;
drag(drop(node));
setNodeRef(node);
}}
onClick={handleClick}
onKeyDown={(e) => {
Expand Down
Loading
Loading