Skip to content

Commit 2cf2abb

Browse files
kiwiwongjiming
and
jiming
authored
feat: support the horizontal layout of MenuBar (#553)
* feat: support the horizontal layout of MenuBar  * feat: update menuBar unit test * feat: menubar supports custom icon * feat: supplement the unit test code of menuBar * feat: optimize code and supplement unit tests Co-authored-by: jiming <jiming@dtstack.com>
1 parent 44b4e64 commit 2cf2abb

File tree

19 files changed

+802
-21
lines changed

19 files changed

+802
-21
lines changed

src/components/menu/__tests__/menu.test.tsx

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useRef } from 'react';
22
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
33
import renderer from 'react-test-renderer';
44

@@ -11,6 +11,7 @@ import {
1111
horizontalMenuClassName,
1212
verticalMenuClassName,
1313
} from '../base';
14+
import { MenuRef } from '../index';
1415

1516
const menuData = [
1617
{
@@ -118,6 +119,11 @@ const menuData = [
118119
];
119120
const TEST_ID = 'test-id';
120121

122+
function MenuTest(props) {
123+
const ref = useRef<MenuRef>(null);
124+
return <Menu ref={ref} data={menuData} {...props} />;
125+
}
126+
121127
describe('Test the Menu Component', () => {
122128
test('Match the List snapshot', () => {
123129
const component = renderer.create(
@@ -306,4 +312,36 @@ describe('Test the Menu Component', () => {
306312
});
307313
});
308314
});
315+
316+
test('Dispose the Menu', () => {
317+
const TEST_DATA1 = 'test1';
318+
const TEST_DATA2 = 'test2';
319+
const mockData = [
320+
{
321+
id: TEST_DATA1,
322+
name: TEST_DATA1,
323+
title: TEST_DATA1,
324+
data: [
325+
{
326+
id: TEST_DATA2,
327+
name: TEST_DATA2,
328+
'data-testid': TEST_DATA2,
329+
},
330+
],
331+
},
332+
];
333+
const menu = renderer.create(<MenuTest />);
334+
const menuNode: any = (menu as renderer.ReactTestRenderer).root.findByType(
335+
Menu
336+
);
337+
expect(menuNode._fiber).not.toBeUndefined();
338+
339+
const menuRef = menuNode._fiber.ref;
340+
render(<Menu trigger="click" ref={menuRef} data={mockData} />);
341+
expect(menuRef?.current?.dispose).not.toBeUndefined();
342+
343+
menuRef.current?.dispose();
344+
const item = document.body.querySelectorAll('ul')[1];
345+
expect(item.style.opacity).toEqual('0');
346+
});
309347
});

src/components/menu/menu.tsx

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import React, { useEffect, useCallback, useRef } from 'react';
1+
import React, {
2+
useEffect,
3+
useCallback,
4+
useRef,
5+
useImperativeHandle,
6+
forwardRef,
7+
} from 'react';
28
import { classNames } from 'mo/common/className';
39
import { debounce } from 'lodash';
410
import { mergeFunctions } from 'mo/common/utils';
@@ -56,7 +62,7 @@ const setPositionForSubMenu = (
5662
subMenu.style.left = `${pos.x}px`;
5763
};
5864

59-
export function Menu(props: React.PropsWithChildren<IMenuProps>) {
65+
function MenuComp(props: React.PropsWithChildren<IMenuProps>, ref) {
6066
const {
6167
className,
6268
mode = MenuMode.Vertical,
@@ -82,7 +88,7 @@ export function Menu(props: React.PropsWithChildren<IMenuProps>) {
8288
if (data.length > 0) {
8389
const renderMenusByData = (menus: IMenuProps[]) => {
8490
return menus.map((item: IMenuProps) => {
85-
if (item.type === 'divider') return <Divider />;
91+
if (item.type === 'divider') return <Divider key={item.id} />;
8692

8793
const handleClick = mergeFunctions(onClick, item.onClick);
8894
if (item.data && item.data.length > 0) {
@@ -200,6 +206,12 @@ export function Menu(props: React.PropsWithChildren<IMenuProps>) {
200206
};
201207
}, []);
202208

209+
useImperativeHandle(ref, () => ({
210+
dispose: () => {
211+
initialMenuStyle();
212+
},
213+
}));
214+
203215
return (
204216
<ul
205217
className={claNames}
@@ -213,3 +225,11 @@ export function Menu(props: React.PropsWithChildren<IMenuProps>) {
213225
</ul>
214226
);
215227
}
228+
229+
export type MenuRef = {
230+
dispose: () => void;
231+
};
232+
233+
export const Menu = forwardRef<MenuRef, React.PropsWithChildren<IMenuProps>>(
234+
MenuComp
235+
);

src/controller/__tests__/menuBar.test.ts

+45-3
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
import { ID_SIDE_BAR } from 'mo/common/id';
22
import { MonacoService } from 'mo/monaco/monacoService';
3-
import { MenuBarService, BuiltinService } from 'mo/services';
3+
import { MenuBarService, BuiltinService, LayoutService } from 'mo/services';
44
import { constants, modules } from 'mo/services/builtinService/const';
55
import 'reflect-metadata';
66
import { container } from 'tsyringe';
77
import { MenuBarController } from '../menuBar';
8+
import { MenuBarMode } from 'mo/model/workbench/layout';
89

910
const menuBarController = container.resolve(MenuBarController);
1011
const menuBarService = container.resolve(MenuBarService);
1112
const monacoService = container.resolve(MonacoService);
1213
const builtinService = container.resolve(BuiltinService);
14+
const layoutService = container.resolve(LayoutService);
1315

1416
const mockEle = document.createElement('div');
1517

1618
describe('The menuBar controller', () => {
1719
test('Should support to inject the default value', () => {
1820
menuBarController.initView();
19-
20-
expect(menuBarService.getState().data).toEqual(
21+
const mode = layoutService.getMenuBarMode();
22+
const menuBarData = menuBarController.getMenuBarDataByMode(
23+
mode,
2124
modules.builtInMenuBarData()
2225
);
26+
27+
expect(menuBarService.getState().data).toEqual(menuBarData);
2328
menuBarService.reset();
2429
});
2530

@@ -160,4 +165,41 @@ describe('The menuBar controller', () => {
160165
mockExecute.mockClear();
161166
menuBarService.update = originalUpdate;
162167
});
168+
169+
test('Should support to change the layout mode', () => {
170+
const mockEvent = {} as any;
171+
const mockItem = { id: constants.MENUBAR_MODE_HORIZONTAL };
172+
const mockExecute = jest.fn();
173+
const originalSetMenus = menuBarService.setMenus;
174+
const originalUpdateMenuBarMode = menuBarController.updateMenuBarMode;
175+
176+
// change default mode
177+
const defaultMode = layoutService.getMenuBarMode();
178+
const anotherMode =
179+
defaultMode === MenuBarMode.horizontal
180+
? MenuBarMode.vertical
181+
: MenuBarMode.horizontal;
182+
layoutService.setMenuBarMode(anotherMode);
183+
menuBarController.initView();
184+
expect(layoutService.getMenuBarMode()).toBe(anotherMode);
185+
186+
// update to horizontal mode
187+
menuBarService.setMenus = mockExecute;
188+
layoutService.setMenuBarMode(MenuBarMode.vertical);
189+
menuBarController.onClick(mockEvent, mockItem);
190+
expect(mockExecute).toBeCalled();
191+
mockExecute.mockClear();
192+
193+
// update to vertical mode
194+
mockItem.id = constants.MENUBAR_MODE_VERTICAL;
195+
layoutService.setMenuBarMode(MenuBarMode.horizontal);
196+
menuBarController.onClick(mockEvent, mockItem);
197+
expect(mockExecute).toBeCalled();
198+
mockExecute.mockClear();
199+
200+
menuBarService.setMenus = originalSetMenus;
201+
menuBarController.updateMenuBarMode = originalUpdateMenuBarMode;
202+
layoutService.reset();
203+
menuBarService.reset();
204+
});
163205
});

src/controller/menuBar.ts

+72-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'reflect-metadata';
22
import { container, singleton } from 'tsyringe';
33
import { IActivityBarItem, IMenuBarItem } from 'mo/model';
44
import { MenuBarEvent } from 'mo/model/workbench/menuBar';
5+
import { MenuBarMode } from 'mo/model/workbench/layout';
56
import { Controller } from 'mo/react/controller';
67
import {
78
IMenuBarService,
@@ -25,6 +26,11 @@ export interface IMenuBarController extends Partial<Controller> {
2526
updateMenuBar?: () => void;
2627
updateActivityBar?: () => void;
2728
updateSideBar?: () => void;
29+
updateMenuBarMode?: (mode: keyof typeof MenuBarMode) => void;
30+
getMenuBarDataByMode?: (
31+
mode: keyof typeof MenuBarMode,
32+
menuData: IMenuBarItem[]
33+
) => IMenuBarItem[];
2834
}
2935

3036
@singleton()
@@ -60,9 +66,16 @@ export class MenuBarController
6066
MENU_VIEW_STATUSBAR,
6167
MENU_QUICK_COMMAND,
6268
MENU_VIEW_PANEL,
69+
MENUBAR_MODE_HORIZONTAL,
70+
MENUBAR_MODE_VERTICAL,
6371
} = this.builtinService.getConstants();
6472
if (builtInMenuBarData) {
65-
this.menuBarService.setMenus(builtInMenuBarData);
73+
const mode = this.layoutService.getMenuBarMode();
74+
const menuBarData = this.getMenuBarDataByMode(
75+
mode,
76+
builtInMenuBarData
77+
);
78+
this.menuBarService.setMenus(menuBarData);
6679
}
6780
([
6881
[ACTION_QUICK_CREATE_FILE, () => this.createFile()],
@@ -76,6 +89,14 @@ export class MenuBarController
7689
[MENU_QUICK_COMMAND, () => this.gotoQuickCommand()],
7790
[ID_SIDE_BAR, () => this.updateSideBar()],
7891
[MENU_VIEW_PANEL, () => this.updatePanel()],
92+
[
93+
MENUBAR_MODE_HORIZONTAL,
94+
() => this.updateMenuBarMode(MenuBarMode.horizontal),
95+
],
96+
[
97+
MENUBAR_MODE_VERTICAL,
98+
() => this.updateMenuBarMode(MenuBarMode.vertical),
99+
],
79100
] as [string, () => void][]).forEach(([key, value]) => {
80101
if (key) {
81102
this.automation[key] = value;
@@ -179,6 +200,13 @@ export class MenuBarController
179200
}
180201
};
181202

203+
public updateMenuBarMode = (mode: keyof typeof MenuBarMode) => {
204+
this.layoutService.setMenuBarMode(mode);
205+
const { builtInMenuBarData } = this.builtinService.getModules();
206+
const menuBarData = this.getMenuBarDataByMode(mode, builtInMenuBarData);
207+
this.menuBarService.setMenus(menuBarData);
208+
};
209+
182210
public updateStatusBar = () => {
183211
const hidden = this.layoutService.toggleStatusBarVisibility();
184212
const { MENU_VIEW_STATUSBAR } = this.builtinService.getConstants();
@@ -200,4 +228,47 @@ export class MenuBarController
200228
QuickTogglePanelAction.ID
201229
);
202230
};
231+
232+
/**
233+
* Get the menu bar data after filtering out the menu contained in ids
234+
* @param menuData
235+
* @param ids
236+
* @returns Filtered menu bar data
237+
*/
238+
private getFilteredMenuBarData(
239+
menuData: IMenuBarItem[],
240+
ids: (UniqueId | undefined)[]
241+
): IMenuBarItem[] {
242+
const newData: IMenuBarItem[] = [];
243+
if (Array.isArray(menuData)) {
244+
menuData.forEach((item: IMenuBarItem) => {
245+
if (ids.includes(item.id)) return;
246+
const newItem = { ...item };
247+
if (Array.isArray(item.data) && item.data.length > 0) {
248+
newItem.data = this.getFilteredMenuBarData(item.data, ids);
249+
}
250+
newData.push(newItem);
251+
});
252+
}
253+
return newData;
254+
}
255+
256+
public getMenuBarDataByMode(
257+
mode: keyof typeof MenuBarMode,
258+
menuData: IMenuBarItem[]
259+
): IMenuBarItem[] {
260+
const {
261+
MENUBAR_MODE_VERTICAL,
262+
MENUBAR_MODE_HORIZONTAL,
263+
} = this.builtinService.getConstants();
264+
const ids: (string | undefined)[] = [];
265+
if (mode === MenuBarMode.horizontal) {
266+
ids.push(MENUBAR_MODE_HORIZONTAL);
267+
} else if (mode === MenuBarMode.vertical) {
268+
ids.push(MENUBAR_MODE_VERTICAL);
269+
}
270+
271+
const menuBarData = this.getFilteredMenuBarData(menuData, ids);
272+
return menuBarData;
273+
}
203274
}

src/extensions/locales-defaults/locales/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
"menu.showPanel.title": "Toggle Panel",
2929
"menu.run": "Run",
3030
"menu.help": "Help",
31+
"menu.menuBarHorizontal": "Menu Bar Horizontal Mode",
32+
"menu.menuBarVertical": "Menu Bar Vertical Mode",
3133
"sidebar.explore.title": "Explorer",
3234
"sidebar.explore.folders": "Folders",
3335
"sidebar.explore.openEditor": "Open Editors",

src/extensions/locales-defaults/locales/zh-CN.json

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"menu.runTask": "运行任务",
3535
"menu.help": "帮助",
3636
"menu.about": "关于",
37+
"menu.menuBarHorizontal": "菜单栏水平模式",
38+
"menu.menuBarVertical": "菜单栏垂直模式",
3739
"sidebar.explore.title": "浏览",
3840
"sidebar.explore.openEditor": "打开的编辑器",
3941
"sidebar.explore.openEditor.group": "第 ${i} 组",

src/i18n/localization.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export type LocaleSourceIdType = {
3535
'menu.showPanel.title': string;
3636
'menu.run': string;
3737
'menu.help': string;
38+
'menu.menuBarHorizontal': string;
39+
'menu.menuBarVertical': string;
3840
'sidebar.explore.title': string;
3941
'sidebar.explore.folders': string;
4042
'sidebar.explore.openEditor': string;

src/model/workbench/layout.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ export enum Position {
22
left = 'left',
33
right = 'right',
44
}
5+
6+
export enum MenuBarMode {
7+
horizontal = 'horizontal',
8+
vertical = 'vertical',
9+
}
10+
511
export interface ViewVisibility {
612
hidden: boolean;
713
}
@@ -12,14 +18,19 @@ export interface IPanelViewState extends ViewVisibility {
1218
export interface ISidebarViewState extends ViewVisibility {
1319
position: keyof typeof Position;
1420
}
21+
22+
export interface IMenuBarViewState extends ViewVisibility {
23+
mode: keyof typeof MenuBarMode;
24+
}
25+
1526
export interface ILayout {
1627
splitPanePos: string[];
1728
horizontalSplitPanePos: string[];
1829
activityBar: ViewVisibility;
1930
panel: IPanelViewState;
2031
statusBar: ViewVisibility;
2132
sidebar: ISidebarViewState;
22-
menuBar: ViewVisibility;
33+
menuBar: IMenuBarViewState;
2334
}
2435

2536
export class LayoutModel implements ILayout {
@@ -29,15 +40,15 @@ export class LayoutModel implements ILayout {
2940
public panel: IPanelViewState;
3041
public statusBar: ViewVisibility;
3142
public sidebar: ISidebarViewState;
32-
public menuBar: ViewVisibility;
43+
public menuBar: IMenuBarViewState;
3344
constructor(
3445
splitPanePos: string[] = ['300px', 'auto'],
3546
horizontalSplitPanePos = ['70%', 'auto'],
3647
activityBar = { hidden: false },
3748
panel = { hidden: false, panelMaximized: false },
3849
statusBar = { hidden: false },
3950
sidebar = { hidden: false, position: Position.left },
40-
menuBar = { hidden: false }
51+
menuBar = { hidden: false, mode: MenuBarMode.vertical }
4152
) {
4253
this.splitPanePos = splitPanePos;
4354
this.horizontalSplitPanePos = horizontalSplitPanePos;

0 commit comments

Comments
 (0)