Skip to content

feat(workspaces): context menu on tabs to duplicate and close all other tabs #7053

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: gagik/context-menu-compass-ui
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import LeafyGreenTextInput from '@leafygreen-ui/text-input';
import { SearchInput } from '@leafygreen-ui/search-input';
export type { ToastProps } from '@leafygreen-ui/toast';
export { ToastProvider, useToast } from '@leafygreen-ui/toast';
export { usePrevious } from '@leafygreen-ui/hooks';
export { usePrevious, useMergeRefs } from '@leafygreen-ui/hooks';
import Toggle from '@leafygreen-ui/toggle';
import Tooltip from '@leafygreen-ui/tooltip';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ import { Tab } from './tab';
describe('Tab', function () {
let onCloseSpy: sinon.SinonSpy;
let onSelectSpy: sinon.SinonSpy;
let onDuplicateSpy: sinon.SinonSpy;
let onCloseAllOthersSpy: sinon.SinonSpy;

beforeEach(function () {
onCloseSpy = sinon.spy();
onSelectSpy = sinon.spy();
onDuplicateSpy = sinon.spy();
onCloseAllOthersSpy = sinon.spy();
});

afterEach(cleanup);
Expand All @@ -28,6 +32,8 @@ describe('Tab', function () {
type="Databases"
onClose={onCloseSpy}
onSelect={onSelectSpy}
onDuplicate={onDuplicateSpy}
onCloseAllOthers={onCloseAllOthersSpy}
title="docs"
isSelected
isDragging={false}
Expand Down Expand Up @@ -73,6 +79,8 @@ describe('Tab', function () {
type="Databases"
onClose={onCloseSpy}
onSelect={onSelectSpy}
onDuplicate={onDuplicateSpy}
onCloseAllOthers={onCloseAllOthersSpy}
title="docs"
isSelected={false}
isDragging={false}
Expand All @@ -98,4 +106,48 @@ describe('Tab', function () {
).to.not.equal('none');
});
});

describe('when right-clicking', function () {
beforeEach(function () {
render(
<Tab
type="Databases"
onClose={onCloseSpy}
onSelect={onSelectSpy}
onDuplicate={onDuplicateSpy}
onCloseAllOthers={onCloseAllOthersSpy}
title="docs"
isSelected={false}
isDragging={false}
tabContentId="1"
tooltip={[['Connection', 'ABC']]}
iconGlyph="Folder"
/>
);
});

describe('clicking menu items', function () {
it('should propagate clicks on "Duplicate"', async function () {
const tab = await screen.findByText('docs');
userEvent.click(tab, { button: 2 });
expect(screen.getByTestId('context-menu')).to.be.visible;

const menuItem = await screen.findByText('Duplicate');
menuItem.click();
expect(onDuplicateSpy.callCount).to.equal(1);
expect(onCloseAllOthersSpy.callCount).to.equal(0);
});

it('should propagate clicks on "Close all other tabs"', async function () {
const tab = await screen.findByText('docs');
userEvent.click(tab, { button: 2 });
expect(screen.getByTestId('context-menu')).to.be.visible;

const menuItem = await screen.findByText('Close all other tabs');
menuItem.click();
expect(onDuplicateSpy.callCount).to.equal(0);
expect(onCloseAllOthersSpy.callCount).to.equal(1);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS as cssDndKit } from '@dnd-kit/utilities';
import { useId } from '@react-aria/utils';
import { useDarkMode } from '../../hooks/use-theme';
import { Icon, IconButton } from '../leafygreen';
import { Icon, IconButton, useMergeRefs } from '../leafygreen';
import { mergeProps } from '../../utils/merge-props';
import { useDefaultAction } from '../../hooks/use-default-action';
import { LogoIcon } from '../icons/logo-icon';
import { Tooltip } from '../leafygreen';
import { ServerIcon } from '../icons/server-icon';
import { useTabTheme } from './use-tab-theme';
import { useContextMenuItems } from '../context-menu';

function focusedChild(className: string) {
return `&:hover ${className}, &:focus-visible ${className}, &:focus-within:not(:focus) ${className}`;
Expand Down Expand Up @@ -192,7 +193,9 @@ export type WorkspaceTabCoreProps = {
isSelected: boolean;
isDragging: boolean;
onSelect: () => void;
onDuplicate: () => void;
onClose: () => void;
onCloseAllOthers: () => void;
tabContentId: string;
};

Expand All @@ -207,7 +210,9 @@ function Tab({
isSelected,
isDragging,
onSelect,
onDuplicate,
onClose,
onCloseAllOthers,
tabContentId,
iconGlyph,
className: tabClassName,
Expand All @@ -234,6 +239,16 @@ function Tab({
return css(tabTheme);
}, [tabTheme, darkMode]);

const contextMenuRef = useContextMenuItems(
() => [
{ label: 'Close all other tabs', onAction: onCloseAllOthers },
{ label: 'Duplicate', onAction: onDuplicate },
],
[onCloseAllOthers, onDuplicate]
);

const mergedRef = useMergeRefs([setNodeRef, contextMenuRef]);

const style = {
transform: cssDndKit.Transform.toString(transform),
transition,
Expand All @@ -251,7 +266,7 @@ function Tab({
justify="start"
trigger={
<div
ref={setNodeRef}
ref={mergedRef}
style={style}
className={cx(
tabStyles,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ describe('WorkspaceTabs', function () {
let onSelectNextSpy: sinon.SinonSpy;
let onSelectPrevSpy: sinon.SinonSpy;
let onMoveTabSpy: sinon.SinonSpy;
let onDuplicateSpy: sinon.SinonSpy;
let onCloseAllOthersSpy: sinon.SinonSpy;

beforeEach(function () {
onCreateNewTabSpy = sinon.spy();
Expand All @@ -44,6 +46,8 @@ describe('WorkspaceTabs', function () {
onSelectNextSpy = sinon.spy();
onSelectPrevSpy = sinon.spy();
onMoveTabSpy = sinon.spy();
onDuplicateSpy = sinon.spy();
onCloseAllOthersSpy = sinon.spy();
});

afterEach(cleanup);
Expand All @@ -59,6 +63,8 @@ describe('WorkspaceTabs', function () {
onSelectNextTab={onSelectNextSpy}
onSelectPrevTab={onSelectPrevSpy}
onMoveTab={onMoveTabSpy}
onDuplicateTab={onDuplicateSpy}
onCloseAllOtherTabs={onCloseAllOthersSpy}
tabs={[]}
selectedTabIndex={0}
/>
Expand Down Expand Up @@ -87,7 +93,11 @@ describe('WorkspaceTabs', function () {
onCreateNewTab={onCreateNewTabSpy}
onCloseTab={onCloseTabSpy}
onSelectTab={onSelectSpy}
onSelectNextTab={onSelectNextSpy}
onSelectPrevTab={onSelectPrevSpy}
onMoveTab={onMoveTabSpy}
onDuplicateTab={onDuplicateSpy}
onCloseAllOtherTabs={onCloseAllOthersSpy}
tabs={[1, 2, 3].map((tabId) => mockTab(tabId))}
selectedTabIndex={1}
/>
Expand Down Expand Up @@ -156,7 +166,11 @@ describe('WorkspaceTabs', function () {
onCreateNewTab={onCreateNewTabSpy}
onCloseTab={onCloseTabSpy}
onSelectTab={onSelectSpy}
onSelectNextTab={onSelectNextSpy}
onSelectPrevTab={onSelectPrevSpy}
onMoveTab={onMoveTabSpy}
onDuplicateTab={onDuplicateSpy}
onCloseAllOtherTabs={onCloseAllOthersSpy}
tabs={[1, 2].map((tabId) => mockTab(tabId))}
selectedTabIndex={0}
/>
Expand All @@ -182,7 +196,11 @@ describe('WorkspaceTabs', function () {
onCreateNewTab={onCreateNewTabSpy}
onCloseTab={onCloseTabSpy}
onSelectTab={onSelectSpy}
onSelectNextTab={onSelectNextSpy}
onSelectPrevTab={onSelectPrevSpy}
onMoveTab={onMoveTabSpy}
onDuplicateTab={onDuplicateSpy}
onCloseAllOtherTabs={onCloseAllOthersSpy}
tabs={[1, 2].map((tabId) => mockTab(tabId))}
selectedTabIndex={1}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,19 @@ type SortableItemProps = {
selectedTabIndex: number;
activeId: UniqueIdentifier | null;
onSelect: (tabIndex: number) => void;
onDuplicate: (tabIndex: number) => void;
onClose: (tabIndex: number) => void;
onCloseAllOthers: (tabIndex: number) => void;
};

type SortableListProps = {
tabs: TabItem[];
selectedTabIndex: number;
onMove: (oldTabIndex: number, newTabIndex: number) => void;
onSelect: (tabIndex: number) => void;
onDuplicate: (tabIndex: number) => void;
onClose: (tabIndex: number) => void;
onCloseAllOthers: (tabIndex: number) => void;
};

type WorkspaceTabsProps = {
Expand All @@ -167,7 +171,9 @@ type WorkspaceTabsProps = {
onSelectTab: (tabIndex: number) => void;
onSelectNextTab: () => void;
onSelectPrevTab: () => void;
onDuplicateTab: (tabIndex: number) => void;
onCloseTab: (tabIndex: number) => void;
onCloseAllOtherTabs: (tabIndex: number) => void;
onMoveTab: (oldTabIndex: number, newTabIndex: number) => void;
tabs: TabItem[];
selectedTabIndex: number;
Expand Down Expand Up @@ -209,7 +215,9 @@ const SortableList = ({
onMove,
onSelect,
selectedTabIndex,
onDuplicate,
onClose,
onCloseAllOthers,
}: SortableListProps) => {
const items = tabs.map((tab) => tab.id);
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
Expand Down Expand Up @@ -266,7 +274,9 @@ const SortableList = ({
tab={tab}
activeId={activeId}
onSelect={onSelect}
onDuplicate={onDuplicate}
onClose={onClose}
onCloseAllOthers={onCloseAllOthers}
selectedTabIndex={selectedTabIndex}
/>
))}
Expand All @@ -282,16 +292,26 @@ const SortableItem = ({
selectedTabIndex,
activeId,
onSelect,
onDuplicate,
onClose,
onCloseAllOthers,
}: SortableItemProps) => {
const onTabSelected = useCallback(() => {
onSelect(index);
}, [onSelect, index]);

const onTabDuplicated = useCallback(() => {
onDuplicate(index);
}, [onDuplicate, index]);

const onTabClosed = useCallback(() => {
onClose(index);
}, [onClose, index]);

const onAllOthersTabsClosed = useCallback(() => {
onCloseAllOthers(index);
}, [onCloseAllOthers, index]);

const isSelected = useMemo(
() => selectedTabIndex === index,
[selectedTabIndex, index]
Expand All @@ -304,14 +324,18 @@ const SortableItem = ({
isDragging,
tabContentId: tabId,
onSelect: onTabSelected,
onDuplicate: onTabDuplicated,
onClose: onTabClosed,
onCloseAllOthers: onAllOthersTabsClosed,
});
};

function WorkspaceTabs({
['aria-label']: ariaLabel,
onCreateNewTab,
onDuplicateTab,
onCloseTab,
onCloseAllOtherTabs,
onMoveTab,
onSelectTab,
onSelectNextTab,
Expand Down Expand Up @@ -408,7 +432,9 @@ function WorkspaceTabs({
tabs={tabs}
onMove={onMoveTab}
onSelect={onSelectTab}
onDuplicate={onDuplicateTab}
onClose={onCloseTab}
onCloseAllOthers={onCloseAllOtherTabs}
selectedTabIndex={selectedTabIndex}
/>
</div>
Expand Down
10 changes: 10 additions & 0 deletions packages/compass-workspaces/src/components/workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import type {
} from '../stores/workspaces';
import {
closeTab,
closeAllOtherTabs,
getActiveTab,
moveTab,
openFallbackWorkspace,
openTabFromCurrent,
duplicateTab,
selectNextTab,
selectPrevTab,
selectTab,
Expand Down Expand Up @@ -75,7 +77,9 @@ type CompassWorkspacesProps = {
onSelectPrevTab(): void;
onMoveTab(from: number, to: number): void;
onCreateTab(defaultTab?: OpenWorkspaceOptions | null): void;
onDuplicateTab(at: number): void;
onCloseTab(at: number): void;
onCloseAllOtherTabs(at: number): void;
onNamespaceNotFound(
tab: Extract<WorkspaceTab, { namespace: string }>,
fallbackNamespace: string | null
Expand All @@ -93,7 +97,9 @@ const CompassWorkspaces: React.FunctionComponent<CompassWorkspacesProps> = ({
onSelectPrevTab,
onMoveTab,
onCreateTab,
onDuplicateTab,
onCloseTab,
onCloseAllOtherTabs,
onNamespaceNotFound,
}) => {
const { log, mongoLogId } = useLogger('COMPASS-WORKSPACES');
Expand Down Expand Up @@ -202,7 +208,9 @@ const CompassWorkspaces: React.FunctionComponent<CompassWorkspacesProps> = ({
onSelectPrevTab={onSelectPrevTab}
onMoveTab={onMoveTab}
onCreateNewTab={onCreateNewTab}
onDuplicateTab={onDuplicateTab}
onCloseTab={onCloseTab}
onCloseAllOtherTabs={onCloseAllOtherTabs}
tabs={workspaceTabs}
selectedTabIndex={activeTabIndex}
></WorkspaceTabs>
Expand Down Expand Up @@ -234,7 +242,9 @@ export default connect(
onSelectPrevTab: selectPrevTab,
onMoveTab: moveTab,
onCreateTab: openTabFromCurrent,
onDuplicateTab: duplicateTab,
onCloseTab: closeTab,
onCloseAllOtherTabs: closeAllOtherTabs,
onNamespaceNotFound: openFallbackWorkspace,
}
)(CompassWorkspaces);
Loading
Loading