diff --git a/app/test/tree.spec.ts b/app/test/tree.spec.ts index 58ac53970..b503268f4 100644 --- a/app/test/tree.spec.ts +++ b/app/test/tree.spec.ts @@ -10,3 +10,11 @@ test('Tree', async ({ page }) => { const button = await page.$('text="New Notebook"'); expect(button).toBeDefined(); }); + +test('should go to subfolder', async ({ page }) => { + await page.goto(`${BASE_URL}retro/tree/binder`); + + const breadcrumb = await page.waitForSelector('.jp-FileBrowser-crumbs'); + + expect(await breadcrumb.textContent()).toEqual('/binder/'); +}); diff --git a/packages/application-extension/package.json b/packages/application-extension/package.json index 1788628c7..b00dcc437 100644 --- a/packages/application-extension/package.json +++ b/packages/application-extension/package.json @@ -50,6 +50,8 @@ "@jupyterlab/mainmenu": "^3.1.8", "@jupyterlab/settingregistry": "^3.1.8", "@jupyterlab/translation": "^3.1.8", + "@lumino/coreutils": "^1.8.0", + "@lumino/disposable": "^1.7.0", "@lumino/widgets": "^1.23.0", "@retrolab/application": "^0.3.2", "@retrolab/ui-components": "^0.3.2" diff --git a/packages/application-extension/src/index.ts b/packages/application-extension/src/index.ts index 49b3fd1bb..af379950c 100644 --- a/packages/application-extension/src/index.ts +++ b/packages/application-extension/src/index.ts @@ -16,7 +16,7 @@ import { ICommandPalette } from '@jupyterlab/apputils'; -import { PageConfig, PathExt } from '@jupyterlab/coreutils'; +import { PageConfig, PathExt, URLExt } from '@jupyterlab/coreutils'; import { IDocumentManager, renameDialog } from '@jupyterlab/docmanager'; @@ -30,6 +30,10 @@ import { RetroApp, RetroShell, IRetroShell } from '@retrolab/application'; import { jupyterIcon, retroInlineIcon } from '@retrolab/ui-components'; +import { PromiseDelegate } from '@lumino/coreutils'; + +import { DisposableDelegate, DisposableSet } from '@lumino/disposable'; + import { Widget } from '@lumino/widgets'; /** @@ -475,6 +479,68 @@ const translator: JupyterFrontEndPlugin = { provides: ITranslator }; +/** + * The default tree route resolver plugin. + */ +const tree: JupyterFrontEndPlugin = { + id: '@retrolab/application-extension:tree-resolver', + autoStart: true, + requires: [IRouter], + provides: JupyterFrontEnd.ITreeResolver, + activate: ( + app: JupyterFrontEnd, + router: IRouter + ): JupyterFrontEnd.ITreeResolver => { + const { commands } = app; + const set = new DisposableSet(); + const delegate = new PromiseDelegate(); + + const treePattern = new RegExp('/retro(/tree/.*)?'); + + set.add( + commands.addCommand('retrolab-router:tree', { + execute: (async (args: IRouter.ILocation) => { + if (set.isDisposed) { + return; + } + + const query = URLExt.queryStringToObject(args.search ?? ''); + const browser = query['file-browser-path'] || ''; + + // Remove the file browser path from the query string. + delete query['file-browser-path']; + + // Clean up artifacts immediately upon routing. + set.dispose(); + + delegate.resolve({ browser, file: PageConfig.getOption('treePath') }); + }) as (args: any) => Promise + }) + ); + set.add( + router.register({ command: 'retrolab-router:tree', pattern: treePattern }) + ); + + // If a route is handled by the router without the tree command being + // invoked, resolve to `null` and clean up artifacts. + const listener = () => { + if (set.isDisposed) { + return; + } + set.dispose(); + delegate.resolve(null); + }; + router.routed.connect(listener); + set.add( + new DisposableDelegate(() => { + router.routed.disconnect(listener); + }) + ); + + return { paths: delegate.promise }; + } +}; + /** * Zen mode plugin */ @@ -552,6 +618,7 @@ const plugins: JupyterFrontEndPlugin[] = [ title, topVisibility, translator, + tree, zen ]; diff --git a/retrolab/app.py b/retrolab/app.py index 56b50f909..544e114fd 100644 --- a/retrolab/app.py +++ b/retrolab/app.py @@ -114,7 +114,12 @@ class RetroTreeHandler(RetroHandler): if await maybe_future(cm.is_hidden(path)) and not cm.allow_hidden: self.log.info("Refusing to serve hidden directory, via 404 Error") raise web.HTTPError(404) - tpl = self.render_template("tree.html", page_config=self.get_page_config()) + + # Set treePath for routing to the directory + page_config = self.get_page_config() + page_config['treePath'] = path + + tpl = self.render_template("tree.html", page_config=page_config) return self.write(tpl) elif await maybe_future(cm.file_exists(path)): # it's not a directory, we have redirecting to do