Repository: DigitalSlideArchive/HistomicsUI (language: python)

Issue:
Long Analyses Menus aren't accessible
If you install a lot of CLI plugins, or a CLI plugin has a lot of versions or algorithms, the Analyses menu will extend off of the web page; it is not scrollable and elements that are off the page cannot be reached.

This might be solvable with css, but it might require either javascript or refactoring the template.

The menu is specifically a bootstrap dropdown menu done with bootstrap's css for positioning it.  It does not have to remain a bootstrap dropdown menu, provided the functionality is fixed.


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 71675ec4..7f4472cb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@
 # See https://pre-commit.com/hooks.html for more hooks
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.5.0
+  rev: v4.6.0
   hooks:
   - id: check-added-large-files
   - id: check-ast
@@ -43,7 +43,7 @@ repos:
     files: README.rst
     name: rst-linter of README.rst
 - repo: https://github.com/codespell-project/codespell
-  rev: v2.2.6
+  rev: v2.3.0
   hooks:
   - id: codespell
     args:
@@ -62,13 +62,13 @@ repos:
         - './histomicsui/web_client/package.json'
 - repo: https://github.com/astral-sh/ruff-pre-commit
   # Ruff version.
-  rev: v0.1.3
+  rev: v0.6.0
   hooks:
     - id: ruff
       args: [--fix, --exit-non-zero-on-fix]
       types_or: [python, pyi, jupyter]
 - repo: https://github.com/asottile/pyupgrade
-  rev: v3.15.0
+  rev: v3.17.0
   hooks:
     - id: pyupgrade
       args:
@@ -79,6 +79,6 @@ repos:
   hooks:
     - id: autopep8
 - repo: https://github.com/PyCQA/flake8
-  rev: 6.1.0
+  rev: 7.1.1
   hooks:
     - id: flake8
diff --git a/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl b/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl
index 15fb3a14..c1cde948 100644
--- a/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl
+++ b/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl
@@ -1,3 +1,8 @@
 ul.h-analyses-dropdown
+  min-width 120px
+
   li > a
     padding 3px 20px
+
+  .dropdown-menu
+    min-width 120px
diff --git a/histomicsui/web_client/templates/layout/headerAnalyses.pug b/histomicsui/web_client/templates/layout/headerAnalyses.pug
index 22f6d618..080f8ba3 100644
--- a/histomicsui/web_client/templates/layout/headerAnalyses.pug
+++ b/histomicsui/web_client/templates/layout/headerAnalyses.pug
@@ -1,21 +1,74 @@
+mixin analysisMenu(image, imageName)
+  li.dropdown-submenu
+    a(tabindex='0', href='#', data-name=imageName) #{imageName}
+    ul.dropdown-menu
+      each version, versionName in image
+        li.dropdown-submenu
+          a(tabindex='0', href='#') #{versionName}
+          ul.dropdown-menu
+            each cli, cliName in version
+              - var api = cli.run.replace(/\/run$/, '');
+              li
+                a.h-analysis-item(
+                    tabindex='0',
+                    href='#',
+                    data-api=api,
+                    data-image=imageName,
+                    data-version=versionName,
+                    data-cli=cliName) #{cliName}
+
 a.h-analyses-dropdown-link(data-toggle='dropdown')
   | #[span.icon-tasks] Analyses #[span.icon-down-open]
