From 2ad3fd08d764e373efbb4499306413799554cb4e Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Thu, 9 May 2019 16:15:25 +0200 Subject: [PATCH 1/9] open dialog first version --- packages/filebrowser/src/index.ts | 1 + packages/filebrowser/src/opendialog.ts | 231 ++++++++++++++++++ .../src/openfiledialog.spec.ts | 135 ++++++++++ 3 files changed, 367 insertions(+) create mode 100644 packages/filebrowser/src/opendialog.ts create mode 100644 tests/test-filebrowser/src/openfiledialog.spec.ts diff --git a/packages/filebrowser/src/index.ts b/packages/filebrowser/src/index.ts index 3f4642db94ee..4178b6dcdd01 100644 --- a/packages/filebrowser/src/index.ts +++ b/packages/filebrowser/src/index.ts @@ -6,5 +6,6 @@ export * from './crumbs'; export * from './tokens'; export * from './listing'; export * from './model'; +export * from './opendialog'; export * from './upload'; export * from './uploadstatus'; diff --git a/packages/filebrowser/src/opendialog.ts b/packages/filebrowser/src/opendialog.ts new file mode 100644 index 000000000000..3951fb7fa7e0 --- /dev/null +++ b/packages/filebrowser/src/opendialog.ts @@ -0,0 +1,231 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { filter, IIterator, toArray } from '@phosphor/algorithm'; +import { PanelLayout, Widget } from '@phosphor/widgets'; +import { PathExt } from '@jupyterlab/coreutils'; + +import { Dialog } from '@jupyterlab/apputils'; +import { IDocumentManager } from '@jupyterlab/docmanager'; +import { Contents } from '@jupyterlab/services'; + +import { FileBrowser } from './browser'; +import { FileBrowserModel } from './model'; +import { IFileBrowserFactory } from './factory'; + +/** + * The class name added to open file dialog + */ +const OPEN_DIALOG_CLASS = 'jp-Open-Dialog'; + +/** + * Create and show a open files dialog. + * + * Note: if nothing is selected when `getValue` will return the browser + * model current path. + * + * @param options - The dialog setup options. + * + * @returns A promise that resolves with whether the dialog was accepted. + */ +export function getOpenFiles( + options: OpenFileDialog.IOptions +): Promise>> { + let dialogOptions: Partial>> = { + title: options.title, + buttons: [ + Dialog.cancelButton(), + Dialog.okButton({ + label: 'Select' + }) + ], + focusNodeSelector: options.focusNodeSelector, + host: options.host, + renderer: options.renderer, + body: new OpenDialog(options.manager, options.filter) + }; + let dialog = new Dialog(dialogOptions); + return dialog.launch(); +} + +/** + * Create and show a open directory dialog. + * + * Note: if nothing is selected when `getValue` will return the browser + * model current path. + * + * @param options - The dialog setup options. + * + * @returns A promise that resolves with whether the dialog was accepted. + */ +export function getExistingDirectory( + options: OpenDirectoryDialog.IOptions +): Promise>> { + return getOpenFiles({ + ...options, + filter: model => false + }); +} + +/** + * File browser model with optional filter on element. + */ +class FilterFileBrowserModel extends FileBrowserModel { + constructor(options: FilterFileBrowserModel.IOptions) { + super(options); + + this._filter = options.filter ? options.filter : model => true; + } + + /** + * Create an iterator over the filtered model's items. + * + * @returns A new iterator over the model's items. + */ + items(): IIterator { + return filter(super.items(), (value, index) => { + if (value.type === 'directory') { + return true; + } else { + return this._filter(value); + } + }); + } + + private _filter: (value: Contents.IModel) => boolean; +} + +/** + * Open dialog widget + */ +class OpenDialog extends Widget + implements Dialog.IBodyWidget> { + constructor( + manager: IDocumentManager, + filter?: (value: Contents.IModel) => boolean + ) { + super(); + this.addClass(OPEN_DIALOG_CLASS); + + this._browser = Private.createFilteredFileBrowser( + 'filtered-file-browser-dialog', + manager, + filter + ); + + // Build the sub widgets + let layout = new PanelLayout(); + layout.addWidget(this._browser); + + // Set Widget content + this.layout = layout; + } + + /** + * Get the selected items. + */ + async getValue(): Promise { + const selection = toArray(this._browser.selectedItems()); + console.log(selection); + if (selection.length === 0) { + // Return current path + return [ + { + path: this._browser.model.path, + name: PathExt.basename(this._browser.model.path), + type: 'directory', + content: undefined, + writable: false, + created: 'unknown', + last_modified: 'unknown', + mimetype: 'text/plain', + format: 'text' + } + ]; + } else { + return selection; + } + } + + private _browser: FileBrowser; +} + +export namespace OpenDirectoryDialog { + export interface IOptions + extends Partial< + Pick< + Dialog.IOptions>, + Exclude< + keyof Dialog.IOptions>, + 'body' | 'buttons' | 'defaultButton' + > + > + > { + /** + * Document manager + */ + manager: IDocumentManager; + } +} + +export namespace OpenFileDialog { + export interface IOptions extends OpenDirectoryDialog.IOptions { + /** + * Filter function on file browser item model + */ + filter?: (value: Contents.IModel) => boolean; + } +} + +namespace FilterFileBrowserModel { + export interface IOptions extends FileBrowserModel.IOptions { + /** + * Filter function on file browser item model + */ + filter?: (value: Contents.IModel) => boolean; + } +} + +namespace Private { + /** + * Create a new file browser instance. + * + * @param id - The widget/DOM id of the file browser. + * + * @param manager - A document manager instance. + * + * @param filter - function to filter file browser item. + * + * @param options - The optional file browser configuration object. + * + * #### Notes + * The ID parameter is used to set the widget ID. It is also used as part of + * the unique key necessary to store the file browser's restoration data in + * the state database if that functionality is enabled. + * + * If, after the file browser has been generated by the factory, the ID of the + * resulting widget is changed by client code, the restoration functionality + * will not be disrupted as long as there are no ID collisions, i.e., as long + * as the initial ID passed into the factory is used for only one file browser + * instance. + */ + export const createFilteredFileBrowser = ( + id: string, + manager: IDocumentManager, + filter?: (value: Contents.IModel) => boolean, + options: IFileBrowserFactory.IOptions = {} + ) => { + const model = new FilterFileBrowserModel({ + manager, + filter, + driveName: options.driveName, + refreshInterval: options.refreshInterval + }); + const widget = new FileBrowser({ + id, + model + }); + + return widget; + }; +} diff --git a/tests/test-filebrowser/src/openfiledialog.spec.ts b/tests/test-filebrowser/src/openfiledialog.spec.ts new file mode 100644 index 000000000000..4cb4bb579ce1 --- /dev/null +++ b/tests/test-filebrowser/src/openfiledialog.spec.ts @@ -0,0 +1,135 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager'; +import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry'; +import { getExistingDirectory, getOpenFiles } from '@jupyterlab/filebrowser'; +import { ServiceManager } from '@jupyterlab/services'; +import { expect } from 'chai'; +import { acceptDialog, dismissDialog } from '@jupyterlab/testutils'; + +describe('@jupyterlab/filebrowser', () => { + let manager: IDocumentManager; + let serviceManager: ServiceManager.IManager; + let registry: DocumentRegistry; + + before(async () => { + const opener: DocumentManager.IWidgetOpener = { + open: widget => { + /* no op */ + } + }; + + registry = new DocumentRegistry({ + textModelFactory: new TextModelFactory() + }); + serviceManager = new ServiceManager({ standby: 'never' }); + manager = new DocumentManager({ + registry, + opener, + manager: serviceManager + }); + + // const contents = serviceManager.contents; + // await contents.newUntitled({ type: 'directory' }); + // await contents.newUntitled({ type: 'file' }); + // await contents.newUntitled({ type: 'notebook' }); + }); + + describe('getOpenFiles()', () => { + it('should create a dialog', async () => { + let dialog = getOpenFiles({ + manager + }); + + await dismissDialog(); + + const result = await dialog; + + expect(result.button.accept).false; + expect(await result.value).null; + }); + + it('should accept options', async () => { + const node = document.createElement('div'); + + document.body.appendChild(node); + + let dialog = getOpenFiles({ + manager, + title: 'Select a notebook', + host: node, + filter: value => value.type === 'notebook' + }); + + await acceptDialog(); + + const result = await dialog; + + expect(result.button.accept).true; + expect((await result.value).length).equals(1); + + document.body.removeChild(node); + }); + + // it('should filter files', async () => { + // expect('').true; + // }); + + // it('should return one selected file', async () => { + // expect('').true; + // }); + + // it('should return current path if nothing is selected', () => { + // expect('').true; + // }); + }); + + describe('getExistingDirectory()', () => { + it('should create a dialog', async () => { + let dialog = getExistingDirectory({ + manager + }); + + await dismissDialog(); + + const result = await dialog; + + expect(result.button.accept).to.false; + expect(await result.value).null; + }); + + it('should accept options', async () => { + const node = document.createElement('div'); + + document.body.appendChild(node); + + let dialog = getExistingDirectory({ + manager, + title: 'Select a folder', + host: node + }); + + await acceptDialog(); + + const result = await dialog; + + expect(result.button.accept).true; + expect((await result.value).length).equals(1); + + document.body.removeChild(node); + }); + + // it('should filter all files', async () => { + // expect('').true; + // }); + + // it('should return one selected directory', async () => { + // expect('').true; + // }); + + // it('should return current path if nothing is selected', () => { + // expect('').true; + // }); + }); +}); From 3ed51256a09e0287389acf6592de86d13f3765d3 Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Fri, 10 May 2019 16:37:35 +0200 Subject: [PATCH 2/9] Add some unit tests --- packages/filebrowser/src/opendialog.ts | 3 +- .../src/openfiledialog.spec.ts | 165 +++++++++++++++--- 2 files changed, 141 insertions(+), 27 deletions(-) diff --git a/packages/filebrowser/src/opendialog.ts b/packages/filebrowser/src/opendialog.ts index 3951fb7fa7e0..0dec236b9076 100644 --- a/packages/filebrowser/src/opendialog.ts +++ b/packages/filebrowser/src/opendialog.ts @@ -70,7 +70,7 @@ export function getExistingDirectory( /** * File browser model with optional filter on element. */ -class FilterFileBrowserModel extends FileBrowserModel { +export class FilterFileBrowserModel extends FileBrowserModel { constructor(options: FilterFileBrowserModel.IOptions) { super(options); @@ -126,7 +126,6 @@ class OpenDialog extends Widget */ async getValue(): Promise { const selection = toArray(this._browser.selectedItems()); - console.log(selection); if (selection.length === 0) { // Return current path return [ diff --git a/tests/test-filebrowser/src/openfiledialog.spec.ts b/tests/test-filebrowser/src/openfiledialog.spec.ts index 4cb4bb579ce1..94785fc5b8c9 100644 --- a/tests/test-filebrowser/src/openfiledialog.spec.ts +++ b/tests/test-filebrowser/src/openfiledialog.spec.ts @@ -1,12 +1,24 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. +import { toArray } from '@phosphor/algorithm'; + import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager'; import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry'; -import { getExistingDirectory, getOpenFiles } from '@jupyterlab/filebrowser'; +import { + getExistingDirectory, + getOpenFiles, + FilterFileBrowserModel +} from '@jupyterlab/filebrowser'; import { ServiceManager } from '@jupyterlab/services'; import { expect } from 'chai'; -import { acceptDialog, dismissDialog } from '@jupyterlab/testutils'; +import { + acceptDialog, + dismissDialog, + waitForDialog, + sleep +} from '@jupyterlab/testutils'; +import { simulate } from 'simulate-event'; describe('@jupyterlab/filebrowser', () => { let manager: IDocumentManager; @@ -30,10 +42,64 @@ describe('@jupyterlab/filebrowser', () => { manager: serviceManager }); - // const contents = serviceManager.contents; - // await contents.newUntitled({ type: 'directory' }); - // await contents.newUntitled({ type: 'file' }); - // await contents.newUntitled({ type: 'notebook' }); + const contents = serviceManager.contents; + await contents.newUntitled({ type: 'directory' }); + await contents.newUntitled({ type: 'file' }); + await contents.newUntitled({ type: 'notebook' }); + }); + + describe('FilterFileBrowserModel', () => { + describe('#constructor()', () => { + it('should construct a new filtered file browser model', () => { + let model = new FilterFileBrowserModel({ manager }); + expect(model).to.be.an.instanceof(FilterFileBrowserModel); + }); + + it('should accept filter option', () => { + let model = new FilterFileBrowserModel({ + manager, + filter: model => false + }); + expect(model).to.be.an.instanceof(FilterFileBrowserModel); + }); + }); + + describe('#items()', () => { + it('should list all elements if no filter is defined', async () => { + let model = new FilterFileBrowserModel({ manager }); + await model.cd(); + + const items = toArray(model.items()); + expect(items.length).equal(4); + }); + + it('should list all directory whatever the filter', async () => { + let model = new FilterFileBrowserModel({ + manager, + filter: model => false + }); + await model.cd(); + + const items = toArray(model.items()); + expect(items.length).equal(2); + expect(items[0].type).equal('directory'); + expect(items[1].type).equal('directory'); + }); + + it('should respect the filter', async () => { + let model = new FilterFileBrowserModel({ + manager, + filter: model => model.type === 'notebook' + }); + await model.cd(); + + const items = toArray(model.items()); + expect(items.length).equal(3); + expect(items[0].type).equal('directory'); + expect(items[1].type).equal('directory'); + expect(items[2].type).equal('notebook'); + }); + }); }); describe('getOpenFiles()', () => { @@ -67,22 +133,64 @@ describe('@jupyterlab/filebrowser', () => { const result = await dialog; expect(result.button.accept).true; - expect((await result.value).length).equals(1); + let items = await result.value; + expect(items.length).equal(1); document.body.removeChild(node); }); - // it('should filter files', async () => { - // expect('').true; - // }); + it('should return one selected file', async () => { + const node = document.createElement('div'); - // it('should return one selected file', async () => { - // expect('').true; - // }); + document.body.appendChild(node); - // it('should return current path if nothing is selected', () => { - // expect('').true; - // }); + let dialog = getOpenFiles({ + manager, + title: 'Select a notebook', + host: node, + filter: value => value.type === 'notebook' + }); + + await waitForDialog(); + + let counter = 0; + let listing = node.getElementsByClassName('jp-DirListing-content')[0]; + 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++; + } + + if (items.length > 0) { + // Emulate notebook selection + simulate(items.item(2), 'mousedown'); + } + + await acceptDialog(); + let result = await dialog; + let files = await result.value; + expect(files.length).equal(1); + expect(files[0].type).equal('notebook'); + + document.body.removeChild(node); + }); + + it('should return current path if nothing is selected', async () => { + let dialog = getOpenFiles({ + manager + }); + + await acceptDialog(); + + const result = await dialog; + const items = await result.value; + + expect(items.length).equal(1); + expect(items[0].type).equal('directory'); + expect(items[0].path).equal(''); + }); }); describe('getExistingDirectory()', () => { @@ -95,7 +203,7 @@ describe('@jupyterlab/filebrowser', () => { const result = await dialog; - expect(result.button.accept).to.false; + expect(result.button.accept).false; expect(await result.value).null; }); @@ -115,21 +223,28 @@ describe('@jupyterlab/filebrowser', () => { const result = await dialog; expect(result.button.accept).true; - expect((await result.value).length).equals(1); + expect((await result.value).length).equal(1); document.body.removeChild(node); }); - // it('should filter all files', async () => { - // expect('').true; - // }); - // it('should return one selected directory', async () => { // expect('').true; // }); - // it('should return current path if nothing is selected', () => { - // expect('').true; - // }); + it('should return current path if nothing is selected', async () => { + let dialog = getOpenFiles({ + manager + }); + + await acceptDialog(); + + const result = await dialog; + const items = await result.value; + + expect(items.length).equal(1); + expect(items[0].type).equal('directory'); + expect(items[0].path).equal(''); + }); }); }); From 6d23d4ede00b157be401f46d9c2fce0436a21b9f Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Thu, 16 May 2019 12:54:04 +0200 Subject: [PATCH 3/9] do not make `getValue` asynchronous --- packages/filebrowser/src/opendialog.ts | 10 +- .../src/openfiledialog.spec.ts | 96 +++++++++---------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/packages/filebrowser/src/opendialog.ts b/packages/filebrowser/src/opendialog.ts index 0dec236b9076..cf7d96c645c0 100644 --- a/packages/filebrowser/src/opendialog.ts +++ b/packages/filebrowser/src/opendialog.ts @@ -30,8 +30,8 @@ const OPEN_DIALOG_CLASS = 'jp-Open-Dialog'; */ export function getOpenFiles( options: OpenFileDialog.IOptions -): Promise>> { - let dialogOptions: Partial>> = { +): Promise> { + let dialogOptions: Partial> = { title: options.title, buttons: [ Dialog.cancelButton(), @@ -60,7 +60,7 @@ export function getOpenFiles( */ export function getExistingDirectory( options: OpenDirectoryDialog.IOptions -): Promise>> { +): Promise> { return getOpenFiles({ ...options, filter: model => false @@ -99,7 +99,7 @@ export class FilterFileBrowserModel extends FileBrowserModel { * Open dialog widget */ class OpenDialog extends Widget - implements Dialog.IBodyWidget> { + implements Dialog.IBodyWidget { constructor( manager: IDocumentManager, filter?: (value: Contents.IModel) => boolean @@ -124,7 +124,7 @@ class OpenDialog extends Widget /** * Get the selected items. */ - async getValue(): Promise { + getValue(): Contents.IModel[] { const selection = toArray(this._browser.selectedItems()); if (selection.length === 0) { // Return current path diff --git a/tests/test-filebrowser/src/openfiledialog.spec.ts b/tests/test-filebrowser/src/openfiledialog.spec.ts index 94785fc5b8c9..537c00735331 100644 --- a/tests/test-filebrowser/src/openfiledialog.spec.ts +++ b/tests/test-filebrowser/src/openfiledialog.spec.ts @@ -14,11 +14,11 @@ import { ServiceManager } from '@jupyterlab/services'; import { expect } from 'chai'; import { acceptDialog, - dismissDialog, - waitForDialog, - sleep + dismissDialog + // waitForDialog, + // sleep } from '@jupyterlab/testutils'; -import { simulate } from 'simulate-event'; +// import { simulate } from 'simulate-event'; describe('@jupyterlab/filebrowser', () => { let manager: IDocumentManager; @@ -73,7 +73,7 @@ describe('@jupyterlab/filebrowser', () => { expect(items.length).equal(4); }); - it('should list all directory whatever the filter', async () => { + it('should list all directories whatever the filter', async () => { let model = new FilterFileBrowserModel({ manager, filter: model => false @@ -113,7 +113,7 @@ describe('@jupyterlab/filebrowser', () => { const result = await dialog; expect(result.button.accept).false; - expect(await result.value).null; + expect(result.value).null; }); it('should accept options', async () => { @@ -133,49 +133,49 @@ describe('@jupyterlab/filebrowser', () => { const result = await dialog; expect(result.button.accept).true; - let items = await result.value; + let items = result.value; expect(items.length).equal(1); document.body.removeChild(node); }); - it('should return one selected file', async () => { - const node = document.createElement('div'); - - document.body.appendChild(node); - - let dialog = getOpenFiles({ - manager, - title: 'Select a notebook', - host: node, - filter: value => value.type === 'notebook' - }); - - await waitForDialog(); - - let counter = 0; - let listing = node.getElementsByClassName('jp-DirListing-content')[0]; - 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++; - } - - if (items.length > 0) { - // Emulate notebook selection - simulate(items.item(2), 'mousedown'); - } - - await acceptDialog(); - let result = await dialog; - let files = await result.value; - expect(files.length).equal(1); - expect(files[0].type).equal('notebook'); - - document.body.removeChild(node); - }); + // it('should return one selected file', async () => { + // const node = document.createElement('div'); + + // document.body.appendChild(node); + + // let dialog = getOpenFiles({ + // manager, + // title: 'Select a notebook', + // host: node, + // filter: value => value.type === 'notebook' + // }); + + // await waitForDialog(); + + // let counter = 0; + // let listing = node.getElementsByClassName('jp-DirListing-content')[0]; + // 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++; + // } + + // if (items.length > 0) { + // // Emulate notebook selection + // simulate(items.item(2), 'mousedown'); + // } + + // await acceptDialog(); + // let result = await dialog; + // let files = result.value; + // expect(files.length).equal(1); + // expect(files[0].type).equal('notebook'); + + // document.body.removeChild(node); + // }); it('should return current path if nothing is selected', async () => { let dialog = getOpenFiles({ @@ -185,7 +185,7 @@ describe('@jupyterlab/filebrowser', () => { await acceptDialog(); const result = await dialog; - const items = await result.value; + const items = result.value; expect(items.length).equal(1); expect(items[0].type).equal('directory'); @@ -204,7 +204,7 @@ describe('@jupyterlab/filebrowser', () => { const result = await dialog; expect(result.button.accept).false; - expect(await result.value).null; + expect(result.value).null; }); it('should accept options', async () => { @@ -223,7 +223,7 @@ describe('@jupyterlab/filebrowser', () => { const result = await dialog; expect(result.button.accept).true; - expect((await result.value).length).equal(1); + expect(result.value.length).equal(1); document.body.removeChild(node); }); @@ -240,7 +240,7 @@ describe('@jupyterlab/filebrowser', () => { await acceptDialog(); const result = await dialog; - const items = await result.value; + const items = result.value; expect(items.length).equal(1); expect(items[0].type).equal('directory'); From 81e508878fa711853702b5b569598fb115995f24 Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Thu, 23 May 2019 14:40:45 +0200 Subject: [PATCH 4/9] Figure out how to emulate file selection --- .../src/openfiledialog.spec.ts | 123 ++++++++++++------ 1 file changed, 85 insertions(+), 38 deletions(-) diff --git a/tests/test-filebrowser/src/openfiledialog.spec.ts b/tests/test-filebrowser/src/openfiledialog.spec.ts index 537c00735331..ee1caa43b8e4 100644 --- a/tests/test-filebrowser/src/openfiledialog.spec.ts +++ b/tests/test-filebrowser/src/openfiledialog.spec.ts @@ -14,11 +14,11 @@ import { ServiceManager } from '@jupyterlab/services'; import { expect } from 'chai'; import { acceptDialog, - dismissDialog - // waitForDialog, - // sleep + dismissDialog, + waitForDialog, + sleep } from '@jupyterlab/testutils'; -// import { simulate } from 'simulate-event'; +import { simulate } from 'simulate-event'; describe('@jupyterlab/filebrowser', () => { let manager: IDocumentManager; @@ -139,43 +139,50 @@ describe('@jupyterlab/filebrowser', () => { document.body.removeChild(node); }); - // it('should return one selected file', async () => { - // const node = document.createElement('div'); + it('should return one selected file', async () => { + const node = document.createElement('div'); - // document.body.appendChild(node); + document.body.appendChild(node); - // let dialog = getOpenFiles({ - // manager, - // title: 'Select a notebook', - // host: node, - // filter: value => value.type === 'notebook' - // }); + let dialog = getOpenFiles({ + manager, + title: 'Select a notebook', + host: node, + filter: value => value.type === 'notebook' + }); - // await waitForDialog(); + await waitForDialog(); - // let counter = 0; - // let listing = node.getElementsByClassName('jp-DirListing-content')[0]; - // 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++; - // } + let counter = 0; + let listing = node.getElementsByClassName('jp-DirListing-content')[0]; + 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++; + } - // if (items.length > 0) { - // // Emulate notebook selection - // simulate(items.item(2), 'mousedown'); - // } + if (items.length > 0) { + // Emulate notebook file selection + // Get node coordinates we need to be precised as code test for hit position + const rect = items.item(2).getBoundingClientRect(); - // await acceptDialog(); - // let result = await dialog; - // let files = result.value; - // expect(files.length).equal(1); - // expect(files[0].type).equal('notebook'); + simulate(items.item(2), 'mousedown', { + clientX: 0.5 * (rect.left + rect.right), + clientY: 0.5 * (rect.bottom + rect.top) + }); + } + + await acceptDialog(); + let result = await dialog; + let files = result.value; + expect(files.length).equal(1); + expect(files[0].type).equal('notebook'); + expect(files[0].name).equal('Untitled.ipynb'); - // document.body.removeChild(node); - // }); + document.body.removeChild(node); + }); it('should return current path if nothing is selected', async () => { let dialog = getOpenFiles({ @@ -228,12 +235,52 @@ describe('@jupyterlab/filebrowser', () => { document.body.removeChild(node); }); - // it('should return one selected directory', async () => { - // expect('').true; - // }); + it('should return one selected directory', async () => { + const node = document.createElement('div'); + + document.body.appendChild(node); + + let dialog = getExistingDirectory({ + manager, + title: 'Select a folder', + host: node + }); + + await waitForDialog(); + + let counter = 0; + let listing = node.getElementsByClassName('jp-DirListing-content')[0]; + 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++; + } + + if (items.length > 0) { + // Emulate notebook file selection + // Get node coordinates we need to be precised as code test for hit position + const rect = items.item(1).getBoundingClientRect(); + + simulate(items.item(1), 'mousedown', { + clientX: 0.5 * (rect.left + rect.right), + clientY: 0.5 * (rect.bottom + rect.top) + }); + } + + await acceptDialog(); + let result = await dialog; + let files = result.value; + expect(files.length).equal(1); + expect(files[0].type).equal('directory'); + expect(files[0].name).equal('Untitled Folder'); + + document.body.removeChild(node); + }); it('should return current path if nothing is selected', async () => { - let dialog = getOpenFiles({ + let dialog = getExistingDirectory({ manager }); From a4543e553e0f5ec19df526e143d11c701d75e845 Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Thu, 23 May 2019 15:26:37 +0200 Subject: [PATCH 5/9] Improve API --- packages/filebrowser/src/opendialog.ts | 167 ++++++++++-------- .../src/openfiledialog.spec.ts | 26 ++- 2 files changed, 101 insertions(+), 92 deletions(-) diff --git a/packages/filebrowser/src/opendialog.ts b/packages/filebrowser/src/opendialog.ts index cf7d96c645c0..40844e85c589 100644 --- a/packages/filebrowser/src/opendialog.ts +++ b/packages/filebrowser/src/opendialog.ts @@ -19,52 +19,101 @@ import { IFileBrowserFactory } from './factory'; const OPEN_DIALOG_CLASS = 'jp-Open-Dialog'; /** - * Create and show a open files dialog. - * - * Note: if nothing is selected when `getValue` will return the browser - * model current path. - * - * @param options - The dialog setup options. - * - * @returns A promise that resolves with whether the dialog was accepted. + * Namespace for file dialog */ -export function getOpenFiles( - options: OpenFileDialog.IOptions -): Promise> { - let dialogOptions: Partial> = { - title: options.title, - buttons: [ - Dialog.cancelButton(), - Dialog.okButton({ - label: 'Select' - }) - ], - focusNodeSelector: options.focusNodeSelector, - host: options.host, - renderer: options.renderer, - body: new OpenDialog(options.manager, options.filter) - }; - let dialog = new Dialog(dialogOptions); - return dialog.launch(); +export namespace FileDialog { + /** + * Options for the open directory dialog + */ + export interface IDirectoryOptions + extends Partial< + Pick< + Dialog.IOptions>, + Exclude< + keyof Dialog.IOptions>, + 'body' | 'buttons' | 'defaultButton' + > + > + > { + /** + * Document manager + */ + manager: IDocumentManager; + } + + /** + * Options for the open file dialog + */ + export interface IFileOptions extends IDirectoryOptions { + /** + * Filter function on file browser item model + */ + filter?: (value: Contents.IModel) => boolean; + } + + /** + * Create and show a open files dialog. + * + * Note: if nothing is selected when `getValue` will return the browser + * model current path. + * + * @param options - The dialog setup options. + * + * @returns A promise that resolves with whether the dialog was accepted. + */ + export function getOpenFiles( + options: IFileOptions + ): Promise> { + let dialogOptions: Partial> = { + title: options.title, + buttons: [ + Dialog.cancelButton(), + Dialog.okButton({ + label: 'Select' + }) + ], + focusNodeSelector: options.focusNodeSelector, + host: options.host, + renderer: options.renderer, + body: new OpenDialog(options.manager, options.filter) + }; + let dialog = new Dialog(dialogOptions); + return dialog.launch(); + } + + /** + * Create and show a open directory dialog. + * + * Note: if nothing is selected when `getValue` will return the browser + * model current path. + * + * @param options - The dialog setup options. + * + * @returns A promise that resolves with whether the dialog was accepted. + */ + export function getExistingDirectory( + options: IDirectoryOptions + ): Promise> { + return getOpenFiles({ + ...options, + filter: model => false + }); + } } /** - * Create and show a open directory dialog. - * - * Note: if nothing is selected when `getValue` will return the browser - * model current path. - * - * @param options - The dialog setup options. - * - * @returns A promise that resolves with whether the dialog was accepted. + * Namespace for the filtered file browser model */ -export function getExistingDirectory( - options: OpenDirectoryDialog.IOptions -): Promise> { - return getOpenFiles({ - ...options, - filter: model => false - }); +export namespace FilterFileBrowserModel { + /** + * Constructor options + */ + export interface IOptions extends FileBrowserModel.IOptions { + /** + * Filter function on file browser item model + */ + filter?: (value: Contents.IModel) => boolean; + } } /** @@ -149,42 +198,6 @@ class OpenDialog extends Widget private _browser: FileBrowser; } -export namespace OpenDirectoryDialog { - export interface IOptions - extends Partial< - Pick< - Dialog.IOptions>, - Exclude< - keyof Dialog.IOptions>, - 'body' | 'buttons' | 'defaultButton' - > - > - > { - /** - * Document manager - */ - manager: IDocumentManager; - } -} - -export namespace OpenFileDialog { - export interface IOptions extends OpenDirectoryDialog.IOptions { - /** - * Filter function on file browser item model - */ - filter?: (value: Contents.IModel) => boolean; - } -} - -namespace FilterFileBrowserModel { - export interface IOptions extends FileBrowserModel.IOptions { - /** - * Filter function on file browser item model - */ - filter?: (value: Contents.IModel) => boolean; - } -} - namespace Private { /** * Create a new file browser instance. diff --git a/tests/test-filebrowser/src/openfiledialog.spec.ts b/tests/test-filebrowser/src/openfiledialog.spec.ts index ee1caa43b8e4..af4bf905303b 100644 --- a/tests/test-filebrowser/src/openfiledialog.spec.ts +++ b/tests/test-filebrowser/src/openfiledialog.spec.ts @@ -5,11 +5,7 @@ import { toArray } from '@phosphor/algorithm'; import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager'; import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry'; -import { - getExistingDirectory, - getOpenFiles, - FilterFileBrowserModel -} from '@jupyterlab/filebrowser'; +import { FileDialog, FilterFileBrowserModel } from '@jupyterlab/filebrowser'; import { ServiceManager } from '@jupyterlab/services'; import { expect } from 'chai'; import { @@ -102,9 +98,9 @@ describe('@jupyterlab/filebrowser', () => { }); }); - describe('getOpenFiles()', () => { + describe('FileDialog.getOpenFiles()', () => { it('should create a dialog', async () => { - let dialog = getOpenFiles({ + let dialog = FileDialog.getOpenFiles({ manager }); @@ -121,7 +117,7 @@ describe('@jupyterlab/filebrowser', () => { document.body.appendChild(node); - let dialog = getOpenFiles({ + let dialog = FileDialog.getOpenFiles({ manager, title: 'Select a notebook', host: node, @@ -144,7 +140,7 @@ describe('@jupyterlab/filebrowser', () => { document.body.appendChild(node); - let dialog = getOpenFiles({ + let dialog = FileDialog.getOpenFiles({ manager, title: 'Select a notebook', host: node, @@ -185,7 +181,7 @@ describe('@jupyterlab/filebrowser', () => { }); it('should return current path if nothing is selected', async () => { - let dialog = getOpenFiles({ + let dialog = FileDialog.getOpenFiles({ manager }); @@ -200,9 +196,9 @@ describe('@jupyterlab/filebrowser', () => { }); }); - describe('getExistingDirectory()', () => { + describe('FileDialog.getExistingDirectory()', () => { it('should create a dialog', async () => { - let dialog = getExistingDirectory({ + let dialog = FileDialog.getExistingDirectory({ manager }); @@ -219,7 +215,7 @@ describe('@jupyterlab/filebrowser', () => { document.body.appendChild(node); - let dialog = getExistingDirectory({ + let dialog = FileDialog.getExistingDirectory({ manager, title: 'Select a folder', host: node @@ -240,7 +236,7 @@ describe('@jupyterlab/filebrowser', () => { document.body.appendChild(node); - let dialog = getExistingDirectory({ + let dialog = FileDialog.getExistingDirectory({ manager, title: 'Select a folder', host: node @@ -280,7 +276,7 @@ describe('@jupyterlab/filebrowser', () => { }); it('should return current path if nothing is selected', async () => { - let dialog = getExistingDirectory({ + let dialog = FileDialog.getExistingDirectory({ manager }); From 59ef0601525169717177c9d8bd254c7b84ef76a3 Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Thu, 23 May 2019 15:26:58 +0200 Subject: [PATCH 6/9] Add documentation --- docs/source/developer/ui_helpers.rst | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/source/developer/ui_helpers.rst b/docs/source/developer/ui_helpers.rst index 008353033217..25970fbe103b 100644 --- a/docs/source/developer/ui_helpers.rst +++ b/docs/source/developer/ui_helpers.rst @@ -65,3 +65,42 @@ a ``Promise`` resolving in a ``Dialog.IResult`` object. InputDialog.getText({ title: 'Provide a text' }).then(value => { console.log('text ' + value.value); }); + +File Dialogs +'''''''''''' + +Two helper functions to ask a user to open a file or a directory are +available in the ``filebrowser`` package under the namespace ``FileDialog``. + +Here is an example to request a file. + +.. code:: typescript + + const dialog = FileDialog.getExistingDirectory({ + manager, // IDocumentManager + filter: model => model.type == 'notebook' // optional (model: Contents.IModel) => boolean + }); + + const result = await dialog; + + if(result.button.accept){ + let files = result.value; + } + +And for a folder. + +.. code:: typescript + + const dialog = FileDialog.getExistingDirectory({ + manager // IDocumentManager + }); + + const result = await dialog; + + if(result.button.accept){ + let folders = result.value; + } + +.. note:: The document manager can be obtained in a plugin by requesting + ``IFileBrowserFactory`` service. The manager will be accessed through + ``factory.defaultBrowser.model.manager``. From 9cf7caa593e41993f0b4526d6708eb389df431ec Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Tue, 11 Jun 2019 12:52:40 +0200 Subject: [PATCH 7/9] Move `FilterFileBrowserModel` to model.ts --- packages/filebrowser/src/model.ts | 46 +++++++++++++++++++++++- packages/filebrowser/src/opendialog.ts | 49 ++------------------------ 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/packages/filebrowser/src/model.ts b/packages/filebrowser/src/model.ts index b8253b271205..78fd2621bf63 100644 --- a/packages/filebrowser/src/model.ts +++ b/packages/filebrowser/src/model.ts @@ -23,7 +23,8 @@ import { find, IIterator, IterableOrArrayLike, - ArrayExt + ArrayExt, + filter } from '@phosphor/algorithm'; import { PromiseDelegate, ReadonlyJSONObject } from '@phosphor/coreutils'; @@ -672,6 +673,49 @@ export namespace FileBrowserModel { } } +/** + * File browser model with optional filter on element. + */ +export class FilterFileBrowserModel extends FileBrowserModel { + constructor(options: FilterFileBrowserModel.IOptions) { + super(options); + + this._filter = options.filter ? options.filter : model => true; + } + + /** + * Create an iterator over the filtered model's items. + * + * @returns A new iterator over the model's items. + */ + items(): IIterator { + return filter(super.items(), (value, index) => { + if (value.type === 'directory') { + return true; + } else { + return this._filter(value); + } + }); + } + + private _filter: (value: Contents.IModel) => boolean; +} + +/** + * Namespace for the filtered file browser model + */ +export namespace FilterFileBrowserModel { + /** + * Constructor options + */ + export interface IOptions extends FileBrowserModel.IOptions { + /** + * Filter function on file browser item model + */ + filter?: (value: Contents.IModel) => boolean; + } +} + /** * The namespace for the file browser model private data. */ diff --git a/packages/filebrowser/src/opendialog.ts b/packages/filebrowser/src/opendialog.ts index 40844e85c589..13c0135a6b6a 100644 --- a/packages/filebrowser/src/opendialog.ts +++ b/packages/filebrowser/src/opendialog.ts @@ -1,7 +1,7 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { filter, IIterator, toArray } from '@phosphor/algorithm'; +import { toArray } from '@phosphor/algorithm'; import { PanelLayout, Widget } from '@phosphor/widgets'; import { PathExt } from '@jupyterlab/coreutils'; @@ -10,8 +10,8 @@ import { IDocumentManager } from '@jupyterlab/docmanager'; import { Contents } from '@jupyterlab/services'; import { FileBrowser } from './browser'; -import { FileBrowserModel } from './model'; -import { IFileBrowserFactory } from './factory'; +import { FilterFileBrowserModel } from './model'; +import { IFileBrowserFactory } from './tokens'; /** * The class name added to open file dialog @@ -101,49 +101,6 @@ export namespace FileDialog { } } -/** - * Namespace for the filtered file browser model - */ -export namespace FilterFileBrowserModel { - /** - * Constructor options - */ - export interface IOptions extends FileBrowserModel.IOptions { - /** - * Filter function on file browser item model - */ - filter?: (value: Contents.IModel) => boolean; - } -} - -/** - * File browser model with optional filter on element. - */ -export class FilterFileBrowserModel extends FileBrowserModel { - constructor(options: FilterFileBrowserModel.IOptions) { - super(options); - - this._filter = options.filter ? options.filter : model => true; - } - - /** - * Create an iterator over the filtered model's items. - * - * @returns A new iterator over the model's items. - */ - items(): IIterator { - return filter(super.items(), (value, index) => { - if (value.type === 'directory') { - return true; - } else { - return this._filter(value); - } - }); - } - - private _filter: (value: Contents.IModel) => boolean; -} - /** * Open dialog widget */ From 328aa91ec454b9a6f8c9969563f9ba3b2d898e0f Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Tue, 11 Jun 2019 12:53:27 +0200 Subject: [PATCH 8/9] Improve unit test design --- .../src/openfiledialog.spec.ts | 92 ++++++++++++------- 1 file changed, 59 insertions(+), 33 deletions(-) diff --git a/tests/test-filebrowser/src/openfiledialog.spec.ts b/tests/test-filebrowser/src/openfiledialog.spec.ts index af4bf905303b..17edf3baa544 100644 --- a/tests/test-filebrowser/src/openfiledialog.spec.ts +++ b/tests/test-filebrowser/src/openfiledialog.spec.ts @@ -5,14 +5,19 @@ import { toArray } from '@phosphor/algorithm'; import { DocumentManager, IDocumentManager } from '@jupyterlab/docmanager'; import { DocumentRegistry, TextModelFactory } from '@jupyterlab/docregistry'; -import { FileDialog, FilterFileBrowserModel } from '@jupyterlab/filebrowser'; +import { + FileDialog, + FilterFileBrowserModel, + FileBrowserModel +} from '@jupyterlab/filebrowser'; import { ServiceManager } from '@jupyterlab/services'; import { expect } from 'chai'; import { acceptDialog, dismissDialog, waitForDialog, - sleep + sleep, + framePromise } from '@jupyterlab/testutils'; import { simulate } from 'simulate-event'; @@ -62,38 +67,50 @@ describe('@jupyterlab/filebrowser', () => { describe('#items()', () => { it('should list all elements if no filter is defined', async () => { - let model = new FilterFileBrowserModel({ manager }); + let filteredModel = new FilterFileBrowserModel({ manager }); + await filteredModel.cd(); + let model = new FileBrowserModel({ manager }); await model.cd(); + const filteredItems = toArray(filteredModel.items()); const items = toArray(model.items()); - expect(items.length).equal(4); + expect(filteredItems.length).equal(items.length); }); it('should list all directories whatever the filter', async () => { - let model = new FilterFileBrowserModel({ + let filteredModel = new FilterFileBrowserModel({ manager, filter: model => false }); + await filteredModel.cd(); + let model = new FileBrowserModel({ manager }); await model.cd(); + const filteredItems = toArray(filteredModel.items()); const items = toArray(model.items()); - expect(items.length).equal(2); - expect(items[0].type).equal('directory'); - expect(items[1].type).equal('directory'); + const folders = items.filter(item => item.type === 'directory'); + expect(filteredItems.length).equal(folders.length); }); it('should respect the filter', async () => { - let model = new FilterFileBrowserModel({ + let filteredModel = new FilterFileBrowserModel({ manager, filter: model => model.type === 'notebook' }); + await filteredModel.cd(); + let model = new FileBrowserModel({ manager }); await model.cd(); + const filteredItems = toArray(filteredModel.items()); const items = toArray(model.items()); - expect(items.length).equal(3); - expect(items[0].type).equal('directory'); - expect(items[1].type).equal('directory'); - expect(items[2].type).equal('notebook'); + const shownItems = items.filter( + item => item.type === 'directory' || item.type === 'notebook' + ); + expect(filteredItems.length).equal(shownItems.length); + const notebooks = filteredItems.filter( + item => item.type === 'notebook' + ); + expect(notebooks.length).to.be.greaterThan(0); }); }); }); @@ -148,10 +165,14 @@ describe('@jupyterlab/filebrowser', () => { }); await waitForDialog(); + await framePromise(); let counter = 0; let listing = node.getElementsByClassName('jp-DirListing-content')[0]; + expect(listing).to.be.ok; + let items = listing.getElementsByTagName('li'); + counter = 0; // Wait for the directory listing to be populated while (items.length === 0 && counter < 100) { await sleep(10); @@ -159,23 +180,24 @@ describe('@jupyterlab/filebrowser', () => { counter++; } - if (items.length > 0) { - // Emulate notebook file selection - // Get node coordinates we need to be precised as code test for hit position - const rect = items.item(2).getBoundingClientRect(); + // Fails if there is no items shown + expect(items.length).to.be.greaterThan(0); - simulate(items.item(2), 'mousedown', { - clientX: 0.5 * (rect.left + rect.right), - clientY: 0.5 * (rect.bottom + rect.top) - }); - } + // Emulate notebook file selection + // Get node coordinates we need to be precised as code test for hit position + const rect = items.item(items.length - 1).getBoundingClientRect(); + + simulate(items.item(items.length - 1), 'mousedown', { + clientX: 0.5 * (rect.left + rect.right), + clientY: 0.5 * (rect.bottom + rect.top) + }); await acceptDialog(); let result = await dialog; let files = result.value; expect(files.length).equal(1); expect(files[0].type).equal('notebook'); - expect(files[0].name).equal('Untitled.ipynb'); + expect(files[0].name).matches(/Untitled\d*.ipynb/); document.body.removeChild(node); }); @@ -243,9 +265,12 @@ describe('@jupyterlab/filebrowser', () => { }); await waitForDialog(); + await framePromise(); let counter = 0; let listing = node.getElementsByClassName('jp-DirListing-content')[0]; + expect(listing).to.be.ok; + let items = listing.getElementsByTagName('li'); // Wait for the directory listing to be populated while (items.length === 0 && counter < 100) { @@ -254,23 +279,24 @@ describe('@jupyterlab/filebrowser', () => { counter++; } - if (items.length > 0) { - // Emulate notebook file selection - // Get node coordinates we need to be precised as code test for hit position - const rect = items.item(1).getBoundingClientRect(); + // Fails if there is no items shown + expect(items.length).to.be.greaterThan(0); - simulate(items.item(1), 'mousedown', { - clientX: 0.5 * (rect.left + rect.right), - clientY: 0.5 * (rect.bottom + rect.top) - }); - } + // Emulate notebook file selection + // Get node coordinates we need to be precised as code test for hit position + const rect = items.item(items.length - 1).getBoundingClientRect(); + + simulate(items.item(items.length - 1), 'mousedown', { + clientX: 0.5 * (rect.left + rect.right), + clientY: 0.5 * (rect.bottom + rect.top) + }); await acceptDialog(); let result = await dialog; let files = result.value; expect(files.length).equal(1); expect(files[0].type).equal('directory'); - expect(files[0].name).equal('Untitled Folder'); + expect(files[0].name).matches(/Untitled Folder( \d+)?/); document.body.removeChild(node); }); From 6b69db33e9f19442e820c182a60f7b0b20308882 Mon Sep 17 00:00:00 2001 From: Frederic Collonval Date: Thu, 22 Aug 2019 20:54:11 +0200 Subject: [PATCH 9/9] Fix following rebase --- docs/source/developer/ui_helpers.rst | 9 ++-- packages/filebrowser/src/opendialog.ts | 18 +++++++- .../src/openfiledialog.spec.ts | 45 ++++++++++++++----- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/docs/source/developer/ui_helpers.rst b/docs/source/developer/ui_helpers.rst index 25970fbe103b..5b6b9e563a0b 100644 --- a/docs/source/developer/ui_helpers.rst +++ b/docs/source/developer/ui_helpers.rst @@ -77,6 +77,7 @@ Here is an example to request a file. .. code:: typescript const dialog = FileDialog.getExistingDirectory({ + iconRegistry, // IIconRegistry manager, // IDocumentManager filter: model => model.type == 'notebook' // optional (model: Contents.IModel) => boolean }); @@ -92,6 +93,7 @@ And for a folder. .. code:: typescript const dialog = FileDialog.getExistingDirectory({ + iconRegistry, // IIconRegistry manager // IDocumentManager }); @@ -101,6 +103,7 @@ And for a folder. let folders = result.value; } -.. note:: The document manager can be obtained in a plugin by requesting - ``IFileBrowserFactory`` service. The manager will be accessed through - ``factory.defaultBrowser.model.manager``. +.. note:: The document manager and the icon registry can be obtained in a plugin by + requesting ``IFileBrowserFactory`` token. The ``manager`` will be accessed through + ``factory.defaultBrowser.model.manager`` and the ``iconRegistry`` through + ``factory.defaultBrowser.model.iconRegistry``. diff --git a/packages/filebrowser/src/opendialog.ts b/packages/filebrowser/src/opendialog.ts index 13c0135a6b6a..3c777fc3445b 100644 --- a/packages/filebrowser/src/opendialog.ts +++ b/packages/filebrowser/src/opendialog.ts @@ -8,6 +8,7 @@ import { PathExt } from '@jupyterlab/coreutils'; import { Dialog } from '@jupyterlab/apputils'; import { IDocumentManager } from '@jupyterlab/docmanager'; import { Contents } from '@jupyterlab/services'; +import { IIconRegistry } from '@jupyterlab/ui-components'; import { FileBrowser } from './browser'; import { FilterFileBrowserModel } from './model'; @@ -35,6 +36,11 @@ export namespace FileDialog { > > > { + /** + * An icon registry instance. + */ + iconRegistry: IIconRegistry; + /** * Document manager */ @@ -75,7 +81,11 @@ export namespace FileDialog { focusNodeSelector: options.focusNodeSelector, host: options.host, renderer: options.renderer, - body: new OpenDialog(options.manager, options.filter) + body: new OpenDialog( + options.iconRegistry, + options.manager, + options.filter + ) }; let dialog = new Dialog(dialogOptions); return dialog.launch(); @@ -107,6 +117,7 @@ export namespace FileDialog { class OpenDialog extends Widget implements Dialog.IBodyWidget { constructor( + iconRegistry: IIconRegistry, manager: IDocumentManager, filter?: (value: Contents.IModel) => boolean ) { @@ -115,6 +126,7 @@ class OpenDialog extends Widget this._browser = Private.createFilteredFileBrowser( 'filtered-file-browser-dialog', + iconRegistry, manager, filter ); @@ -161,6 +173,8 @@ namespace Private { * * @param id - The widget/DOM id of the file browser. * + * @param iconRegistry - An icon registry instance. + * * @param manager - A document manager instance. * * @param filter - function to filter file browser item. @@ -180,11 +194,13 @@ namespace Private { */ export const createFilteredFileBrowser = ( id: string, + iconRegistry: IIconRegistry, manager: IDocumentManager, filter?: (value: Contents.IModel) => boolean, options: IFileBrowserFactory.IOptions = {} ) => { const model = new FilterFileBrowserModel({ + iconRegistry, manager, filter, driveName: options.driveName, diff --git a/tests/test-filebrowser/src/openfiledialog.spec.ts b/tests/test-filebrowser/src/openfiledialog.spec.ts index 17edf3baa544..0e53d212c8dd 100644 --- a/tests/test-filebrowser/src/openfiledialog.spec.ts +++ b/tests/test-filebrowser/src/openfiledialog.spec.ts @@ -10,7 +10,10 @@ import { FilterFileBrowserModel, FileBrowserModel } from '@jupyterlab/filebrowser'; -import { ServiceManager } from '@jupyterlab/services'; +import { ServiceManager, Contents } from '@jupyterlab/services'; + +import { defaultIconRegistry, IIconRegistry } from '@jupyterlab/ui-components'; + import { expect } from 'chai'; import { acceptDialog, @@ -22,6 +25,7 @@ import { import { simulate } from 'simulate-event'; describe('@jupyterlab/filebrowser', () => { + let iconRegistry: IIconRegistry; let manager: IDocumentManager; let serviceManager: ServiceManager.IManager; let registry: DocumentRegistry; @@ -37,6 +41,7 @@ describe('@jupyterlab/filebrowser', () => { textModelFactory: new TextModelFactory() }); serviceManager = new ServiceManager({ standby: 'never' }); + iconRegistry = defaultIconRegistry; manager = new DocumentManager({ registry, opener, @@ -52,14 +57,15 @@ describe('@jupyterlab/filebrowser', () => { describe('FilterFileBrowserModel', () => { describe('#constructor()', () => { it('should construct a new filtered file browser model', () => { - let model = new FilterFileBrowserModel({ manager }); + let model = new FilterFileBrowserModel({ iconRegistry, manager }); expect(model).to.be.an.instanceof(FilterFileBrowserModel); }); it('should accept filter option', () => { let model = new FilterFileBrowserModel({ + iconRegistry, manager, - filter: model => false + filter: (model: Contents.IModel) => false }); expect(model).to.be.an.instanceof(FilterFileBrowserModel); }); @@ -67,9 +73,12 @@ describe('@jupyterlab/filebrowser', () => { describe('#items()', () => { it('should list all elements if no filter is defined', async () => { - let filteredModel = new FilterFileBrowserModel({ manager }); + let filteredModel = new FilterFileBrowserModel({ + iconRegistry, + manager + }); await filteredModel.cd(); - let model = new FileBrowserModel({ manager }); + let model = new FileBrowserModel({ iconRegistry, manager }); await model.cd(); const filteredItems = toArray(filteredModel.items()); @@ -79,11 +88,12 @@ describe('@jupyterlab/filebrowser', () => { it('should list all directories whatever the filter', async () => { let filteredModel = new FilterFileBrowserModel({ + iconRegistry, manager, - filter: model => false + filter: (model: Contents.IModel) => false }); await filteredModel.cd(); - let model = new FileBrowserModel({ manager }); + let model = new FileBrowserModel({ iconRegistry, manager }); await model.cd(); const filteredItems = toArray(filteredModel.items()); @@ -94,14 +104,17 @@ describe('@jupyterlab/filebrowser', () => { it('should respect the filter', async () => { let filteredModel = new FilterFileBrowserModel({ + iconRegistry, manager, - filter: model => model.type === 'notebook' + filter: (model: Contents.IModel) => model.type === 'notebook' }); await filteredModel.cd(); - let model = new FileBrowserModel({ manager }); + let model = new FileBrowserModel({ iconRegistry, manager }); await model.cd(); - const filteredItems = toArray(filteredModel.items()); + const filteredItems = toArray( + filteredModel.items() + ) as Contents.IModel[]; const items = toArray(model.items()); const shownItems = items.filter( item => item.type === 'directory' || item.type === 'notebook' @@ -118,6 +131,7 @@ describe('@jupyterlab/filebrowser', () => { describe('FileDialog.getOpenFiles()', () => { it('should create a dialog', async () => { let dialog = FileDialog.getOpenFiles({ + iconRegistry, manager }); @@ -135,10 +149,11 @@ describe('@jupyterlab/filebrowser', () => { document.body.appendChild(node); let dialog = FileDialog.getOpenFiles({ + iconRegistry, manager, title: 'Select a notebook', host: node, - filter: value => value.type === 'notebook' + filter: (value: Contents.IModel) => value.type === 'notebook' }); await acceptDialog(); @@ -158,10 +173,11 @@ describe('@jupyterlab/filebrowser', () => { document.body.appendChild(node); let dialog = FileDialog.getOpenFiles({ + iconRegistry, manager, title: 'Select a notebook', host: node, - filter: value => value.type === 'notebook' + filter: (value: Contents.IModel) => value.type === 'notebook' }); await waitForDialog(); @@ -204,6 +220,7 @@ describe('@jupyterlab/filebrowser', () => { it('should return current path if nothing is selected', async () => { let dialog = FileDialog.getOpenFiles({ + iconRegistry, manager }); @@ -221,6 +238,7 @@ describe('@jupyterlab/filebrowser', () => { describe('FileDialog.getExistingDirectory()', () => { it('should create a dialog', async () => { let dialog = FileDialog.getExistingDirectory({ + iconRegistry, manager }); @@ -238,6 +256,7 @@ describe('@jupyterlab/filebrowser', () => { document.body.appendChild(node); let dialog = FileDialog.getExistingDirectory({ + iconRegistry, manager, title: 'Select a folder', host: node @@ -259,6 +278,7 @@ describe('@jupyterlab/filebrowser', () => { document.body.appendChild(node); let dialog = FileDialog.getExistingDirectory({ + iconRegistry, manager, title: 'Select a folder', host: node @@ -303,6 +323,7 @@ describe('@jupyterlab/filebrowser', () => { it('should return current path if nothing is selected', async () => { let dialog = FileDialog.getExistingDirectory({ + iconRegistry, manager });