Skip to content

Commit 7ee186c

Browse files
authored
feat(workspaces): context menu on tabs to duplicate and close all other tabs COMPASS-9397 (#7053)
* Add context menu to tab * Refactor closeTab action * Add menu item to close all other tabs * Add menu item to duplicate tab * Add unit tests for the store * Add unit tests to components package * Call cleanupRemovedTabs * Merge CloseTab and CloseAllOtherTabs actions into CloseTabs * Make closeAllOtherTabs confirm and close tabs one-by-one * Use cleanupRemovedTabs in closeTab too * Incorporate feedback from review
1 parent 1173a48 commit 7ee186c

File tree

7 files changed

+246
-23
lines changed

7 files changed

+246
-23
lines changed

packages/compass-components/src/components/workspace-tabs/tab.spec.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@ import { Tab } from './tab';
1313
describe('Tab', function () {
1414
let onCloseSpy: sinon.SinonSpy;
1515
let onSelectSpy: sinon.SinonSpy;
16+
let onDuplicateSpy: sinon.SinonSpy;
17+
let onCloseAllOthersSpy: sinon.SinonSpy;
1618

1719
beforeEach(function () {
1820
onCloseSpy = sinon.spy();
1921
onSelectSpy = sinon.spy();
22+
onDuplicateSpy = sinon.spy();
23+
onCloseAllOthersSpy = sinon.spy();
2024
});
2125

2226
afterEach(cleanup);
@@ -28,6 +32,8 @@ describe('Tab', function () {
2832
type="Databases"
2933
onClose={onCloseSpy}
3034
onSelect={onSelectSpy}
35+
onDuplicate={onDuplicateSpy}
36+
onCloseAllOthers={onCloseAllOthersSpy}
3137
title="docs"
3238
isSelected
3339
isDragging={false}
@@ -73,6 +79,8 @@ describe('Tab', function () {
7379
type="Databases"
7480
onClose={onCloseSpy}
7581
onSelect={onSelectSpy}
82+
onDuplicate={onDuplicateSpy}
83+
onCloseAllOthers={onCloseAllOthersSpy}
7684
title="docs"
7785
isSelected={false}
7886
isDragging={false}
@@ -98,4 +106,48 @@ describe('Tab', function () {
98106
).to.not.equal('none');
99107
});
100108
});
109+
110+
describe('when right-clicking', function () {
111+
beforeEach(function () {
112+
render(
113+
<Tab
114+
type="Databases"
115+
onClose={onCloseSpy}
116+
onSelect={onSelectSpy}
117+
onDuplicate={onDuplicateSpy}
118+
onCloseAllOthers={onCloseAllOthersSpy}
119+
title="docs"
120+
isSelected={false}
121+
isDragging={false}
122+
tabContentId="1"
123+
tooltip={[['Connection', 'ABC']]}
124+
iconGlyph="Folder"
125+
/>
126+
);
127+
});
128+
129+
describe('clicking menu items', function () {
130+
it('should propagate clicks on "Duplicate"', async function () {
131+
const tab = await screen.findByText('docs');
132+
userEvent.click(tab, { button: 2 });
133+
expect(screen.getByTestId('context-menu')).to.be.visible;
134+
135+
const menuItem = await screen.findByText('Duplicate');
136+
menuItem.click();
137+
expect(onDuplicateSpy.callCount).to.equal(1);
138+
expect(onCloseAllOthersSpy.callCount).to.equal(0);
139+
});
140+
141+
it('should propagate clicks on "Close all other tabs"', async function () {
142+
const tab = await screen.findByText('docs');
143+
userEvent.click(tab, { button: 2 });
144+
expect(screen.getByTestId('context-menu')).to.be.visible;
145+
146+
const menuItem = await screen.findByText('Close all other tabs');
147+
menuItem.click();
148+
expect(onDuplicateSpy.callCount).to.equal(0);
149+
expect(onCloseAllOthersSpy.callCount).to.equal(1);
150+
});
151+
});
152+
});
101153
});

packages/compass-components/src/components/workspace-tabs/tab.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { useSortable } from '@dnd-kit/sortable';
77
import { CSS as cssDndKit } from '@dnd-kit/utilities';
88
import { useId } from '@react-aria/utils';
99
import { useDarkMode } from '../../hooks/use-theme';
10-
import { Icon, IconButton } from '../leafygreen';
10+
import { Icon, IconButton, useMergeRefs } from '../leafygreen';
1111
import { mergeProps } from '../../utils/merge-props';
1212
import { useDefaultAction } from '../../hooks/use-default-action';
1313
import { LogoIcon } from '../icons/logo-icon';
1414
import { Tooltip } from '../leafygreen';
1515
import { ServerIcon } from '../icons/server-icon';
1616
import { useTabTheme } from './use-tab-theme';
17+
import { useContextMenuItems } from '../context-menu';
1718

1819
function focusedChild(className: string) {
1920
return `&:hover ${className}, &:focus-visible ${className}, &:focus-within:not(:focus) ${className}`;
@@ -192,7 +193,9 @@ export type WorkspaceTabCoreProps = {
192193
isSelected: boolean;
193194
isDragging: boolean;
194195
onSelect: () => void;
196+
onDuplicate: () => void;
195197
onClose: () => void;
198+
onCloseAllOthers: () => void;
196199
tabContentId: string;
197200
};
198201

@@ -207,7 +210,9 @@ function Tab({
207210
isSelected,
208211
isDragging,
209212
onSelect,
213+
onDuplicate,
210214
onClose,
215+
onCloseAllOthers,
211216
tabContentId,
212217
iconGlyph,
213218
className: tabClassName,
@@ -234,6 +239,16 @@ function Tab({
234239
return css(tabTheme);
235240
}, [tabTheme, darkMode]);
236241

242+
const contextMenuRef = useContextMenuItems(
243+
() => [
244+
{ label: 'Close all other tabs', onAction: onCloseAllOthers },
245+
{ label: 'Duplicate', onAction: onDuplicate },
246+
],
247+
[onCloseAllOthers, onDuplicate]
248+
);
249+
250+
const mergedRef = useMergeRefs([setNodeRef, contextMenuRef]);
251+
237252
const style = {
238253
transform: cssDndKit.Transform.toString(transform),
239254
transition,
@@ -251,7 +266,7 @@ function Tab({
251266
justify="start"
252267
trigger={
253268
<div
254-
ref={setNodeRef}
269+
ref={mergedRef}
255270
style={style}
256271
className={cx(
257272
tabStyles,

packages/compass-components/src/components/workspace-tabs/workspace-tabs.spec.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ describe('WorkspaceTabs', function () {
3636
let onSelectNextSpy: sinon.SinonSpy;
3737
let onSelectPrevSpy: sinon.SinonSpy;
3838
let onMoveTabSpy: sinon.SinonSpy;
39+
let onDuplicateSpy: sinon.SinonSpy;
40+
let onCloseAllOthersSpy: sinon.SinonSpy;
3941

4042
beforeEach(function () {
4143
onCreateNewTabSpy = sinon.spy();
@@ -44,6 +46,8 @@ describe('WorkspaceTabs', function () {
4446
onSelectNextSpy = sinon.spy();
4547
onSelectPrevSpy = sinon.spy();
4648
onMoveTabSpy = sinon.spy();
49+
onDuplicateSpy = sinon.spy();
50+
onCloseAllOthersSpy = sinon.spy();
4751
});
4852

4953
afterEach(cleanup);
@@ -59,6 +63,8 @@ describe('WorkspaceTabs', function () {
5963
onSelectNextTab={onSelectNextSpy}
6064
onSelectPrevTab={onSelectPrevSpy}
6165
onMoveTab={onMoveTabSpy}
66+
onDuplicateTab={onDuplicateSpy}
67+
onCloseAllOtherTabs={onCloseAllOthersSpy}
6268
tabs={[]}
6369
selectedTabIndex={0}
6470
/>
@@ -87,7 +93,11 @@ describe('WorkspaceTabs', function () {
8793
onCreateNewTab={onCreateNewTabSpy}
8894
onCloseTab={onCloseTabSpy}
8995
onSelectTab={onSelectSpy}
96+
onSelectNextTab={onSelectNextSpy}
97+
onSelectPrevTab={onSelectPrevSpy}
9098
onMoveTab={onMoveTabSpy}
99+
onDuplicateTab={onDuplicateSpy}
100+
onCloseAllOtherTabs={onCloseAllOthersSpy}
91101
tabs={[1, 2, 3].map((tabId) => mockTab(tabId))}
92102
selectedTabIndex={1}
93103
/>
@@ -156,7 +166,11 @@ describe('WorkspaceTabs', function () {
156166
onCreateNewTab={onCreateNewTabSpy}
157167
onCloseTab={onCloseTabSpy}
158168
onSelectTab={onSelectSpy}
169+
onSelectNextTab={onSelectNextSpy}
170+
onSelectPrevTab={onSelectPrevSpy}
159171
onMoveTab={onMoveTabSpy}
172+
onDuplicateTab={onDuplicateSpy}
173+
onCloseAllOtherTabs={onCloseAllOthersSpy}
160174
tabs={[1, 2].map((tabId) => mockTab(tabId))}
161175
selectedTabIndex={0}
162176
/>
@@ -182,7 +196,11 @@ describe('WorkspaceTabs', function () {
182196
onCreateNewTab={onCreateNewTabSpy}
183197
onCloseTab={onCloseTabSpy}
184198
onSelectTab={onSelectSpy}
199+
onSelectNextTab={onSelectNextSpy}
200+
onSelectPrevTab={onSelectPrevSpy}
185201
onMoveTab={onMoveTabSpy}
202+
onDuplicateTab={onDuplicateSpy}
203+
onCloseAllOtherTabs={onCloseAllOthersSpy}
186204
tabs={[1, 2].map((tabId) => mockTab(tabId))}
187205
selectedTabIndex={1}
188206
/>

packages/compass-components/src/components/workspace-tabs/workspace-tabs.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,19 @@ type SortableItemProps = {
150150
selectedTabIndex: number;
151151
activeId: UniqueIdentifier | null;
152152
onSelect: (tabIndex: number) => void;
153+
onDuplicate: (tabIndex: number) => void;
153154
onClose: (tabIndex: number) => void;
155+
onCloseAllOthers: (tabIndex: number) => void;
154156
};
155157

156158
type SortableListProps = {
157159
tabs: TabItem[];
158160
selectedTabIndex: number;
159161
onMove: (oldTabIndex: number, newTabIndex: number) => void;
160162
onSelect: (tabIndex: number) => void;
163+
onDuplicate: (tabIndex: number) => void;
161164
onClose: (tabIndex: number) => void;
165+
onCloseAllOthers: (tabIndex: number) => void;
162166
};
163167

164168
type WorkspaceTabsProps = {
@@ -167,7 +171,9 @@ type WorkspaceTabsProps = {
167171
onSelectTab: (tabIndex: number) => void;
168172
onSelectNextTab: () => void;
169173
onSelectPrevTab: () => void;
174+
onDuplicateTab: (tabIndex: number) => void;
170175
onCloseTab: (tabIndex: number) => void;
176+
onCloseAllOtherTabs: (tabIndex: number) => void;
171177
onMoveTab: (oldTabIndex: number, newTabIndex: number) => void;
172178
tabs: TabItem[];
173179
selectedTabIndex: number;
@@ -209,7 +215,9 @@ const SortableList = ({
209215
onMove,
210216
onSelect,
211217
selectedTabIndex,
218+
onDuplicate,
212219
onClose,
220+
onCloseAllOthers,
213221
}: SortableListProps) => {
214222
const items = tabs.map((tab) => tab.id);
215223
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
@@ -266,7 +274,9 @@ const SortableList = ({
266274
tab={tab}
267275
activeId={activeId}
268276
onSelect={onSelect}
277+
onDuplicate={onDuplicate}
269278
onClose={onClose}
279+
onCloseAllOthers={onCloseAllOthers}
270280
selectedTabIndex={selectedTabIndex}
271281
/>
272282
))}
@@ -282,16 +292,26 @@ const SortableItem = ({
282292
selectedTabIndex,
283293
activeId,
284294
onSelect,
295+
onDuplicate,
285296
onClose,
297+
onCloseAllOthers,
286298
}: SortableItemProps) => {
287299
const onTabSelected = useCallback(() => {
288300
onSelect(index);
289301
}, [onSelect, index]);
290302

303+
const onTabDuplicated = useCallback(() => {
304+
onDuplicate(index);
305+
}, [onDuplicate, index]);
306+
291307
const onTabClosed = useCallback(() => {
292308
onClose(index);
293309
}, [onClose, index]);
294310

311+
const onAllOthersTabsClosed = useCallback(() => {
312+
onCloseAllOthers(index);
313+
}, [onCloseAllOthers, index]);
314+
295315
const isSelected = useMemo(
296316
() => selectedTabIndex === index,
297317
[selectedTabIndex, index]
@@ -304,14 +324,18 @@ const SortableItem = ({
304324
isDragging,
305325
tabContentId: tabId,
306326
onSelect: onTabSelected,
327+
onDuplicate: onTabDuplicated,
307328
onClose: onTabClosed,
329+
onCloseAllOthers: onAllOthersTabsClosed,
308330
});
309331
};
310332

311333
function WorkspaceTabs({
312334
['aria-label']: ariaLabel,
313335
onCreateNewTab,
336+
onDuplicateTab,
314337
onCloseTab,
338+
onCloseAllOtherTabs,
315339
onMoveTab,
316340
onSelectTab,
317341
onSelectNextTab,
@@ -408,7 +432,9 @@ function WorkspaceTabs({
408432
tabs={tabs}
409433
onMove={onMoveTab}
410434
onSelect={onSelectTab}
435+
onDuplicate={onDuplicateTab}
411436
onClose={onCloseTab}
437+
onCloseAllOthers={onCloseAllOtherTabs}
412438
selectedTabIndex={selectedTabIndex}
413439
/>
414440
</div>

packages/compass-workspaces/src/components/workspaces.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import type {
1616
} from '../stores/workspaces';
1717
import {
1818
closeTab,
19+
closeAllOtherTabs,
1920
getActiveTab,
2021
moveTab,
2122
openFallbackWorkspace,
2223
openTabFromCurrent,
24+
duplicateTab,
2325
selectNextTab,
2426
selectPrevTab,
2527
selectTab,
@@ -75,7 +77,9 @@ type CompassWorkspacesProps = {
7577
onSelectPrevTab(): void;
7678
onMoveTab(from: number, to: number): void;
7779
onCreateTab(defaultTab?: OpenWorkspaceOptions | null): void;
80+
onDuplicateTab(at: number): void;
7881
onCloseTab(at: number): void;
82+
onCloseAllOtherTabs(at: number): void;
7983
onNamespaceNotFound(
8084
tab: Extract<WorkspaceTab, { namespace: string }>,
8185
fallbackNamespace: string | null
@@ -93,7 +97,9 @@ const CompassWorkspaces: React.FunctionComponent<CompassWorkspacesProps> = ({
9397
onSelectPrevTab,
9498
onMoveTab,
9599
onCreateTab,
100+
onDuplicateTab,
96101
onCloseTab,
102+
onCloseAllOtherTabs,
97103
onNamespaceNotFound,
98104
}) => {
99105
const { log, mongoLogId } = useLogger('COMPASS-WORKSPACES');
@@ -202,7 +208,9 @@ const CompassWorkspaces: React.FunctionComponent<CompassWorkspacesProps> = ({
202208
onSelectPrevTab={onSelectPrevTab}
203209
onMoveTab={onMoveTab}
204210
onCreateNewTab={onCreateNewTab}
211+
onDuplicateTab={onDuplicateTab}
205212
onCloseTab={onCloseTab}
213+
onCloseAllOtherTabs={onCloseAllOtherTabs}
206214
tabs={workspaceTabs}
207215
selectedTabIndex={activeTabIndex}
208216
></WorkspaceTabs>
@@ -234,7 +242,9 @@ export default connect(
234242
onSelectPrevTab: selectPrevTab,
235243
onMoveTab: moveTab,
236244
onCreateTab: openTabFromCurrent,
245+
onDuplicateTab: duplicateTab,
237246
onCloseTab: closeTab,
247+
onCloseAllOtherTabs: closeAllOtherTabs,
238248
onNamespaceNotFound: openFallbackWorkspace,
239249
}
240250
)(CompassWorkspaces);

0 commit comments

Comments
 (0)