-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: allow to show/hide navigation items (#8322)
* feat: allow to show/hide navigation items fixes podman-desktop/podman-desktop#7758 Signed-off-by: Florent Benoit <[email protected]>
- Loading branch information
Showing
13 changed files
with
627 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
packages/main/src/navigation-items-menu-builder.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/********************************************************************** | ||
* Copyright (C) 2024 Red Hat, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
***********************************************************************/ | ||
|
||
import type { BrowserWindow, ContextMenuParams, MenuItem, MenuItemConstructorOptions } from 'electron'; | ||
import { beforeEach, describe, expect, test, vi } from 'vitest'; | ||
|
||
import { NavigationItemsMenuBuilder } from './navigation-items-menu-builder.js'; | ||
import type { ConfigurationRegistry } from './plugin/configuration-registry.js'; | ||
|
||
let navigationItemsMenuBuilder: TestNavigationItemsMenuBuilder; | ||
|
||
const getConfigurationMock = vi.fn(); | ||
const configurationRegistryMock = { | ||
getConfiguration: getConfigurationMock, | ||
updateConfigurationValue: vi.fn(), | ||
} as unknown as ConfigurationRegistry; | ||
|
||
const browserWindowMock = { | ||
webContents: {}, | ||
} as unknown as BrowserWindow; | ||
|
||
class TestNavigationItemsMenuBuilder extends NavigationItemsMenuBuilder { | ||
buildHideMenuItem(linkText: string): MenuItemConstructorOptions | undefined { | ||
return super.buildHideMenuItem(linkText); | ||
} | ||
buildNavigationToggleMenuItems(): MenuItemConstructorOptions[] { | ||
return super.buildNavigationToggleMenuItems(); | ||
} | ||
} | ||
|
||
beforeEach(() => { | ||
vi.resetAllMocks(); | ||
navigationItemsMenuBuilder = new TestNavigationItemsMenuBuilder(configurationRegistryMock); | ||
}); | ||
|
||
describe('buildHideMenuItem', async () => { | ||
test('build hide item', async () => { | ||
getConfigurationMock.mockReturnValue({ get: () => [] } as unknown as ConfigurationRegistry); | ||
|
||
const menu = navigationItemsMenuBuilder.buildHideMenuItem('Hello'); | ||
expect(menu?.label).toBe(`Hide 'Hello'`); | ||
expect(menu?.click).toBeDefined(); | ||
expect(menu?.visible).toBe(true); | ||
|
||
// click on the menu | ||
menu?.click?.({} as MenuItem, browserWindowMock, {} as unknown as KeyboardEvent); | ||
|
||
expect(getConfigurationMock).toBeCalled(); | ||
// if clicking it should send the item to the configuration as being disabled | ||
expect(configurationRegistryMock.updateConfigurationValue).toBeCalledWith( | ||
'navbar.disabledItems', | ||
['Hello'], | ||
'DEFAULT', | ||
); | ||
}); | ||
|
||
test('should not create a menu item if in excluded list', async () => { | ||
getConfigurationMock.mockReturnValue({ get: () => [] } as unknown as ConfigurationRegistry); | ||
|
||
const menu = navigationItemsMenuBuilder.buildHideMenuItem('Accounts'); | ||
expect(menu).toBeUndefined(); | ||
}); | ||
}); | ||
|
||
describe('buildNavigationToggleMenuItems', async () => { | ||
test('build navigation toggle menu items', async () => { | ||
getConfigurationMock.mockReturnValue({ get: () => ['existing'] } as unknown as ConfigurationRegistry); | ||
|
||
// send 3 items, two being visible, one being hidden | ||
navigationItemsMenuBuilder.receiveNavigationItems([ | ||
{ name: 'A & A', visible: true }, | ||
{ name: 'B', visible: false }, | ||
{ name: 'C', visible: true }, | ||
]); | ||
|
||
const menu = navigationItemsMenuBuilder.buildNavigationToggleMenuItems(); | ||
|
||
// 4 items (first one being a separator) | ||
expect(menu.length).toBe(4); | ||
|
||
// check the first item is a separator | ||
expect(menu[0].type).toBe('separator'); | ||
|
||
// label should be escaped as we have an & | ||
expect(menu[1].label).toBe('A && A'); | ||
expect(menu[1].checked).toBe(true); | ||
expect(menu[2].label).toBe('B'); | ||
expect(menu[2].checked).toBe(false); | ||
expect(menu[3].label).toBe('C'); | ||
expect(menu[3].checked).toBe(true); | ||
|
||
// click on the A item | ||
menu[1].click?.({} as MenuItem, browserWindowMock, {} as unknown as KeyboardEvent); | ||
|
||
expect(getConfigurationMock).toBeCalled(); | ||
// if clicking it should send the item to the configuration as being disabled | ||
expect(configurationRegistryMock.updateConfigurationValue).toBeCalledWith( | ||
'navbar.disabledItems', | ||
// item A & A should not be escaped | ||
['existing', 'A & A'], | ||
'DEFAULT', | ||
); | ||
|
||
// reset the calls | ||
vi.mocked(configurationRegistryMock.updateConfigurationValue).mockClear(); | ||
|
||
// click on the B item should unhide it so disabled items should be empty | ||
menu[2].click?.({} as MenuItem, browserWindowMock, {} as unknown as KeyboardEvent); | ||
expect(configurationRegistryMock.updateConfigurationValue).toBeCalledWith( | ||
'navbar.disabledItems', | ||
['existing'], | ||
'DEFAULT', | ||
); | ||
}); | ||
}); | ||
|
||
describe('buildNavigationMenu', async () => { | ||
test('no items if no linktext', async () => { | ||
const parameters = {} as unknown as ContextMenuParams; | ||
|
||
const menu = navigationItemsMenuBuilder.buildNavigationMenu(parameters); | ||
|
||
expect(menu).toStrictEqual([]); | ||
}); | ||
|
||
test('no items if outside of range of navbar', async () => { | ||
const parameters = { | ||
linkText: 'outside', | ||
x: 0, | ||
y: 0, | ||
} as unknown as ContextMenuParams; | ||
|
||
const menu = navigationItemsMenuBuilder.buildNavigationMenu(parameters); | ||
|
||
expect(menu).toStrictEqual([]); | ||
}); | ||
|
||
test('should call the build if inside range of navbar', async () => { | ||
const spyMock = vi.spyOn(navigationItemsMenuBuilder, 'buildHideMenuItem'); | ||
spyMock.mockReturnValue({} as MenuItemConstructorOptions); | ||
const parameters = { | ||
linkText: 'inside', | ||
x: 30, | ||
y: 100, | ||
} as unknown as ContextMenuParams; | ||
|
||
const menu = navigationItemsMenuBuilder.buildNavigationMenu(parameters); | ||
|
||
expect(menu.length).toBe(1); | ||
expect(spyMock).toBeCalledWith('inside'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/********************************************************************** | ||
* Copyright (C) 2024 Red Hat, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
***********************************************************************/ | ||
|
||
import type { ContextMenuParams, MenuItemConstructorOptions } from 'electron'; | ||
|
||
import type { ConfigurationRegistry } from './plugin/configuration-registry.js'; | ||
import { CONFIGURATION_DEFAULT_SCOPE } from './plugin/configuration-registry-constants.js'; | ||
|
||
// items that can't be hidden | ||
const EXCLUDED_ITEMS = ['Accounts', 'Settings']; | ||
|
||
// This class is responsible of creating the items to hide a given selected item of the left navigation bar | ||
// and also display a list of all items with the ability to toggle the visibility of each item. | ||
export class NavigationItemsMenuBuilder { | ||
private navigationItems: { name: string; visible: boolean }[] = []; | ||
|
||
constructor(private configurationRegistry: ConfigurationRegistry) {} | ||
|
||
receiveNavigationItems(data: { name: string; visible: boolean }[]): void { | ||
this.navigationItems = data; | ||
} | ||
|
||
protected async updateNavbarHiddenItem(itemName: string, visible: boolean): Promise<void> { | ||
// grab the disabled items, and add the new one | ||
const configuration = this.configurationRegistry.getConfiguration('navbar'); | ||
let items = configuration.get<string[]>('disabledItems', []); | ||
|
||
if (visible) { | ||
items = items.filter(i => i !== itemName); | ||
} else if (!items.includes(itemName)) { | ||
items.push(itemName); | ||
} | ||
await this.configurationRegistry.updateConfigurationValue( | ||
'navbar.disabledItems', | ||
items, | ||
CONFIGURATION_DEFAULT_SCOPE, | ||
); | ||
} | ||
|
||
protected escapeLabel(label: string): string { | ||
return label.replace('&', '&&'); | ||
} | ||
|
||
protected buildHideMenuItem(linkText: string): MenuItemConstructorOptions | undefined { | ||
const rawItemName = linkText; | ||
|
||
// need to filter any counter from the item name | ||
// it's at the end with parenthesis like itemName (2) | ||
const itemName = rawItemName.replace(/\s\(\d+\)$/, ''); | ||
|
||
if (EXCLUDED_ITEMS.includes(itemName)) { | ||
return undefined; | ||
} | ||
|
||
// on electron, need to esccape the & character to show it | ||
const itemDisplayName = this.escapeLabel(itemName); | ||
|
||
const item: MenuItemConstructorOptions = { | ||
label: `Hide '${itemDisplayName}'`, | ||
visible: true, | ||
click: (): void => { | ||
// flag the item as being disabled | ||
this.updateNavbarHiddenItem(itemName, false).catch((e: unknown) => console.error('error disabling item', e)); | ||
}, | ||
}; | ||
return item; | ||
} | ||
|
||
protected buildNavigationToggleMenuItems(): MenuItemConstructorOptions[] { | ||
const items: MenuItemConstructorOptions[] = []; | ||
|
||
// add all navigation items to be able to show/hide them | ||
const menuForNavItems: Electron.MenuItemConstructorOptions[] = this.navigationItems.map(item => ({ | ||
label: this.escapeLabel(item.name), | ||
type: 'checkbox', | ||
checked: item.visible, | ||
click: (): void => { | ||
// send the item to the frontend to show/hide it | ||
this.updateNavbarHiddenItem(item.name, !item.visible).catch((e: unknown) => | ||
console.error('error disabling item', e), | ||
); | ||
}, | ||
})); | ||
if (menuForNavItems.length > 0) { | ||
// add separator | ||
items.push({ type: 'separator' }); | ||
// add all items | ||
items.push(...menuForNavItems); | ||
} | ||
|
||
return items; | ||
} | ||
|
||
buildNavigationMenu(parameters: ContextMenuParams): MenuItemConstructorOptions[] { | ||
const items: MenuItemConstructorOptions[] = []; | ||
// allow to hide the item being selected | ||
if (parameters.linkText && parameters.x < 48 && parameters.y > 76) { | ||
const menu = this.buildHideMenuItem(parameters.linkText); | ||
if (menu) { | ||
items.push(menu); | ||
} | ||
} | ||
if (parameters.x < 48) { | ||
// add all navigation items to be able to show/hide them | ||
items.push(...this.buildNavigationToggleMenuItems()); | ||
} | ||
return items; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.