Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.

Commit 3b9b2bb

Browse files
authored
Merge pull request #289 from jtpio/tree-menu
Improve menus
2 parents 748f9eb + f168b94 commit 3b9b2bb

28 files changed

+251
-31
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "RetroLab Menu Entries",
3+
"description": "RetroLab Menu Entries",
4+
"jupyter.lab.menus": {
5+
"main": [
6+
{
7+
"id": "jp-mainmenu-file",
8+
"items": [
9+
{
10+
"command": "application:rename",
11+
"rank": 4
12+
},
13+
{
14+
"command": "notebook:trust",
15+
"rank": 20
16+
}
17+
]
18+
}
19+
]
20+
},
21+
"properties": {},
22+
"additionalProperties": false,
23+
"type": "object"
24+
}

packages/application-extension/src/index.ts

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ namespace CommandIDs {
7979
*/
8080
export const openTree = 'application:open-tree';
8181

82+
/**
83+
* Rename the current document
84+
*/
85+
export const rename = 'application:rename';
86+
8287
/**
8388
* Resolve tree path
8489
*/
@@ -187,14 +192,34 @@ const opener: JupyterFrontEndPlugin<void> = {
187192
};
188193

189194
/**
190-
* A plugin to dispose the Tabs menu
195+
* A plugin to customize menus
196+
*
197+
* TODO: use this plugin to customize the menu items and their order
191198
*/
192-
const noTabsMenu: JupyterFrontEndPlugin<void> = {
193-
id: '@retrolab/application-extension:no-tabs-menu',
199+
const menus: JupyterFrontEndPlugin<void> = {
200+
id: '@retrolab/application-extension:menus',
194201
requires: [IMainMenu],
195202
autoStart: true,
196203
activate: (app: JupyterFrontEnd, menu: IMainMenu) => {
204+
// always disable the Tabs menu
197205
menu.tabsMenu.dispose();
206+
207+
const page = PageConfig.getOption('retroPage');
208+
switch (page) {
209+
case 'consoles':
210+
case 'terminals':
211+
case 'tree':
212+
menu.editMenu.dispose();
213+
menu.kernelMenu.dispose();
214+
menu.runMenu.dispose();
215+
break;
216+
case 'edit':
217+
menu.kernelMenu.dispose();
218+
menu.runMenu.dispose();
219+
break;
220+
default:
221+
break;
222+
}
198223
}
199224
};
200225

@@ -384,65 +409,88 @@ const tabTitle: JupyterFrontEndPlugin<void> = {
384409
const title: JupyterFrontEndPlugin<void> = {
385410
id: '@retrolab/application-extension:title',
386411
autoStart: true,
387-
requires: [IRetroShell],
412+
requires: [IRetroShell, ITranslator],
388413
optional: [IDocumentManager, IRouter],
389414
activate: (
390415
app: JupyterFrontEnd,
391416
shell: IRetroShell,
417+
translator: ITranslator,
392418
docManager: IDocumentManager | null,
393419
router: IRouter | null
394420
) => {
421+
const { commands } = app;
422+
const trans = translator.load('retrolab');
423+
395424
const widget = new Widget();
396425
widget.id = 'jp-title';
397426
app.shell.add(widget, 'top', { rank: 10 });
398427

399-
const addTitle = async () => {
428+
const addTitle = async (): Promise<void> => {
400429
const current = shell.currentWidget;
401430
if (!current || !(current instanceof DocumentWidget)) {
402431
return;
403432
}
404433
if (widget.node.children.length > 0) {
405434
return;
406435
}
436+
407437
const h = document.createElement('h1');
408438
h.textContent = current.title.label;
409439
widget.node.appendChild(h);
410440
widget.node.style.marginLeft = '10px';
411441
if (!docManager) {
412442
return;
413443
}
414-
widget.node.onclick = async () => {
415-
const result = await renameDialog(docManager, current.context.path);
416444

417-
// activate the current widget to bring the focus
418-
if (current) {
419-
current.activate();
420-
}
445+
const isEnabled = () => {
446+
const { currentWidget } = shell;
447+
return !!(currentWidget && docManager.contextForWidget(currentWidget));
448+
};
421449

422-
if (result === null) {
423-
return;
424-
}
450+
commands.addCommand(CommandIDs.rename, {
451+
label: () => trans.__('Rename…'),
452+
isEnabled,
453+
execute: async () => {
454+
if (!isEnabled()) {
455+
return;
456+
}
425457

426-
const newPath = current.context.path ?? result.path;
427-
const basename = PathExt.basename(newPath);
428-
h.textContent = basename;
429-
if (!router) {
430-
return;
431-
}
432-
const matches = router.current.path.match(TREE_PATTERN) ?? [];
433-
const [, route, path] = matches;
434-
if (!route || !path) {
435-
return;
458+
const result = await renameDialog(docManager, current.context.path);
459+
460+
// activate the current widget to bring the focus
461+
if (current) {
462+
current.activate();
463+
}
464+
465+
if (result === null) {
466+
return;
467+
}
468+
469+
const newPath = current.context.path ?? result.path;
470+
const basename = PathExt.basename(newPath);
471+
h.textContent = basename;
472+
if (!router) {
473+
return;
474+
}
475+
const matches = router.current.path.match(TREE_PATTERN) ?? [];
476+
const [, route, path] = matches;
477+
if (!route || !path) {
478+
return;
479+
}
480+
const encoded = encodeURIComponent(newPath);
481+
router.navigate(`/retro/${route}/${encoded}`, {
482+
skipRouting: true
483+
});
436484
}
437-
const encoded = encodeURIComponent(newPath);
438-
router.navigate(`/retro/${route}/${encoded}`, {
439-
skipRouting: true
440-
});
485+
});
486+
487+
widget.node.onclick = async () => {
488+
void commands.execute(CommandIDs.rename);
441489
};
442490
};
443491

444492
shell.currentChanged.connect(addTitle);
445-
addTitle();
493+
void addTitle();
446494
}
447495
};
448496

@@ -665,7 +713,7 @@ const zen: JupyterFrontEndPlugin<void> = {
665713
const plugins: JupyterFrontEndPlugin<any>[] = [
666714
dirty,
667715
logo,
668-
noTabsMenu,
716+
menus,
669717
opener,
670718
pages,
671719
paths,

packages/tree-extension/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const newTerminal: JupyterFrontEndPlugin<void> = {
135135
* A plugin to add the file browser widget to an ILabShell
136136
*/
137137
const browserWidget: JupyterFrontEndPlugin<void> = {
138-
id: '@jupyterlab-classic/tree-extension:widget',
138+
id: '@retrolab/tree-extension:widget',
139139
requires: [IFileBrowserFactory, ITranslator],
140140
optional: [IRunningSessionManagers],
141141
autoStart: true,

ui-tests/test/editor.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import path from 'path';
5+
6+
import { test } from './fixtures';
7+
8+
import { expect } from '@playwright/test';
9+
10+
const FILE = 'environment.yml';
11+
12+
test.use({ autoGoto: false });
13+
14+
const processRenameDialog = async (page, prevName: string, newName: string) => {
15+
// Rename in the input dialog
16+
await page.fill(
17+
`//div[normalize-space(.)='File Path${prevName}New Name']/input`,
18+
newName
19+
);
20+
21+
await Promise.all([
22+
await page.click('text="Rename"'),
23+
// wait until the URL is updated
24+
await page.waitForNavigation()
25+
]);
26+
};
27+
28+
test.describe('Editor', () => {
29+
test.beforeEach(async ({ page, tmpPath }) => {
30+
await page.contents.uploadFile(
31+
path.resolve(__dirname, `../../binder/${FILE}`),
32+
`${tmpPath}/${FILE}`
33+
);
34+
});
35+
36+
test('Renaming the file by clicking on the title', async ({
37+
page,
38+
tmpPath
39+
}) => {
40+
const file = `${tmpPath}/${FILE}`;
41+
await page.goto(`edit/${file}`);
42+
43+
// Click on the title
44+
await page.click(`text="${FILE}"`);
45+
46+
const newName = 'test.yml';
47+
await processRenameDialog(page, file, newName);
48+
49+
// Check the URL contains the new name
50+
const url = page.url();
51+
expect(url).toContain(newName);
52+
});
53+
54+
test('Renaming the file via the menu entry', async ({ page, tmpPath }) => {
55+
const file = `${tmpPath}/${FILE}`;
56+
await page.goto(`edit/${file}`);
57+
58+
// Click on the title
59+
await page.menu.clickMenuItem('File>Rename…');
60+
61+
// Rename in the input dialog
62+
const newName = 'test.yml';
63+
64+
await processRenameDialog(page, file, newName);
65+
66+
// Check the URL contains the new name
67+
const url = page.url();
68+
expect(url).toContain(newName);
69+
});
70+
});

ui-tests/test/menus.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import path from 'path';
5+
6+
import { test } from './fixtures';
7+
8+
import { expect } from '@playwright/test';
9+
10+
const NOTEBOOK = 'empty.ipynb';
11+
12+
const MENU_PATHS = [
13+
'File',
14+
'File>New',
15+
'Edit',
16+
'View',
17+
'Run',
18+
'Kernel',
19+
'Settings',
20+
'Settings>Theme',
21+
'Help'
22+
];
23+
24+
test.use({ autoGoto: false });
25+
26+
test.describe('Notebook Menus', () => {
27+
test.beforeEach(async ({ page, tmpPath }) => {
28+
await page.contents.uploadFile(
29+
path.resolve(__dirname, `./notebooks/${NOTEBOOK}`),
30+
`${tmpPath}/${NOTEBOOK}`
31+
);
32+
});
33+
34+
MENU_PATHS.forEach(menuPath => {
35+
test(`Open menu item ${menuPath}`, async ({ page, tmpPath }) => {
36+
await page.goto(`notebooks/${tmpPath}/${NOTEBOOK}`);
37+
await page.menu.open(menuPath);
38+
expect(await page.menu.isOpen(menuPath)).toBeTruthy();
39+
40+
const imageName = `opened-menu-${menuPath.replace(/>/g, '-')}.png`;
41+
const menu = await page.menu.getOpenMenu();
42+
expect(await menu.screenshot()).toMatchSnapshot(imageName.toLowerCase());
43+
});
44+
});
45+
});
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading

ui-tests/test/notebooks/empty.ipynb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "6f7028b9-4d2c-4fa2-96ee-bfa77bbee434",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": []
10+
}
11+
],
12+
"metadata": {
13+
"kernelspec": {
14+
"display_name": "Python 3 (ipykernel)",
15+
"language": "python",
16+
"name": "python3"
17+
},
18+
"language_info": {
19+
"codemirror_mode": {
20+
"name": "ipython",
21+
"version": 3
22+
},
23+
"file_extension": ".py",
24+
"mimetype": "text/x-python",
25+
"name": "python",
26+
"nbconvert_exporter": "python",
27+
"pygments_lexer": "ipython3",
28+
"version": "3.9.7"
29+
}
30+
},
31+
"nbformat": 4,
32+
"nbformat_minor": 5
33+
}
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)