Skip to content

Commit

Permalink
Add defaultPath option to set the default directory for file dialog (j…
Browse files Browse the repository at this point in the history
…upyterlab#15282)

* add defaultPath option

* add tests for defaultPath option

* Fixed floating promise

There was a floating promise in createFilteredFileBrowser as a result
of calling model.cd(). To fix the floating promise,
createdFilteredFileBrowser is async and respective changes propagated to
places it was used.

* Pass the error reason

* Lint

---------

Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com>
  • Loading branch information
mmichilot and krassowski authored Nov 2, 2023
1 parent b7d8385 commit c6e7a14
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 40 deletions.
119 changes: 81 additions & 38 deletions packages/filebrowser/src/opendialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PanelLayout, Widget } from '@lumino/widgets';
import { FileBrowser } from './browser';
import { FilterFileBrowserModel } from './model';
import { IFileBrowserFactory } from './tokens';
import { PromiseDelegate } from '@lumino/coreutils';

/**
* The class name added to open file dialog
Expand Down Expand Up @@ -43,6 +44,11 @@ export namespace FileDialog {
* The application language translator.
*/
translator?: ITranslator;

/**
* Default path to open
*/
defaultPath?: string;
}

/**
Expand Down Expand Up @@ -70,11 +76,17 @@ export namespace FileDialog {
*
* @returns A promise that resolves with whether the dialog was accepted.
*/
export function getOpenFiles(
export async function getOpenFiles(
options: IFileOptions
): Promise<Dialog.IResult<Contents.IModel[]>> {
const translator = options.translator || nullTranslator;
const trans = translator.load('jupyterlab');
const openDialog = new OpenDialog(
options.manager,
options.filter,
translator,
options.defaultPath
);
const dialogOptions: Partial<Dialog.IOptions<Contents.IModel[]>> = {
title: options.title,
buttons: [
Expand All @@ -86,8 +98,11 @@ export namespace FileDialog {
focusNodeSelector: options.focusNodeSelector,
host: options.host,
renderer: options.renderer,
body: new OpenDialog(options.manager, options.filter, translator)
body: openDialog
};

await openDialog.ready;

const dialog = new Dialog(dialogOptions);
return dialog.launch();
}
Expand Down Expand Up @@ -125,57 +140,71 @@ class OpenDialog
manager: IDocumentManager,
filter?: (value: Contents.IModel) => Partial<IScore> | null,
translator?: ITranslator,
defaultPath?: string,
filterDirectories?: boolean
) {
super();
translator = translator ?? nullTranslator;
const trans = translator.load('jupyterlab');
this.addClass(OPEN_DIALOG_CLASS);

this._browser = Private.createFilteredFileBrowser(
Private.createFilteredFileBrowser(
'filtered-file-browser-dialog',
manager,
filter,
{},
translator,
defaultPath,
filterDirectories
);
)
.then(browser => {
this._browser = browser;

// Add toolbar items
setToolbar(this._browser, (browser: FileBrowser) => [
{
name: 'new-folder',
widget: new ToolbarButton({
icon: newFolderIcon,
onClick: () => {
void browser.createNewDirectory();
},
tooltip: trans.__('New Folder')
})
},
{
name: 'refresher',
widget: new ToolbarButton({
icon: refreshIcon,
onClick: () => {
browser.model.refresh().catch(reason => {
console.error(
'Failed to refresh file browser in open dialog.',
reason
);
});
// Add toolbar items
setToolbar(this._browser, (browser: FileBrowser) => [
{
name: 'new-folder',
widget: new ToolbarButton({
icon: newFolderIcon,
onClick: () => {
void browser.createNewDirectory();
},
tooltip: trans.__('New Folder')
})
},
tooltip: trans.__('Refresh File List')
})
}
]);
{
name: 'refresher',
widget: new ToolbarButton({
icon: refreshIcon,
onClick: () => {
browser.model.refresh().catch(reason => {
console.error(
'Failed to refresh file browser in open dialog.',
reason
);
});
},
tooltip: trans.__('Refresh File List')
})
}
]);

// Build the sub widgets
const layout = new PanelLayout();
layout.addWidget(this._browser);

// Build the sub widgets
const layout = new PanelLayout();
layout.addWidget(this._browser);
// Set Widget content
this.layout = layout;

// Set Widget content
this.layout = layout;
this._ready.resolve();
})
.catch(reason => {
console.error(
'Error while creating file browser in open dialog',
reason
);
this._ready.reject(void 0);
});
}

/**
Expand Down Expand Up @@ -203,6 +232,14 @@ class OpenDialog
}
}

/**
* A promise that resolves when openDialog is successfully created.
*/
get ready(): Promise<void> {
return this._ready.promise;
}

private _ready: PromiseDelegate<void> = new PromiseDelegate<void>();
private _browser: FileBrowser;
}

Expand All @@ -229,14 +266,15 @@ namespace Private {
* as the initial ID passed into the factory is used for only one file browser
* instance.
*/
export const createFilteredFileBrowser = (
export const createFilteredFileBrowser = async (
id: string,
manager: IDocumentManager,
filter?: (value: Contents.IModel) => Partial<IScore> | null,
options: IFileBrowserFactory.IOptions = {},
translator?: ITranslator,
defaultPath?: string,
filterDirectories?: boolean
): FileBrowser => {
): Promise<FileBrowser> => {
translator = translator || nullTranslator;
const model = new FilterFileBrowserModel({
manager,
Expand All @@ -246,6 +284,11 @@ namespace Private {
refreshInterval: options.refreshInterval,
filterDirectories
});

if (defaultPath) {
await model.cd(defaultPath);
}

const widget = new FileBrowser({
id,
model,
Expand Down
108 changes: 106 additions & 2 deletions packages/filebrowser/test/openfiledialog.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { PathExt } from '@jupyterlab/coreutils';
import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager';
import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry';
import { DocumentWidgetOpenerMock } from '@jupyterlab/docregistry/lib/testutils';
Expand All @@ -21,6 +21,7 @@ describe('@jupyterlab/filebrowser', () => {
let manager: IDocumentManager;
let serviceManager: ServiceManager.IManager;
let registry: DocumentRegistry;
let testDirectory: string;

beforeAll(async () => {
const opener = new DocumentWidgetOpenerMock();
Expand All @@ -36,9 +37,14 @@ describe('@jupyterlab/filebrowser', () => {
});

const contents = serviceManager.contents;
await contents.newUntitled({ type: 'directory' });
const directory = await contents.newUntitled({ type: 'directory' });
await contents.newUntitled({ type: 'file' });
await contents.newUntitled({ type: 'notebook' });

// Place a notebook and directory within the test directory (to test defaultPath)
testDirectory = directory.path;
await contents.newUntitled({ type: 'notebook', path: testDirectory });
await contents.newUntitled({ type: 'directory', path: testDirectory });
});

describe('FilterFileBrowserModel', () => {
Expand Down Expand Up @@ -123,6 +129,56 @@ describe('@jupyterlab/filebrowser', () => {
expect(notebooks.length).toBeGreaterThan(0);
});
});

it('should return one selected file whose path matches default path', async () => {
const node = document.createElement('div');

document.body.appendChild(node);

const dialog = FileDialog.getOpenFiles({
manager,
title: 'Select a notebook',
host: node,
defaultPath: testDirectory,
filter: (value: Contents.IModel) =>
value.type === 'notebook' ? {} : null
});

await waitForDialog();
await framePromise();

let counter = 0;
const listing = node.getElementsByClassName('jp-DirListing-content')[0];
expect(listing).toBeTruthy();

let items = listing.getElementsByTagName('li');
counter = 0;
// Wait for the directory listing to be populated
while (items.length === 0 && counter < 100) {
await sleep(10);
items = listing.getElementsByTagName('li');
counter++;
}

// Fails if there is no items shown
expect(items.length).toBeGreaterThan(0);

// Emulate notebook file selection
const item = listing.querySelector('li[data-file-type="notebook"]')!;
simulate(item, 'mousedown');

await acceptDialog();
const result = await dialog;
const files = result.value!;
expect(files.length).toBe(1);
expect(files[0].type).toBe('notebook');
expect(files[0].name).toEqual(expect.stringMatching(/Untitled.*.ipynb/));

const fileDirectory = PathExt.dirname(files[0].path);
expect(fileDirectory).toEqual(testDirectory);

document.body.removeChild(node);
});
});

describe('FileDialog.getOpenFiles()', () => {
Expand Down Expand Up @@ -318,5 +374,53 @@ describe('@jupyterlab/filebrowser', () => {
expect(items[0].type).toBe('directory');
expect(items[0].path).toBe('');
});

it('should return one selected directory whose path matches default path', async () => {
const node = document.createElement('div');

document.body.appendChild(node);

const dialog = FileDialog.getExistingDirectory({
manager,
title: 'Select a folder',
host: node,
defaultPath: testDirectory
});

await waitForDialog();
await framePromise();

let counter = 0;
const listing = node.getElementsByClassName('jp-DirListing-content')[0];
expect(listing).toBeTruthy();

let items = listing.getElementsByTagName('li');
// Wait for the directory listing to be populated
while (items.length === 0 && counter < 100) {
await sleep(10);
items = listing.getElementsByTagName('li');
counter++;
}

// Fails if there is no items shown
expect(items.length).toBeGreaterThan(0);

// Emulate notebook file selection
simulate(items.item(items.length - 1)!, 'mousedown');

await acceptDialog();
const result = await dialog;
const files = result.value!;
expect(files.length).toBe(1);
expect(files[0].type).toBe('directory');
expect(files[0].name).toEqual(
expect.stringMatching(/Untitled Folder( \d+)?/)
);

const parentDirectory = PathExt.dirname(files[0].path);
expect(parentDirectory).toEqual(testDirectory);

document.body.removeChild(node);
});
});
});

0 comments on commit c6e7a14

Please sign in to comment.