-ul.h-analyses-dropdown.dropdown-menu(role='menu')
-  each image, imageName in analyses
-    li.dropdown-submenu
-      a(tabindex='0', href='#', data-name=imageName) #{imageName}
-      ul.dropdown-menu
-        each version, versionName in image
-          li.dropdown-submenu
-            a(tabindex='0', href='#') #{versionName}
-            ul.dropdown-menu
-              each cli, cliName in version
-                - var api = cli.run.replace(/\/run$/, '');
-                li
-                  a.h-analysis-item(
-                      tabindex='0',
-                      href='#',
-                      data-api=api,
-                      data-image=imageName,
-                      data-version=versionName,
-                      data-cli=cliName) #{cliName}
+-
+  let depth = 0;
+  let rows = maxEntries;
+  let keyList = Object.keys(analyses).sort();
+  let entries = keyList.length;
+  if (entries > maxEntries * maxEntries * maxEntries) {
+    rows = Math.ceil(Math.pow(entries, 1./3));
+  }
+  if (entries > maxEntries * maxEntries) {
+    depth = 2;
+  } else if (entries > maxEntries) {
+    depth = 1;
+  }
+if depth === 2
+  ul.h-analyses-dropdown.dropdown-menu(role='menu')
+    for d2v, d2i in Array(Math.ceil(entries / rows / rows))
+      -
+        let start2 = d2i * rows * rows;
+        let end2 = Math.min((d2i + 1) * rows * rows, entries);
+      li.dropdown-submenu
+        a(tabindex='0', href='#', title=keyList[start2] + ' to ' + keyList[end2 - 1]) #{start2 + 1} to #{end2}
+        ul.dropdown-menu
+          for d1v, d1i in Array(Math.ceil((end2 - start2) / rows))
+            -
+              let start1 = start2 + d1i * rows;
+              let end1 = Math.min(start2 + (d1i + 1) * rows, entries);
+            li.dropdown-submenu
+              a(tabindex='0', href='#', title=keyList[start1] + ' to ' + keyList[end1 - 1]) #{start1 + 1} to #{end1}
+              ul.dropdown-menu
+                for d0v, d0i in Array(end1 - start1)
+                  -
+                    let imageName = keyList[start1 + d0i];
+                    let image = analyses[imageName];
+                  +analysisMenu(image, imageName)
+else if depth === 1
+  ul.h-analyses-dropdown.dropdown-menu(role='menu')
+    for d1v, d1i in Array(Math.ceil(entries / rows))
+      -
+        let start1 = d1i * rows;
+        let end1 = Math.min((d1i + 1) * rows, entries);
+      li.dropdown-submenu
+        a(tabindex='0', href='#', title=keyList[start1] + ' to ' + keyList[end1 - 1]) #{start1 + 1} to #{end1}
+        ul.dropdown-menu
+          for d0v, d0i in Array(end1 - start1)
+            -
+              let imageName = keyList[start1 + d0i];
+              let image = analyses[imageName];
+            +analysisMenu(image, imageName)
+else
+  ul.h-analyses-dropdown.dropdown-menu(role='menu')
+    each imageName in keyList
+      - let image = analyses[imageName];
+      +analysisMenu(image, imageName)
diff --git a/histomicsui/web_client/views/layout/HeaderAnalysesView.js b/histomicsui/web_client/views/layout/HeaderAnalysesView.js
index bc673efa..6b05bdc1 100644
--- a/histomicsui/web_client/views/layout/HeaderAnalysesView.js
+++ b/histomicsui/web_client/views/layout/HeaderAnalysesView.js
@@ -27,10 +27,15 @@ var HeaderUserView = View.extend({
             restRequest({
                 url: 'slicer_cli_web/docker_image'
             }).then((analyses) => {
+                let maxEntries = Math.max(10, Math.floor(($('.h-image-view-body').height() || 0) / 26) - 10);
+                if (Object.keys(analyses).length > maxEntries * maxEntries) {
+                    maxEntries = Math.max(10, Math.floor((($('.h-image-view-body').height() || 0) / 26 - 10) / 2));
+                }
                 if (_.keys(analyses || {}).length > 0) {
                     this.$el.removeClass('hidden');
                     this.$el.html(headerAnalysesTemplate({
-                        analyses: analyses || {}
+                        analyses: analyses || {},
+                        maxEntries: maxEntries
                     }));
                     this.$('.h-analyses-dropdown-link').submenupicker();
                 } else {


Test patch:
diff --git a/tests/test_tcga.py b/tests/test_tcga.py
index 7dba14d4..2c9349fa 100644
--- a/tests/test_tcga.py
+++ b/tests/test_tcga.py
@@ -20,7 +20,7 @@
 from . import girder_utilities as utilities
 
 
-@pytest.fixture()
+@pytest.fixture
 def singular(tmp_path_factory):
     root_tmp_dir = tmp_path_factory.getbasetemp().parent
     with filelock.FileLock(root_tmp_dir / '.test_tcga.lock'):