Repository: jupyterlab/retrolab (language: python)

Issue:
Route on file browser navigation
At the moment there is no routing when navigating in the file browser:

![filebrowser-no-routing](https://user-images.githubusercontent.com/591645/101535769-6f98da80-3999-11eb-885b-2ce4076ea8a7.gif)

Also we should be able to restore the file browser on page load, for example opening the `binder` folder when http://localhost:8888/classic/tree/binder is accessed.


Discussion / hints:

Fix patch:
diff --git a/packages/application-extension/package.json b/packages/application-extension/package.json
index 1788628c..b00dcc43 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 49b3fd1b..3e2aa5d4 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
@@ -16,7 +17,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 +31,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';
 
 /**
@@ -70,6 +75,11 @@ namespace CommandIDs {
    * Open the tree page.
    */
   export const openTree = 'application:open-tree';
+
+  /**
+   * Resolve tree path
+   */
+  export const resolveTree = 'application:resolve-tree';
 }
 
 /**
@@ -475,6 +485,91 @@ const translator: JupyterFrontEndPlugin<ITranslator> = {
   provides: ITranslator
 };
 
+/**
+ * The default tree route resolver plugin.
+ */
+const tree: JupyterFrontEndPlugin<JupyterFrontEnd.ITreeResolver> = {
+  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<JupyterFrontEnd.ITreeResolver.Paths>();
+
+    const treePattern = new RegExp('/retro(/tree/.*)?');
+
+    set.add(
+      commands.addCommand(CommandIDs.resolveTree, {
+        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<void>
+      })
+    );
+    set.add(
+      router.register({ command: CommandIDs.resolveTree, 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 };
+  }
+};
+
+const treePathUpdater: JupyterFrontEndPlugin<ITreePathUpdater> = {
+  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') || '/',
+          'retro',
+          'tree',
+          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', treePath);
+      }
+    }
+    return updateTreePath;
+  },
+  autoStart: true
+};
+
 /**
  * Zen mode plugin
  */
@@ -552,6 +647,8 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
   title,
   topVisibility,
   translator,
+  tree,
+  treePathUpdater,
   zen
 ];
 
diff --git a/retrolab/app.py b/retrolab/app.py
index 56b50f90..544e114f 100644
--- a/retrolab/app.py
+++ b/retrolab/app.py
@@ -114,7 +114,12 @@ async def get(self, path=None):
             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


Test patch:
diff --git a/app/test/tree.spec.ts b/app/test/tree.spec.ts
index 58ac5397..65e80e52 100644
--- a/app/test/tree.spec.ts
+++ b/app/test/tree.spec.ts
@@ -10,3 +10,21 @@ 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`);
+
+  expect(
+    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`);
+});