From 6f216e9fe3cda52c0bc37e50df5963fedf5da3bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Wed, 1 Sep 2021 15:18:27 +0200 Subject: [PATCH 1/4] Route on file browser navigation Fixes #15 --- app/test/tree.spec.ts | 8 +++ packages/application-extension/package.json | 2 + packages/application-extension/src/index.ts | 69 ++++++++++++++++++++- retrolab/app.py | 7 ++- 4 files changed, 84 insertions(+), 2 deletions(-) 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 From 65b0ad9978f9441586eeec988c7c9d27304ad84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Wed, 1 Sep 2021 16:25:53 +0200 Subject: [PATCH 2/4] Fix test --- app/test/tree.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/test/tree.spec.ts b/app/test/tree.spec.ts index b503268f4..c910c483b 100644 --- a/app/test/tree.spec.ts +++ b/app/test/tree.spec.ts @@ -14,7 +14,7 @@ test('Tree', async ({ page }) => { 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/'); + expect( + await page.waitForSelector('.jp-FileBrowser-crumbs >> text=/binder/') + ).toBeTruthy(); }); From 57220b21dad154d67382c2bd402fdd5156c6aecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Wed, 1 Sep 2021 17:08:30 +0200 Subject: [PATCH 3/4] Update url with current filebrowser directory --- app/test/tree.spec.ts | 10 ++++++ packages/application-extension/src/index.ts | 34 +++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/app/test/tree.spec.ts b/app/test/tree.spec.ts index c910c483b..65e80e52c 100644 --- a/app/test/tree.spec.ts +++ b/app/test/tree.spec.ts @@ -18,3 +18,13 @@ test('should go to subfolder', async ({ page }) => { await page.waitForSelector('.jp-FileBrowser-crumbs >> text=/binder/') ).toBeTruthy(); }); + +test('should update url when navigating in filebrowser', async ({ page }) => { + await page.goto(`${BASE_URL}retro/tree`); + + await page.dblclick('.jp-FileBrowser-listing >> text=binder'); + + await page.waitForSelector('.jp-FileBrowser-crumbs >> text=/binder/'); + + expect(page.url()).toEqual(`${BASE_URL}retro/tree/binder`); +}); diff --git a/packages/application-extension/src/index.ts b/packages/application-extension/src/index.ts index af379950c..d2716256b 100644 --- a/packages/application-extension/src/index.ts +++ b/packages/application-extension/src/index.ts @@ -4,6 +4,7 @@ import { ILabStatus, IRouter, + ITreePathUpdater, JupyterFrontEnd, JupyterFrontEndPlugin, Router @@ -74,6 +75,11 @@ namespace CommandIDs { * Open the tree page. */ export const openTree = 'application:open-tree'; + + /** + * Resolve tree path + */ + export const resolveTree = 'application:resolve-tree'; } /** @@ -498,7 +504,7 @@ const tree: JupyterFrontEndPlugin = { const treePattern = new RegExp('/retro(/tree/.*)?'); set.add( - commands.addCommand('retrolab-router:tree', { + commands.addCommand(CommandIDs.resolveTree, { execute: (async (args: IRouter.ILocation) => { if (set.isDisposed) { return; @@ -518,7 +524,7 @@ const tree: JupyterFrontEndPlugin = { }) ); set.add( - router.register({ command: 'retrolab-router:tree', pattern: treePattern }) + router.register({ command: CommandIDs.resolveTree, pattern: treePattern }) ); // If a route is handled by the router without the tree command being @@ -541,6 +547,29 @@ const tree: JupyterFrontEndPlugin = { } }; +const treePathUpdater: JupyterFrontEndPlugin = { + id: '@retrolab/application-extension:tree-updater', + requires: [IRouter], + provides: ITreePathUpdater, + activate: (app: JupyterFrontEnd, router: IRouter) => { + function updateTreePath(treePath: string) { + if (treePath !== PageConfig.getOption('treePath')) { + const path = URLExt.join( + PageConfig.getOption('baseUrl') || '/', + PageConfig.getOption('frontendUrl'), + 'tree', + treePath + ); + router.navigate(path, { skipRouting: true }); + // Persist the new tree path to PageConfig as it is used elsewhere at runtime. + PageConfig.setOption('treePath', URLExt.encodeParts(treePath)); + } + } + return updateTreePath; + }, + autoStart: true +}; + /** * Zen mode plugin */ @@ -619,6 +648,7 @@ const plugins: JupyterFrontEndPlugin[] = [ topVisibility, translator, tree, + treePathUpdater, zen ]; From 47e400bfcfcaaf3081ccd5773a438145c2cd36e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Thu, 2 Sep 2021 10:07:50 +0200 Subject: [PATCH 4/4] Fix tree path builder --- packages/application-extension/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/application-extension/src/index.ts b/packages/application-extension/src/index.ts index d2716256b..3e2aa5d43 100644 --- a/packages/application-extension/src/index.ts +++ b/packages/application-extension/src/index.ts @@ -556,13 +556,13 @@ const treePathUpdater: JupyterFrontEndPlugin = { if (treePath !== PageConfig.getOption('treePath')) { const path = URLExt.join( PageConfig.getOption('baseUrl') || '/', - PageConfig.getOption('frontendUrl'), + 'retro', 'tree', - treePath + URLExt.encodeParts(treePath) ); router.navigate(path, { skipRouting: true }); // Persist the new tree path to PageConfig as it is used elsewhere at runtime. - PageConfig.setOption('treePath', URLExt.encodeParts(treePath)); + PageConfig.setOption('treePath', treePath); } } return updateTreePath;