24
packages/application-extension/schema/menus.json
Normal 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"
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
70
ui-tests/test/editor.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
45
ui-tests/test/menus.spec.ts
Normal 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());
|
||||
});
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 31 KiB |
33
ui-tests/test/notebooks/empty.ipynb
Normal 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
|
||||
}
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |