Merge pull request #289 from jtpio/tree-menu

Improve menus
This commit is contained in:
Jeremy Tuloup 2021-11-18 21:33:29 +01:00 committed by GitHub
commit 3b9b2bb17c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 254 additions and 34 deletions

View File

@ -0,0 +1,24 @@
{
"title": "RetroLab Menu Entries",
"description": "RetroLab Menu Entries",
"jupyter.lab.menus": {
"main": [
{
"id": "jp-mainmenu-file",
"items": [
{
"command": "application:rename",
"rank": 4
},
{
"command": "notebook:trust",
"rank": 20
}
]
}
]
},
"properties": {},
"additionalProperties": false,
"type": "object"
}

View File

@ -79,6 +79,11 @@ namespace CommandIDs {
*/
export const openTree = 'application:open-tree';
/**
* Rename the current document
*/
export const rename = 'application:rename';
/**
* Resolve tree path
*/
@ -187,14 +192,34 @@ const opener: JupyterFrontEndPlugin<void> = {
};
/**
* A plugin to dispose the Tabs menu
* A plugin to customize menus
*
* TODO: use this plugin to customize the menu items and their order
*/
const noTabsMenu: JupyterFrontEndPlugin<void> = {
id: '@retrolab/application-extension:no-tabs-menu',
const menus: JupyterFrontEndPlugin<void> = {
id: '@retrolab/application-extension:menus',
requires: [IMainMenu],
autoStart: true,
activate: (app: JupyterFrontEnd, menu: IMainMenu) => {
// always disable the Tabs menu
menu.tabsMenu.dispose();
const page = PageConfig.getOption('retroPage');
switch (page) {
case 'consoles':
case 'terminals':
case 'tree':
menu.editMenu.dispose();
menu.kernelMenu.dispose();
menu.runMenu.dispose();
break;
case 'edit':
menu.kernelMenu.dispose();
menu.runMenu.dispose();
break;
default:
break;
}
}
};
@ -384,19 +409,23 @@ const tabTitle: JupyterFrontEndPlugin<void> = {
const title: JupyterFrontEndPlugin<void> = {
id: '@retrolab/application-extension:title',
autoStart: true,
requires: [IRetroShell],
requires: [IRetroShell, ITranslator],
optional: [IDocumentManager, IRouter],
activate: (
app: JupyterFrontEnd,
shell: IRetroShell,
translator: ITranslator,
docManager: IDocumentManager | null,
router: IRouter | null
) => {
const { commands } = app;
const trans = translator.load('retrolab');
const widget = new Widget();
widget.id = 'jp-title';
app.shell.add(widget, 'top', { rank: 10 });
const addTitle = async () => {
const addTitle = async (): Promise<void> => {
const current = shell.currentWidget;
if (!current || !(current instanceof DocumentWidget)) {
return;
@ -404,6 +433,7 @@ const title: JupyterFrontEndPlugin<void> = {
if (widget.node.children.length > 0) {
return;
}
const h = document.createElement('h1');
h.textContent = current.title.label;
widget.node.appendChild(h);
@ -411,38 +441,56 @@ const title: JupyterFrontEndPlugin<void> = {
if (!docManager) {
return;
}
const isEnabled = () => {
const { currentWidget } = shell;
return !!(currentWidget && docManager.contextForWidget(currentWidget));
};
commands.addCommand(CommandIDs.rename, {
label: () => trans.__('Rename…'),
isEnabled,
execute: async () => {
if (!isEnabled()) {
return;
}
const result = await renameDialog(docManager, current.context.path);
// activate the current widget to bring the focus
if (current) {
current.activate();
}
if (result === null) {
return;
}
const newPath = current.context.path ?? result.path;
const basename = PathExt.basename(newPath);
h.textContent = basename;
if (!router) {
return;
}
const matches = router.current.path.match(TREE_PATTERN) ?? [];
const [, route, path] = matches;
if (!route || !path) {
return;
}
const encoded = encodeURIComponent(newPath);
router.navigate(`/retro/${route}/${encoded}`, {
skipRouting: true
});
}
});
widget.node.onclick = async () => {
const result = await renameDialog(docManager, current.context.path);
// activate the current widget to bring the focus
if (current) {
current.activate();
}
if (result === null) {
return;
}
const newPath = current.context.path ?? result.path;
const basename = PathExt.basename(newPath);
h.textContent = basename;
if (!router) {
return;
}
const matches = router.current.path.match(TREE_PATTERN) ?? [];
const [, route, path] = matches;
if (!route || !path) {
return;
}
const encoded = encodeURIComponent(newPath);
router.navigate(`/retro/${route}/${encoded}`, {
skipRouting: true
});
void commands.execute(CommandIDs.rename);
};
};
shell.currentChanged.connect(addTitle);
addTitle();
void addTitle();
}
};
@ -665,7 +713,7 @@ const zen: JupyterFrontEndPlugin<void> = {
const plugins: JupyterFrontEndPlugin<any>[] = [
dirty,
logo,
noTabsMenu,
menus,
opener,
pages,
paths,

View File

@ -135,7 +135,7 @@ const newTerminal: JupyterFrontEndPlugin<void> = {
* A plugin to add the file browser widget to an ILabShell
*/
const browserWidget: JupyterFrontEndPlugin<void> = {
id: '@jupyterlab-classic/tree-extension:widget',
id: '@retrolab/tree-extension:widget',
requires: [IFileBrowserFactory, ITranslator],
optional: [IRunningSessionManagers],
autoStart: true,

View File

@ -0,0 +1,70 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import path from 'path';
import { test } from './fixtures';
import { expect } from '@playwright/test';
const FILE = 'environment.yml';
test.use({ autoGoto: false });
const processRenameDialog = async (page, prevName: string, newName: string) => {
// Rename in the input dialog
await page.fill(
`//div[normalize-space(.)='File Path${prevName}New Name']/input`,
newName
);
await Promise.all([
await page.click('text="Rename"'),
// wait until the URL is updated
await page.waitForNavigation()
]);
};
test.describe('Editor', () => {
test.beforeEach(async ({ page, tmpPath }) => {
await page.contents.uploadFile(
path.resolve(__dirname, `../../binder/${FILE}`),
`${tmpPath}/${FILE}`
);
});
test('Renaming the file by clicking on the title', async ({
page,
tmpPath
}) => {
const file = `${tmpPath}/${FILE}`;
await page.goto(`edit/${file}`);
// Click on the title
await page.click(`text="${FILE}"`);
const newName = 'test.yml';
await processRenameDialog(page, file, newName);
// Check the URL contains the new name
const url = page.url();
expect(url).toContain(newName);
});
test('Renaming the file via the menu entry', async ({ page, tmpPath }) => {
const file = `${tmpPath}/${FILE}`;
await page.goto(`edit/${file}`);
// Click on the title
await page.menu.clickMenuItem('File>Rename…');
// Rename in the input dialog
const newName = 'test.yml';
await processRenameDialog(page, file, newName);
// Check the URL contains the new name
const url = page.url();
expect(url).toContain(newName);
});
});

View File

@ -0,0 +1,45 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import path from 'path';
import { test } from './fixtures';
import { expect } from '@playwright/test';
const NOTEBOOK = 'empty.ipynb';
const MENU_PATHS = [
'File',
'File>New',
'Edit',
'View',
'Run',
'Kernel',
'Settings',
'Settings>Theme',
'Help'
];
test.use({ autoGoto: false });
test.describe('Notebook Menus', () => {
test.beforeEach(async ({ page, tmpPath }) => {
await page.contents.uploadFile(
path.resolve(__dirname, `./notebooks/${NOTEBOOK}`),
`${tmpPath}/${NOTEBOOK}`
);
});
MENU_PATHS.forEach(menuPath => {
test(`Open menu item ${menuPath}`, async ({ page, tmpPath }) => {
await page.goto(`notebooks/${tmpPath}/${NOTEBOOK}`);
await page.menu.open(menuPath);
expect(await page.menu.isOpen(menuPath)).toBeTruthy();
const imageName = `opened-menu-${menuPath.replace(/>/g, '-')}.png`;
const menu = await page.menu.getOpenMenu();
expect(await menu.screenshot()).toMatchSnapshot(imageName.toLowerCase());
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,33 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "6f7028b9-4d2c-4fa2-96ee-bfa77bbee434",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB