Skip to content

feat(sidebar): context menu to control specific navigation items COMPASS-9393 #7070

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

Merged
merged 19 commits into from
Jul 11, 2025
Merged
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
38 changes: 38 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { Button, Icon, Menu, MenuItem, MenuSeparator } from '../leafygreen';
import { WorkspaceContainer } from '../workspace-container';

import { ItemActionButtonSize } from './constants';
import { actionTestId } from './utils';
import { ActionGlyph } from './action-glyph';
import { isSeparatorMenuAction, type MenuAction } from './item-action-menu';
import { actionTestId, isSeparatorMenuAction } from './utils';
import type { MenuAction } from './types';

const getHiddenOnNarrowStyles = (narrowBreakpoint: string) =>
css({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import { MenuSeparator, Tooltip } from '../leafygreen';

import { ItemActionButtonSize } from './constants';
import type { ItemAction, ItemSeparator } from './types';
import { isSeparatorMenuAction } from './item-action-menu';
import { ItemActionButton } from './item-action-button';
import { actionTestId } from './utils';
import { actionTestId, isSeparatorMenuAction } from './utils';

export type GroupedItemAction<Action extends string> = ItemAction<Action> & {
tooltipProps?: Parameters<typeof Tooltip>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,9 @@ import { Menu, MenuItem, MenuSeparator } from '../leafygreen';

import { ItemActionButtonSize } from './constants';
import { ActionGlyph } from './action-glyph';
import type { ItemBase, ItemSeparator } from './types';
import type { MenuAction } from './types';
import { SmallIconButton } from './small-icon-button';
import { actionTestId } from './utils';

export type MenuAction<Action extends string> =
| ItemBase<Action>
| ItemSeparator;

export function isSeparatorMenuAction(value: unknown): value is ItemSeparator {
return (
typeof value === 'object' &&
value !== null &&
'separator' in value &&
value.separator === true
);
}
import { actionTestId, isSeparatorMenuAction } from './utils';

const containerStyle = css({
flex: 'none',
Expand Down
4 changes: 4 additions & 0 deletions packages/compass-components/src/components/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ export type ItemAction<Action extends string> = {
} & ItemBase<Action>;

export type ItemSeparator = { separator: true };

export type MenuAction<Action extends string> =
| ItemBase<Action>
| ItemSeparator;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect } from 'chai';
import { splitBySeparator } from './utils';

describe('item action utils', function () {
describe('splitBySeparator', function () {
it('returns an empty array for an empty input', function () {
const result = splitBySeparator([]);
expect(result).is.empty;
});

it('returns a single item for a single input', function () {
const result = splitBySeparator([{ label: 'Foo', action: 'foo' }]);
expect(result).deep.equal([[{ label: 'Foo', action: 'foo' }]]);
});

it('splits four items separated by a separator', function () {
const result = splitBySeparator([
{ label: 'Foo', action: 'foo' },
{ label: 'Bar', action: 'bar' },
{ separator: true },
{ label: 'Baz', action: 'baz' },
{ label: 'Qux', action: 'qux' },
]);
expect(result).deep.equal([
[
{ label: 'Foo', action: 'foo' },
{ label: 'Bar', action: 'bar' },
],
[
{ label: 'Baz', action: 'baz' },
{ label: 'Qux', action: 'qux' },
],
]);
});

it('disregards leading separators', function () {
const result = splitBySeparator([
{ separator: true },
{ label: 'Foo', action: 'foo' },
]);
expect(result).deep.equal([[{ label: 'Foo', action: 'foo' }]]);
});

it('disregards trailing separators', function () {
const result = splitBySeparator([
{ label: 'Foo', action: 'foo' },
{ separator: true },
]);
expect(result).deep.equal([[{ label: 'Foo', action: 'foo' }]]);
});
});
});
32 changes: 32 additions & 0 deletions packages/compass-components/src/components/actions/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
import type { ItemBase, ItemSeparator, MenuAction } from './types';

export function isSeparatorMenuAction(value: unknown): value is ItemSeparator {
return (
typeof value === 'object' &&
value !== null &&
'separator' in value &&
value.separator === true
);
}

export function actionTestId(dataTestId: string | undefined, action: string) {
return dataTestId ? `${dataTestId}-${action}-action` : undefined;
}

export function splitBySeparator<Action extends string>(
actions: MenuAction<Action>[]
) {
const result: ItemBase<Action>[][] = [];
let currentGroup: ItemBase<Action>[] = [];
for (const action of actions) {
if (isSeparatorMenuAction(action)) {
if (currentGroup.length > 0) {
result.push(currentGroup);
currentGroup = [];
}
} else {
currentGroup.push(action);
}
}
if (currentGroup.length > 0) {
result.push(currentGroup);
}
return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {
type ContextMenuWrapperProps,
} from '@mongodb-js/compass-context-menu';

export type { ContextMenuItem } from '@mongodb-js/compass-context-menu';
export type {
ContextMenuItem,
ContextMenuItemGroup,
} from '@mongodb-js/compass-context-menu';

// TODO: Remove these once https://jira.mongodb.org/browse/LG-5013 is resolved

Expand Down
25 changes: 9 additions & 16 deletions packages/compass-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,19 @@ import ResizableSidebar, {
defaultSidebarWidth,
} from './components/resizeable-sidebar';

import type {
export type {
ItemAction,
ItemComponentProps,
ItemSeparator,
MenuAction,
} from './components/actions/types';
import type { GroupedItemAction } from './components/actions/item-action-group';
import type { MenuAction } from './components/actions/item-action-menu';
export { splitBySeparator } from './components/actions/utils';
export type { GroupedItemAction } from './components/actions/item-action-group';

import { ItemActionControls } from './components/actions/item-action-controls';
import { ItemActionGroup } from './components/actions/item-action-group';
import { ItemActionMenu } from './components/actions/item-action-menu';
import { DropdownMenuButton } from './components/actions/dropdown-menu-button';
export { ItemActionControls } from './components/actions/item-action-controls';
export { ItemActionGroup } from './components/actions/item-action-group';
export { ItemActionMenu } from './components/actions/item-action-menu';
export { DropdownMenuButton } from './components/actions/dropdown-menu-button';

export { DocumentIcon } from './components/icons/document-icon';
export { FavoriteIcon } from './components/icons/favorite-icon';
Expand Down Expand Up @@ -104,15 +105,11 @@ export {
useContextMenuItems,
useContextMenuGroups,
type ContextMenuItem,
type ContextMenuItemGroup,
} from './components/context-menu';

export type {
FileInputBackend,
ItemAction,
ItemComponentProps,
GroupedItemAction,
MenuAction,
ItemSeparator,
ElectronFileDialogOptions,
ElectronShowFileDialogProvider,
};
Expand All @@ -131,10 +128,6 @@ export {
ResizableSidebar,
WarningSummary,
WorkspaceTabs,
ItemActionControls,
ItemActionGroup,
ItemActionMenu,
DropdownMenuButton,
defaultSidebarWidth,
createElectronFileInputBackend,
createJSDomFileInputDummyBackend,
Expand Down
7 changes: 4 additions & 3 deletions packages/compass-connections-navigation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@
"reformat": "npm run eslint . -- --fix && npm run prettier -- --write ."
},
"dependencies": {
"@mongodb-js/compass-connections": "^1.64.0",
"@mongodb-js/compass-components": "^1.42.0",
"@mongodb-js/connection-info": "^0.15.5",
"@mongodb-js/connection-form": "^1.56.0",
"@mongodb-js/compass-connections": "^1.64.0",
"@mongodb-js/compass-context-menu": "^0.2.0",
"@mongodb-js/compass-workspaces": "^0.45.0",
"@mongodb-js/connection-form": "^1.56.0",
"@mongodb-js/connection-info": "^0.15.5",
"compass-preferences-model": "^2.44.0",
"mongodb-build-info": "^1.7.2",
"react": "^17.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
SidebarTreeItem,
} from './tree-data';

type NavigationBaseItemProps = {
type NavigationBaseItemProps = React.PropsWithChildren<{
item: SidebarTreeItem;
name: string;
isActive: boolean;
Expand All @@ -40,7 +40,7 @@ type NavigationBaseItemProps = {
onAction: (action: Actions) => void;
};
toggleExpand: () => void;
};
}>;

const menuStyles = css({
width: '240px',
Expand Down Expand Up @@ -155,26 +155,33 @@ const ClusterStateBadgeWithTooltip: React.FunctionComponent<{
return null;
};

export const NavigationBaseItem: React.FC<NavigationBaseItemProps> = ({
item,
isActive,
actionProps,
name,
style,
icon,
dataAttributes,
isExpandVisible,
isExpandDisabled,
isExpanded,
isFocused,
hasDefaultAction,
toggleExpand,
children,
}) => {
export const NavigationBaseItem = React.forwardRef<
HTMLDivElement,
NavigationBaseItemProps
>(function NavigationBaseItem(
{
item,
isActive,
actionProps,
name,
style,
icon,
dataAttributes,
isExpandVisible,
isExpandDisabled,
isExpanded,
isFocused,
hasDefaultAction,
toggleExpand,
children,
},
ref
) {
const [hoverProps, isHovered] = useHoverState();

return (
<div
ref={ref}
data-testid="base-navigation-item"
className={cx(itemContainerStyles, {
[itemContainerWithActionStyles]: hasDefaultAction,
Expand Down Expand Up @@ -214,4 +221,4 @@ export const NavigationBaseItem: React.FC<NavigationBaseItemProps> = ({
</div>
</div>
);
};
});
Loading
Loading