Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add file browser actions to the file browser toolbar #6888

Merged
merged 15 commits into from May 29, 2023
10 changes: 10 additions & 0 deletions packages/tree-extension/schema/file-actions.json
@@ -0,0 +1,10 @@
{
"title": "File Browser Widget - File Actions",
"description": "File Browser widget - File Actions settings.",
"jupyter.lab.toolbars": {
"FileBrowser": [{ "name": "fileActions", "rank": 0 }]
},
"properties": {},
"additionalProperties": false,
"type": "object"
}
129 changes: 129 additions & 0 deletions packages/tree-extension/src/fileactions.tsx
@@ -0,0 +1,129 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import {
CommandToolbarButtonComponent,
ReactWidget,
UseSignal,
} from '@jupyterlab/apputils';

import { FileBrowser } from '@jupyterlab/filebrowser';

import { ITranslator } from '@jupyterlab/translation';

import { CommandRegistry } from '@lumino/commands';

import { ISignal } from '@lumino/signaling';

import React from 'react';

/**
* A React component to display the list of command toolbar buttons.
*
*/
const Commands = ({
commands,
browser,
translator,
}: {
commands: CommandRegistry;
browser: FileBrowser;
translator: ITranslator;
}): JSX.Element => {
const trans = translator.load('notebook');
const selection = Array.from(browser.selectedItems());
const oneFolder = selection.some((item) => item.type === 'directory');
const multipleFiles =
selection.filter((item) => item.type === 'file').length > 1;
if (selection.length === 0) {
return <div>{trans.__('Select items to perform actions on them.')}</div>;
} else {
const buttons = ['delete'];
if (!oneFolder) {
buttons.unshift('duplicate');
if (!multipleFiles) {
buttons.unshift('rename');
}
buttons.unshift('download');
buttons.unshift('open');
} else if (selection.length === 1) {
buttons.unshift('rename');
}

return (
<>
{buttons.map((action) => (
<CommandToolbarButtonComponent
key={action}
commands={commands}
id={`filebrowser:${action}`}
args={{ toolbar: true }}
icon={undefined}
/>
))}
</>
);
}
};

/**
* A React component to display the file action buttons in the file browser toolbar.
*
* @param translator The Translation service
*/
const FileActions = ({
commands,
browser,
selectionChanged,
translator,
}: {
commands: CommandRegistry;
browser: FileBrowser;
selectionChanged: ISignal<FileBrowser, void>;
translator: ITranslator;
}): JSX.Element => {
return (
<UseSignal signal={selectionChanged} shouldUpdate={() => true}>
{(): JSX.Element => (
<Commands
commands={commands}
browser={browser}
translator={translator}
/>
)}
</UseSignal>
);
};

/**
* A namespace for FileActionsComponent statics.
*/
export namespace FileActionsComponent {
/**
* Create a new FileActionsComponent
*
* @param translator The translator
*/
export const create = ({
commands,
browser,
selectionChanged,
translator,
}: {
commands: CommandRegistry;
browser: FileBrowser;
selectionChanged: ISignal<FileBrowser, void>;
translator: ITranslator;
}): ReactWidget => {
const widget = ReactWidget.create(
<FileActions
commands={commands}
browser={browser}
selectionChanged={selectionChanged}
translator={translator}
/>
);
widget.addClass('jp-FileActions');
return widget;
};
}
54 changes: 53 additions & 1 deletion packages/tree-extension/src/index.ts
Expand Up @@ -39,10 +39,14 @@ import {
runningIcon,
} from '@jupyterlab/ui-components';

import { Signal } from '@lumino/signaling';

import { Menu, MenuBar } from '@lumino/widgets';

import { NotebookTreeWidget, INotebookTree } from '@jupyter-notebook/tree';

import { FileActionsComponent } from './fileactions';

/**
* The file browser factory.
*/
Expand Down Expand Up @@ -119,6 +123,54 @@ const createNew: JupyterFrontEndPlugin<void> = {
},
};

/**
* A plugin to add file browser actions to the file browser toolbar.
*/
const fileActions: JupyterFrontEndPlugin<void> = {
id: '@jupyter-notebook/tree-extension:file-actions',
autoStart: true,
requires: [IDefaultFileBrowser, IToolbarWidgetRegistry, ITranslator],
activate: (
app: JupyterFrontEnd,
browser: IDefaultFileBrowser,
toolbarRegistry: IToolbarWidgetRegistry,
translator: ITranslator
) => {
// TODO: use upstream signal when available to detect selection changes
// https://github.com/jupyterlab/jupyterlab/issues/14598
const selectionChanged = new Signal<FileBrowser, void>(browser);
const methods = [
'_selectItem',
'_handleMultiSelect',
'handleFileSelect',
] as const;
methods.forEach((method: (typeof methods)[number]) => {
const original = browser['listing'][method];
browser['listing'][method] = (...args: any[]) => {
original.call(browser['listing'], ...args);
selectionChanged.emit(void 0);
};
});

// Create a toolbar item that adds buttons to the file browser toolbar
// to perform actions on the files
toolbarRegistry.addFactory(
FILE_BROWSER_FACTORY,
'fileActions',
(browser: FileBrowser) => {
const { commands } = app;
const fileActions = FileActionsComponent.create({
commands,
browser,
selectionChanged,
translator,
});
return fileActions;
}
);
},
};

