Skip to content

Commit 3cd63c9

Browse files
committed
close, close others, close to the right functionality
1 parent d017f99 commit 3cd63c9

File tree

11 files changed

+198
-7
lines changed

11 files changed

+198
-7
lines changed

apps/array/src/main/preload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
} from "../shared/types/oauth";
77
import type {
88
FolderContextMenuResult,
9+
TabContextMenuResult,
910
TaskContextMenuResult,
1011
} from "./services/contextMenu.types.js";
1112

@@ -328,6 +329,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
328329
folderName: string,
329330
): Promise<FolderContextMenuResult> =>
330331
ipcRenderer.invoke("show-folder-context-menu", folderId, folderName),
332+
showTabContextMenu: (canClose: boolean): Promise<TabContextMenuResult> =>
333+
ipcRenderer.invoke("show-tab-context-menu", canClose),
331334
folders: {
332335
getFolders: (): Promise<
333336
Array<{

apps/array/src/main/services/contextMenu.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { Menu, type MenuItemConstructorOptions } from "electron";
22
import { createIpcService } from "../ipc/createIpcService.js";
33
import type {
44
FolderContextMenuResult,
5+
TabContextMenuResult,
56
TaskContextMenuResult,
67
} from "./contextMenu.types.js";
78

89
export type {
910
FolderContextMenuAction,
1011
FolderContextMenuResult,
12+
TabContextMenuAction,
13+
TabContextMenuResult,
1114
TaskContextMenuAction,
1215
TaskContextMenuResult,
1316
} from "./contextMenu.types.js";
@@ -66,3 +69,32 @@ export const showFolderContextMenuService = createIpcService({
6669
});
6770
},
6871
});
72+
73+
export const showTabContextMenuService = createIpcService({
74+
channel: "show-tab-context-menu",
75+
handler: async (_event, canClose: boolean): Promise<TabContextMenuResult> => {
76+
return new Promise((resolve) => {
77+
const template: MenuItemConstructorOptions[] = [
78+
{
79+
label: "Close tab",
80+
enabled: canClose,
81+
click: () => resolve({ action: "close" }),
82+
},
83+
{ type: "separator" },
84+
{
85+
label: "Close other tabs",
86+
click: () => resolve({ action: "close-others" }),
87+
},
88+
{
89+
label: "Close tabs to the right",
90+
click: () => resolve({ action: "close-right" }),
91+
},
92+
];
93+
94+
const menu = Menu.buildFromTemplate(template);
95+
menu.popup({
96+
callback: () => resolve({ action: null }),
97+
});
98+
});
99+
},
100+
});

apps/array/src/main/services/contextMenu.types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ export type TaskContextMenuAction = "rename" | "duplicate" | "delete" | null;
22

33
export type FolderContextMenuAction = "remove" | null;
44

5+
export type TabContextMenuAction =
6+
| "close"
7+
| "close-others"
8+
| "close-right"
9+
| null;
10+
511
export interface TaskContextMenuResult {
612
action: TaskContextMenuAction;
713
}
@@ -10,6 +16,10 @@ export interface FolderContextMenuResult {
1016
action: FolderContextMenuAction;
1117
}
1218

19+
export interface TabContextMenuResult {
20+
action: TabContextMenuAction;
21+
}
22+
1323
declare global {
1424
interface IElectronAPI {
1525
showTaskContextMenu: (

apps/array/src/renderer/features/panels/components/DraggableTab.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ import { useSortable } from "@dnd-kit/react/sortable";
22
import { Cross2Icon } from "@radix-ui/react-icons";
33
import { Box, Flex, IconButton, Text } from "@radix-ui/themes";
44
import type React from "react";
5+
import { useCallback } from "react";
56

67
interface DraggableTabProps {
78
tabId: string;
89
panelId: string;
910
label: string;
1011
isActive: boolean;
1112
index: number;
13+
closeable?: boolean;
1214
onSelect: () => void;
1315
onClose?: () => void;
16+
onCloseOthers?: () => void;
17+
onCloseToRight?: () => void;
1418
icon?: React.ReactNode;
1519
badge?: React.ReactNode;
1620
hasUnsavedChanges?: boolean;
@@ -22,8 +26,11 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
2226
label,
2327
isActive,
2428
index,
29+
closeable = true,
2530
onSelect,
2631
onClose,
32+
onCloseOthers,
33+
onCloseToRight,
2734
icon,
2835
badge,
2936
hasUnsavedChanges,
@@ -39,6 +46,25 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
3946
data: { tabId, panelId, type: "tab" },
4047
});
4148

49+
const handleContextMenu = useCallback(
50+
async (e: React.MouseEvent) => {
51+
e.preventDefault();
52+
const result = await window.electronAPI.showTabContextMenu(closeable);
53+
switch (result.action) {
54+
case "close":
55+
onClose?.();
56+
break;
57+
case "close-others":
58+
onCloseOthers?.();
59+
break;
60+
case "close-right":
61+
onCloseToRight?.();
62+
break;
63+
}
64+
},
65+
[closeable, onClose, onCloseOthers, onCloseToRight],
66+
);
67+
4268
return (
4369
<Flex
4470
ref={ref}
@@ -50,14 +76,15 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
5076
px="4"
5177
className="group relative cursor-grab select-none border-r border-b-2 transition-colors"
5278
style={{
53-
backgroundColor: isActive ? "var(--accent-3)" : "transparent",
79+
backgroundColor: "transparent",
5480
borderRightColor: "var(--gray-6)",
55-
borderBottomColor: isActive ? "var(--accent-8)" : "transparent",
81+
borderBottomColor: isActive ? "var(--accent-10)" : "transparent",
5682
color: isActive ? "var(--accent-12)" : "var(--gray-11)",
5783
opacity: isDragging ? 0.5 : 1,
58-
minHeight: "40px",
84+
minHeight: "32px",
5985
}}
6086
onClick={onSelect}
87+
onContextMenu={handleContextMenu}
6188
onMouseEnter={(e) => {
6289
if (!isActive) {
6390
e.currentTarget.style.backgroundColor = "var(--gray-3)";

apps/array/src/renderer/features/panels/components/LeafNodeRenderer.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface LeafNodeRendererProps {
99
taskId: string;
1010
task: Task;
1111
closeTab: (taskId: string, panelId: string, tabId: string) => void;
12+
closeOtherTabs: (panelId: string, tabId: string) => void;
13+
closeTabsToRight: (panelId: string, tabId: string) => void;
1214
draggingTabId: string | null;
1315
draggingTabPanelId: string | null;
1416
onActiveTabChange: (panelId: string, tabId: string) => void;
@@ -19,6 +21,8 @@ export const LeafNodeRenderer: React.FC<LeafNodeRendererProps> = ({
1921
taskId,
2022
task,
2123
closeTab,
24+
closeOtherTabs,
25+
closeTabsToRight,
2226
draggingTabId,
2327
draggingTabPanelId,
2428
onActiveTabChange,
@@ -41,6 +45,8 @@ export const LeafNodeRenderer: React.FC<LeafNodeRendererProps> = ({
4145
panelId={node.id}
4246
content={contentWithComponents}
4347
onActiveTabChange={onActiveTabChange}
48+
onCloseOtherTabs={closeOtherTabs}
49+
onCloseTabsToRight={closeTabsToRight}
4450
draggingTabId={draggingTabId}
4551
draggingTabPanelId={draggingTabPanelId}
4652
/>

apps/array/src/renderer/features/panels/components/PanelLayout.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ const PanelLayoutRenderer: React.FC<{
3535
[layoutState, taskId],
3636
);
3737

38+
const handleCloseOtherTabs = useCallback(
39+
(panelId: string, tabId: string) => {
40+
layoutState.closeOtherTabs(taskId, panelId, tabId);
41+
},
42+
[layoutState, taskId],
43+
);
44+
45+
const handleCloseTabsToRight = useCallback(
46+
(panelId: string, tabId: string) => {
47+
layoutState.closeTabsToRight(taskId, panelId, tabId);
48+
},
49+
[layoutState, taskId],
50+
);
51+
3852
const handleLayout = useCallback(
3953
(groupId: string, sizes: number[]) => {
4054
layoutState.updateSizes(taskId, groupId, sizes);
@@ -51,6 +65,8 @@ const PanelLayoutRenderer: React.FC<{
5165
taskId={taskId}
5266
task={task}
5367
closeTab={layoutState.closeTab}
68+
closeOtherTabs={handleCloseOtherTabs}
69+
closeTabsToRight={handleCloseTabsToRight}
5470
draggingTabId={layoutState.draggingTabId}
5571
draggingTabPanelId={layoutState.draggingTabPanelId}
5672
onActiveTabChange={handleSetActiveTab}
@@ -71,7 +87,16 @@ const PanelLayoutRenderer: React.FC<{
7187

7288
return null;
7389
},
74-
[taskId, task, layoutState, handleSetActiveTab, setGroupRef, handleLayout],
90+
[
91+
taskId,
92+
task,
93+
layoutState,
94+
handleSetActiveTab,
95+
handleCloseOtherTabs,
96+
handleCloseTabsToRight,
97+
setGroupRef,
98+
handleLayout,
99+
],
75100
);
76101

77102
return <>{renderNode(node)}</>;

apps/array/src/renderer/features/panels/components/PanelTab.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ interface PanelTabProps {
99
isActive: boolean;
1010
index: number;
1111
draggable?: boolean;
12+
closeable?: boolean;
1213
onSelect: () => void;
1314
onClose?: () => void;
15+
onCloseOthers?: () => void;
16+
onCloseToRight?: () => void;
1417
icon?: React.ReactNode;
1518
badge?: React.ReactNode;
1619
hasUnsavedChanges?: boolean;
@@ -23,8 +26,11 @@ export const PanelTab: React.FC<PanelTabProps> = ({
2326
isActive,
2427
index,
2528
draggable = true,
29+
closeable = true,
2630
onSelect,
2731
onClose,
32+
onCloseOthers,
33+
onCloseToRight,
2834
icon,
2935
badge,
3036
hasUnsavedChanges,
@@ -49,8 +55,11 @@ export const PanelTab: React.FC<PanelTabProps> = ({
4955
label={label}
5056
isActive={isActive}
5157
index={index}
58+
closeable={closeable}
5259
onSelect={onSelect}
5360
onClose={onClose}
61+
onCloseOthers={onCloseOthers}
62+
onCloseToRight={onCloseToRight}
5463
icon={icon}
5564
badge={badge}
5665
hasUnsavedChanges={hasUnsavedChanges}

apps/array/src/renderer/features/panels/components/TabbedPanel.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface TabbedPanelProps {
99
panelId: string;
1010
content: PanelContent;
1111
onActiveTabChange?: (panelId: string, tabId: string) => void;
12+
onCloseOtherTabs?: (panelId: string, tabId: string) => void;
13+
onCloseTabsToRight?: (panelId: string, tabId: string) => void;
1214
draggingTabId?: string | null;
1315
draggingTabPanelId?: string | null;
1416
}
@@ -17,6 +19,8 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
1719
panelId,
1820
content,
1921
onActiveTabChange,
22+
onCloseOtherTabs,
23+
onCloseTabsToRight,
2024
draggingTabId = null,
2125
draggingTabPanelId = null,
2226
}) => {
@@ -43,8 +47,8 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
4347
className="flex-shrink-0 overflow-hidden border-b"
4448
style={{
4549
borderColor: "var(--gray-6)",
46-
minHeight: "40px",
47-
height: "40px",
50+
minHeight: "32px",
51+
height: "32px",
4852
}}
4953
>
5054
{content.tabs.map((tab, index) => (
@@ -56,6 +60,7 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
5660
isActive={tab.id === content.activeTabId}
5761
index={index}
5862
draggable={tab.draggable}
63+
closeable={tab.closeable !== false}
5964
onSelect={() => {
6065
onActiveTabChange?.(panelId, tab.id);
6166
tab.onSelect?.();
@@ -65,6 +70,8 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
6570
? () => handleCloseTab(tab.id)
6671
: undefined
6772
}
73+
onCloseOthers={() => onCloseOtherTabs?.(panelId, tab.id)}
74+
onCloseToRight={() => onCloseTabsToRight?.(panelId, tab.id)}
6875
icon={tab.icon}
6976
hasUnsavedChanges={tab.hasUnsavedChanges}
7077
badge={tab.badge}

apps/array/src/renderer/features/panels/hooks/usePanelLayoutHooks.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export interface PanelLayoutState {
1111
updateSizes: (taskId: string, groupId: string, sizes: number[]) => void;
1212
setActiveTab: (taskId: string, panelId: string, tabId: string) => void;
1313
closeTab: (taskId: string, panelId: string, tabId: string) => void;
14+
closeOtherTabs: (taskId: string, panelId: string, tabId: string) => void;
15+
closeTabsToRight: (taskId: string, panelId: string, tabId: string) => void;
1416
draggingTabId: string | null;
1517
draggingTabPanelId: string | null;
1618
}
@@ -22,6 +24,8 @@ export function usePanelLayoutState(taskId: string): PanelLayoutState {
2224
updateSizes: state.updateSizes,
2325
setActiveTab: state.setActiveTab,
2426
closeTab: state.closeTab,
27+
closeOtherTabs: state.closeOtherTabs,
28+
closeTabsToRight: state.closeTabsToRight,
2529
draggingTabId: state.getLayout(taskId)?.draggingTabId ?? null,
2630
draggingTabPanelId: state.getLayout(taskId)?.draggingTabPanelId ?? null,
2731
}),

0 commit comments

Comments
 (0)