Skip to content

Commit 87c3b95

Browse files
committed
fix: handle cross-panel tab drops on tab bar and other tabs
When dragging a tab from one pane to another via the tab bar (instead of the drop zones), the handler only processed drops with type "panel". Drops on "tab-bar" and cross-panel "tab" targets were ignored, causing the source pane to persist empty while only the visual tab moved.
1 parent 9e71f7f commit 87c3b95

File tree

2 files changed

+83
-18
lines changed

2 files changed

+83
-18
lines changed

apps/array/src/renderer/features/panels/hooks/useDragDropHandlers.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -67,32 +67,50 @@ export const useDragDropHandlers = (taskId: string) => {
6767
const sourceData = event.operation.source?.data;
6868
const targetData = event.operation.target?.data;
6969

70-
// Tab reordering within same panel is handled by onDragOver
71-
// Here we only handle cross-panel moves and splits
72-
73-
// Handle panel splitting/moving
7470
if (
7571
sourceData?.type !== "tab" ||
76-
targetData?.type !== "panel" ||
7772
!sourceData.tabId ||
78-
!sourceData.panelId ||
79-
!targetData.panelId ||
80-
!targetData.zone
73+
!sourceData.panelId
8174
) {
8275
return;
8376
}
8477

8578
const { tabId, panelId: sourcePanelId } = sourceData;
86-
const { panelId: targetPanelId, zone } = targetData;
87-
88-
if (zone === "center") {
89-
moveTab(taskId, tabId, sourcePanelId, targetPanelId);
90-
setFocusedPanel(taskId, targetPanelId);
91-
} else if (isSplitDirection(zone)) {
92-
splitPanel(taskId, tabId, sourcePanelId, targetPanelId, zone);
93-
// For splits, the new panel gets a generated ID, so we can't easily focus it here
94-
// The target panel remains focused which is reasonable behavior
95-
setFocusedPanel(taskId, targetPanelId);
79+
80+
// Handle drop on panel drop zones (center or split directions)
81+
if (targetData?.type === "panel" && targetData.panelId && targetData.zone) {
82+
const { panelId: targetPanelId, zone } = targetData;
83+
84+
if (zone === "center") {
85+
moveTab(taskId, tabId, sourcePanelId, targetPanelId);
86+
setFocusedPanel(taskId, targetPanelId);
87+
} else if (isSplitDirection(zone)) {
88+
splitPanel(taskId, tabId, sourcePanelId, targetPanelId, zone);
89+
setFocusedPanel(taskId, targetPanelId);
90+
}
91+
return;
92+
}
93+
94+
// Handle drop on tab bar (cross-panel move)
95+
if (
96+
targetData?.type === "tab-bar" &&
97+
targetData.panelId &&
98+
targetData.panelId !== sourcePanelId
99+
) {
100+
moveTab(taskId, tabId, sourcePanelId, targetData.panelId);
101+
setFocusedPanel(taskId, targetData.panelId);
102+
return;
103+
}
104+
105+
// Handle drop on another tab in a different panel (cross-panel move)
106+
if (
107+
targetData?.type === "tab" &&
108+
targetData.panelId &&
109+
targetData.panelId !== sourcePanelId
110+
) {
111+
moveTab(taskId, tabId, sourcePanelId, targetData.panelId);
112+
setFocusedPanel(taskId, targetData.panelId);
113+
return;
96114
}
97115
};
98116

apps/array/src/renderer/features/panels/store/panelLayoutStore.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,53 @@ describe("panelLayoutStore", () => {
512512
const updatedMainPanel = getNestedPanel("task-1", 0);
513513
expect(updatedMainPanel.type).toBe("leaf");
514514
});
515+
516+
it("removes split pane when moving its only tab to another pane", () => {
517+
// Create a split: main-panel becomes a group with [original, new-panel]
518+
usePanelLayoutStore
519+
.getState()
520+
.splitPanel(
521+
"task-1",
522+
"file-src/App.tsx",
523+
"main-panel",
524+
"main-panel",
525+
"right",
526+
);
527+
528+
// Verify we have a split with 2 panels
529+
const mainPanelNode = getNestedPanel("task-1", 0);
530+
expect(mainPanelNode.type).toBe("group");
531+
if (mainPanelNode.type !== "group") return;
532+
expect(mainPanelNode.children).toHaveLength(2);
533+
534+
// Get the new panel (contains file-src/App.tsx)
535+
const newPanel = mainPanelNode.children[1];
536+
expect(newPanel.type).toBe("leaf");
537+
if (newPanel.type !== "leaf") return;
538+
539+
// Original panel (contains logs and file-src/Other.tsx)
540+
const originalPanel = mainPanelNode.children[0];
541+
expect(originalPanel.type).toBe("leaf");
542+
if (originalPanel.type !== "leaf") return;
543+
544+
// Move the tab from the new panel back to the original panel
545+
usePanelLayoutStore
546+
.getState()
547+
.moveTab("task-1", "file-src/App.tsx", newPanel.id, originalPanel.id);
548+
549+
// The split should be collapsed since new panel is now empty
550+
const updatedMainPanel = getNestedPanel("task-1", 0);
551+
expect(updatedMainPanel.type).toBe("leaf");
552+
553+
// The tab should now be in the original panel
554+
if (updatedMainPanel.type === "leaf") {
555+
expect(
556+
updatedMainPanel.content.tabs.some(
557+
(t) => t.id === "file-src/App.tsx",
558+
),
559+
).toBe(true);
560+
}
561+
});
515562
});
516563

517564
describe("preview tabs", () => {

0 commit comments

Comments
 (0)