/**
* Plugin to load the default plugins that are loaded on all the Notebook pages
* (tree, edit, view, etc.) so they are visible in the settings editor.
Expand Down Expand Up @@ -238,7 +290,6 @@ const notebookTreeWidget: JupyterFrontEndPlugin<INotebookTree> = {
nbTreeWidget.tabBar.addTab(browser.title);
nbTreeWidget.tabsMovable = false;

// Toolbar
toolbarRegistry.addFactory(
FILE_BROWSER_FACTORY,
'uploader',
Expand Down Expand Up @@ -331,6 +382,7 @@ const notebookTreeWidget: JupyterFrontEndPlugin<INotebookTree> = {
*/
const plugins: JupyterFrontEndPlugin<any>[] = [
createNew,
fileActions,
loadPlugins,
openFileBrowser,
notebookTreeWidget,
Expand Down
31 changes: 31 additions & 0 deletions packages/tree-extension/style/base.css
Expand Up @@ -24,3 +24,34 @@
.jp-FileBrowser-filterBox input {
line-height: 24px;
}

.jp-DirListing-content .jp-DirListing-checkboxWrapper {
visibility: visible;
}

/* Action buttons */

.jp-FileBrowser-toolbar > .jp-FileActions.jp-Toolbar-item {
display: flex;
flex-direction: row;
}

.jp-FileActions .jp-ToolbarButtonComponent-icon {
display: none;
}

.jp-FileActions .jp-ToolbarButtonComponent[data-command='filebrowser:delete'] {
background-color: var(--jp-error-color1);
}

.jp-FileActions
.jp-ToolbarButtonComponent[data-command='filebrowser:delete']
.jp-ToolbarButtonComponent-label {
color: var(--jp-ui-inverse-font-color1);
}

.jp-FileBrowser-toolbar .jp-FileActions .jp-ToolbarButtonComponent {
border: solid 1px var(--jp-border-color2);
margin: 1px;
min-height: 100%;
}
84 changes: 84 additions & 0 deletions ui-tests/test/filebrowser.spec.ts
@@ -0,0 +1,84 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import path from 'path';

import { expect } from '@playwright/test';

import { test } from './fixtures';

test.describe('File Browser', () => {
test.beforeEach(async ({ page, tmpPath }) => {
await page.contents.uploadFile(
path.resolve(__dirname, './notebooks/empty.ipynb'),
`${tmpPath}/empty.ipynb`
);
await page.contents.createDirectory(`${tmpPath}/folder1`);
await page.contents.createDirectory(`${tmpPath}/folder2`);
});

test('Select one folder', async ({ page, tmpPath }) => {
await page.filebrowser.refresh();

await page.getByText('folder1').last().click();

const toolbar = page.getByRole('navigation');

expect(toolbar.getByText('Rename')).toBeVisible();
expect(toolbar.getByText('Delete')).toBeVisible();
});

test('Select one file', async ({ page, tmpPath }) => {
await page.filebrowser.refresh();

await page.getByText('empty.ipynb').last().click();

const toolbar = page.getByRole('navigation');

['Rename', 'Delete', 'Open', 'Download', 'Delete'].forEach(async (text) => {
expect(toolbar.getByText(text)).toBeVisible();

Check failure on line 39 in ui-tests/test/filebrowser.spec.ts

View workflow job for this annotation

GitHub Actions / ui-tests (firefox)

[firefox] › test/filebrowser.spec.ts:31:7 › File Browser › Select one file

1) [firefox] › test/filebrowser.spec.ts:31:7 › File Browser › Select one file ──────────────────── Error: expect(received).toBeVisible() Call log: - expect.toBeVisible with timeout 5000ms - waiting for getByRole('navigation').getByText('Delete') 37 | 38 | ['Rename', 'Delete', 'Open', 'Download', 'Delete'].forEach(async (text) => { > 39 | expect(toolbar.getByText(text)).toBeVisible(); | ^ 40 | }); 41 | }); 42 | at forEach (/home/runner/work/notebook/notebook/ui-tests/test/filebrowser.spec.ts:39:39) at /home/runner/work/notebook/notebook/ui-tests/test/filebrowser.spec.ts:38:56
});
});

test('Select files and folders', async ({ page, tmpPath }) => {
await page.filebrowser.refresh();

await page.keyboard.down('Control');
await page.getByText('folder1').last().click();
await page.getByText('folder2').last().click();
await page.getByText('empty.ipynb').last().click();

const toolbar = page.getByRole('navigation');

expect(toolbar.getByText('Rename')).toBeHidden();
expect(toolbar.getByText('Open')).toBeHidden();
expect(toolbar.getByText('Delete')).toBeVisible();
});

test('Select files and open', async ({ page, tmpPath }) => {
// upload an additional notebook
await page.contents.uploadFile(
path.resolve(__dirname, './notebooks/simple.ipynb'),
`${tmpPath}/simple.ipynb`
);
await page.filebrowser.refresh();

await page.keyboard.down('Control');
await page.getByText('simple.ipynb').last().click();
await page.getByText('empty.ipynb').last().click();

const toolbar = page.getByRole('navigation');

const [nb1, nb2] = await Promise.all([
page.waitForEvent('popup'),
page.waitForEvent('popup'),
toolbar.getByText('Open').last().click(),
]);

await nb1.waitForLoadState();
await nb1.close();

await nb2.waitForLoadState();
await nb2.close();
});
});
Binary file modified ui-tests/test/mobile.spec.ts-snapshots/tree-chromium-linux.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ui-tests/test/mobile.spec.ts-snapshots/tree-firefox-linux.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.