histomicsui 1.8.1.dev163__tar.gz → 1.8.1.dev194__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/.pre-commit-config.yaml +1 -1
  2. {histomicsui-1.8.1.dev163/histomicsui.egg-info → histomicsui-1.8.1.dev194}/PKG-INFO +1 -1
  3. histomicsui-1.8.1.dev194/docs/annotation_group_restrictions.rst +41 -0
  4. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/docs/config_options.rst +13 -0
  5. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194/histomicsui.egg-info}/PKG-INFO +1 -1
  6. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui.egg-info/SOURCES.txt +2 -0
  7. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui.egg-info/scm_file_list.json +61 -58
  8. histomicsui-1.8.1.dev194/histomicsui.egg-info/scm_version.json +8 -0
  9. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_files/.histomicsui_config.yaml +1 -0
  10. histomicsui-1.8.1.dev194/tests/web_client_specs/allowedGroupsSpec.js +371 -0
  11. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/annotationSpec.js +125 -4
  12. histomicsui-1.8.1.dev163/histomicsui.egg-info/scm_version.json +0 -8
  13. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/.circleci/config.yml +0 -0
  14. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/.circleci/get_version.py +0 -0
  15. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/.circleci/publish_pypi.sh +0 -0
  16. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/.editorconfig +0 -0
  17. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/.git-blame-ignore-revs +0 -0
  18. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/.gitignore +0 -0
  19. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/LICENSE +0 -0
  20. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/MANIFEST.in +0 -0
  21. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/NOTICE +0 -0
  22. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/README.rst +0 -0
  23. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/codecov.yml +0 -0
  24. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/docs/controls.rst +0 -0
  25. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/docs/images/cut.gif +0 -0
  26. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/docs/images/difference.gif +0 -0
  27. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/docs/images/intersect.gif +0 -0
  28. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/docs/images/union.gif +0 -0
  29. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/docs/images/xor.gif +0 -0
  30. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/docs/slicer_cli_plugins.rst +0 -0
  31. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/__init__.py +0 -0
  32. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/constants.py +0 -0
  33. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/girder_worker_plugin.py +0 -0
  34. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/handlers.py +0 -0
  35. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/models/__init__.py +0 -0
  36. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/models/aperio.py +0 -0
  37. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/models/case.py +0 -0
  38. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/models/cohort.py +0 -0
  39. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/models/image.py +0 -0
  40. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/models/meta.py +0 -0
  41. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/models/pathology.py +0 -0
  42. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/models/slide.py +0 -0
  43. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/rest/__init__.py +0 -0
  44. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/rest/aperio.py +0 -0
  45. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/rest/hui_resource.py +0 -0
  46. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/rest/image_browse_resource.py +0 -0
  47. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/rest/system.py +0 -0
  48. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui/rest/tcga.py +0 -0
  49. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui.egg-info/dependency_links.txt +0 -0
  50. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui.egg-info/entry_points.txt +0 -0
  51. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui.egg-info/not-zip-safe +0 -0
  52. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui.egg-info/requires.txt +0 -0
  53. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/histomicsui.egg-info/top_level.txt +0 -0
  54. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/pyproject.toml +0 -0
  55. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/requirements-dev.txt +0 -0
  56. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/ruff.toml +0 -0
  57. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/sample_data/Easy1.png +0 -0
  58. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/sample_data/sample_svs_image.TCGA-DU-6399-01A-01-TS1.e8eb65de-d63e-42db-af6f-14fefbbdf7bd.svs +0 -0
  59. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/setup.cfg +0 -0
  60. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/setup.py +0 -0
  61. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/__init__.py +0 -0
  62. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/conftest.py +0 -0
  63. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/datastore.py +0 -0
  64. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/girder_utilities.py +0 -0
  65. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_files/sample.anot +0 -0
  66. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_files/sample.meta +0 -0
  67. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_files/sample_girder_id.anot +0 -0
  68. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_files/test_analysis_detection.xml +0 -0
  69. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_files/test_analysis_features.xml +0 -0
  70. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_handlers.py +0 -0
  71. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_hui_rest.py +0 -0
  72. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_image_browse_endpoints.py +0 -0
  73. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_load.py +0 -0
  74. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/test_tcga.py +0 -0
  75. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/.eslintrc +0 -0
  76. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/analysisSpec.js +0 -0
  77. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/girderUISpec.js +0 -0
  78. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/huiSpec.js +0 -0
  79. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/huiTest.js +0 -0
  80. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/itemSpec.js +0 -0
  81. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/metadataPanelSpec.js +0 -0
  82. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/metadataPlotSpec.js +0 -0
  83. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/overviewPanelSpec.js +0 -0
  84. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/panelLayoutSpec.js +0 -0
  85. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tests/web_client_specs/pixelmapCategorySpec.js +0 -0
  86. {histomicsui-1.8.1.dev163 → histomicsui-1.8.1.dev194}/tox.ini +0 -0
@@ -48,7 +48,7 @@ repos:
48
48
  - id: codespell
49
49
  args:
50
50
  - --ignore-words-list
51
- - "hist,indext,pixelx,thex,subtile,afterAll"
51
+ - "hist,indext,pixelx,thex,subtile,afterAll,groupE"
52
52
  - --exclude-file=histomicsui/web_client/package-lock.json
53
53
  - repo: https://github.com/syntaqx/git-hooks
54
54
  rev: v0.0.18
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: histomicsui
3
- Version: 1.8.1.dev163
3
+ Version: 1.8.1.dev194
4
4
  Summary: Organize, visualize, and analyze histology images.
5
5
  Home-page: https://github.com/DigitalSlideArchive/histomicsui
6
6
  Author: Kitware, Inc.
@@ -0,0 +1,41 @@
1
+ Restricting Annotations to Allowed Groups
2
+ ==========================================
3
+
4
+ An annotation document can restrict which style groups its elements may be assigned to by setting an ``allowed_groups`` key in the annotation's ``attributes``:
5
+
6
+ ::
7
+
8
+ {
9
+ "name": "restricted annotation",
10
+ "attributes": {
11
+ "allowed_groups": ["groupA", "groupB"]
12
+ },
13
+ "elements": [...]
14
+ }
15
+
16
+ ``allowed_groups`` may also be given as a JSON-stringified array for convenience when setting it through the UI metadata editor:
17
+
18
+ ::
19
+
20
+ {
21
+ "attributes": {
22
+ "allowed_groups": "[\"groupA\", \"groupB\"]"
23
+ }
24
+ }
25
+
26
+ When this restriction is present, the Draw panel and the annotation element context menu will only offer the listed groups instead of every style group that exists.
27
+
28
+ Validation
29
+ ----------
30
+
31
+ ``allowed_groups`` is only honored when it is a non-empty array of non-empty strings, or a string that parses as JSON into such an array. Any other value - a missing key, a non-array/non-JSON-array value, an unparsable string, an empty array, or an array containing no valid strings - is treated as *unrestricted*, and every existing style group is offered as usual. Duplicate entries are ignored.
32
+
33
+ Auto-creation of missing groups
34
+ --------------------------------
35
+
36
+ If ``allowed_groups`` names a style group that does not yet exist, it is automatically created the first time the restriction is encountered, by copying the style (fill color, line color, line width, pattern) of the default group. The newly created groups are persisted immediately. This can happen from either the Draw panel or the annotation context menu, whichever sees the restricted annotation first.
37
+
38
+ Effect on the current selection
39
+ --------------------------------
40
+
41
+ - **Draw panel**: If the currently active style group is not one of the annotation's allowed groups, the Draw panel automatically switches to the first allowed group that exists.
@@ -56,6 +56,10 @@ _________________
56
56
  fillColor: rgba(0, 128, 0, 0.25)
57
57
  lineColor: rgb(0, 128, 0)
58
58
  lineWidth: 2
59
+ # patterns can be none, circle, triangle, diamond, flower(number),
60
+ # star(number), jack(number), where number is from 1 to 16. Not
61
+ # specifying a pattern is the same as none.
62
+ pattern: star5
59
63
  -
60
64
  id: Blue
61
65
  fillColor: rgba(0, 0, 255, 0.25)
@@ -75,6 +79,15 @@ If a hotkey is specified for a group, it will only be active if the group was lo
75
79
  action: group
76
80
  param: Red
77
81
 
82
+ Unselected Annotation Capacity
83
+ ______________________________
84
+ When annotation elements are selected, the annotations not included in the user's selection have their opacity decreased to 0.33 times their specified opacity. This can be modified:
85
+ ::
86
+ ---
87
+ # A lower value will make unselected annotations more transparent.
88
+ unselectedOpacityMultiplier: 0.33
89
+
90
+
78
91
  UI Settings
79
92
  ___________
80
93
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: histomicsui
3
- Version: 1.8.1.dev163
3
+ Version: 1.8.1.dev194
4
4
  Summary: Organize, visualize, and analyze histology images.
5
5
  Home-page: https://github.com/DigitalSlideArchive/histomicsui
6
6
  Author: Kitware, Inc.
@@ -16,6 +16,7 @@ tox.ini
16
16
  .circleci/config.yml
17
17
  .circleci/get_version.py
18
18
  .circleci/publish_pypi.sh
19
+ docs/annotation_group_restrictions.rst
19
20
  docs/config_options.rst
20
21
  docs/controls.rst
21
22
  docs/slicer_cli_plugins.rst
@@ -69,6 +70,7 @@ tests/test_files/sample_girder_id.anot
69
70
  tests/test_files/test_analysis_detection.xml
70
71
  tests/test_files/test_analysis_features.xml
71
72
  tests/web_client_specs/.eslintrc
73
+ tests/web_client_specs/allowedGroupsSpec.js
72
74
  tests/web_client_specs/analysisSpec.js
73
75
  tests/web_client_specs/annotationSpec.js
74
76
  tests/web_client_specs/girderUISpec.js
@@ -1,33 +1,28 @@
1
1
  {
2
2
  "files": [
3
+ ".circleci/config.yml",
4
+ ".circleci/get_version.py",
5
+ ".circleci/publish_pypi.sh",
3
6
  ".editorconfig",
4
- "LICENSE",
5
- "NOTICE",
6
- "codecov.yml",
7
- "pyproject.toml",
8
- "setup.cfg",
9
7
  ".git-blame-ignore-revs",
10
8
  ".gitignore",
11
9
  ".pre-commit-config.yaml",
10
+ "LICENSE",
12
11
  "MANIFEST.in",
12
+ "NOTICE",
13
13
  "README.rst",
14
- "requirements-dev.txt",
15
- "ruff.toml",
16
- "setup.py",
17
- "tox.ini",
18
- ".circleci/config.yml",
19
- ".circleci/get_version.py",
20
- ".circleci/publish_pypi.sh",
14
+ "codecov.yml",
15
+ "docs/annotation_group_restrictions.rst",
21
16
  "docs/config_options.rst",
22
17
  "docs/controls.rst",
23
- "docs/slicer_cli_plugins.rst",
24
18
  "docs/images/cut.gif",
25
19
  "docs/images/difference.gif",
26
20
  "docs/images/intersect.gif",
27
21
  "docs/images/union.gif",
28
22
  "docs/images/xor.gif",
29
- "histomicsui/constants.py",
23
+ "docs/slicer_cli_plugins.rst",
30
24
  "histomicsui/__init__.py",
25
+ "histomicsui/constants.py",
31
26
  "histomicsui/girder_worker_plugin.py",
32
27
  "histomicsui/handlers.py",
33
28
  "histomicsui/models/__init__.py",
@@ -40,28 +35,15 @@
40
35
  "histomicsui/models/slide.py",
41
36
  "histomicsui/rest/__init__.py",
42
37
  "histomicsui/rest/aperio.py",
43
- "histomicsui/rest/image_browse_resource.py",
44
38
  "histomicsui/rest/hui_resource.py",
39
+ "histomicsui/rest/image_browse_resource.py",
45
40
  "histomicsui/rest/system.py",
46
41
  "histomicsui/rest/tcga.py",
47
- "histomicsui/web_client/routes.js",
48
42
  "histomicsui/web_client/.gitignore",
49
43
  "histomicsui/web_client/app.js",
50
- "histomicsui/web_client/events.js",
51
- "histomicsui/web_client/histomicsui.ts",
52
- "histomicsui/web_client/index.html",
53
- "histomicsui/web_client/index.js",
54
- "histomicsui/web_client/main.js",
55
- "histomicsui/web_client/package-lock.json",
56
- "histomicsui/web_client/package.json",
57
- "histomicsui/web_client/playwright.config.ts",
58
- "histomicsui/web_client/router.js",
59
- "histomicsui/web_client/tsconfig.json",
60
- "histomicsui/web_client/vite-env.d.ts",
61
- "histomicsui/web_client/vite.config.ts",
62
- "histomicsui/web_client/collections/index.js",
63
44
  "histomicsui/web_client/collections/StyleCollection.js",
64
45
  "histomicsui/web_client/collections/UserCollection.js",
46
+ "histomicsui/web_client/collections/index.js",
65
47
  "histomicsui/web_client/dialogs/confirmDialog.js",
66
48
  "histomicsui/web_client/dialogs/editElement.js",
67
49
  "histomicsui/web_client/dialogs/editRegionOfInterest.js",
@@ -71,9 +53,15 @@
71
53
  "histomicsui/web_client/dialogs/openAnnotatedImage.js",
72
54
  "histomicsui/web_client/dialogs/openImage.js",
73
55
  "histomicsui/web_client/dialogs/saveAnnotation.js",
74
- "histomicsui/web_client/models/index.js",
56
+ "histomicsui/web_client/events.js",
57
+ "histomicsui/web_client/histomicsui.ts",
58
+ "histomicsui/web_client/index.html",
59
+ "histomicsui/web_client/index.js",
60
+ "histomicsui/web_client/main.js",
75
61
  "histomicsui/web_client/models/StyleModel.js",
76
- "histomicsui/web_client/panels/index.js",
62
+ "histomicsui/web_client/models/index.js",
63
+ "histomicsui/web_client/package-lock.json",
64
+ "histomicsui/web_client/package.json",
77
65
  "histomicsui/web_client/panels/AnnotationSelector.js",
78
66
  "histomicsui/web_client/panels/DrawWidget.js",
79
67
  "histomicsui/web_client/panels/FrameSelectorWidget.js",
@@ -82,13 +70,17 @@
82
70
  "histomicsui/web_client/panels/OverviewWidget.js",
83
71
  "histomicsui/web_client/panels/RegionSelector.js",
84
72
  "histomicsui/web_client/panels/ZoomWidget.js",
73
+ "histomicsui/web_client/panels/index.js",
74
+ "histomicsui/web_client/playwright.config.ts",
75
+ "histomicsui/web_client/router.js",
76
+ "histomicsui/web_client/routes.js",
85
77
  "histomicsui/web_client/static/favicon.png",
86
78
  "histomicsui/web_client/stylesheets/body/configView.styl",
87
79
  "histomicsui/web_client/stylesheets/body/image.styl",
80
+ "histomicsui/web_client/stylesheets/dialogs/editStyleGroups.styl",
88
81
  "histomicsui/web_client/stylesheets/dialogs/metadataPlot.styl",
89
82
  "histomicsui/web_client/stylesheets/dialogs/openAnnotatedImage.styl",
90
83
  "histomicsui/web_client/stylesheets/dialogs/saveAnnotation.styl",
91
- "histomicsui/web_client/stylesheets/dialogs/editStyleGroups.styl",
92
84
  "histomicsui/web_client/stylesheets/layout/header.styl",
93
85
  "histomicsui/web_client/stylesheets/layout/headerAnalyses.styl",
94
86
  "histomicsui/web_client/stylesheets/layout/headerImage.styl",
@@ -105,7 +97,6 @@
105
97
  "histomicsui/web_client/stylesheets/popover/annotationPopover.styl",
106
98
  "histomicsui/web_client/stylesheets/views/itemList.styl",
107
99
  "histomicsui/web_client/stylesheets/views/searchResultsView.styl",
108
- "histomicsui/web_client/templates/elementProps.pug",
109
100
  "histomicsui/web_client/templates/body/configView.pug",
110
101
  "histomicsui/web_client/templates/body/image.pug",
111
102
  "histomicsui/web_client/templates/dialogs/annotatedImageList.pug",
@@ -117,6 +108,7 @@
117
108
  "histomicsui/web_client/templates/dialogs/openAnnotatedImage.pug",
118
109
  "histomicsui/web_client/templates/dialogs/openImage.pug",
119
110
  "histomicsui/web_client/templates/dialogs/saveAnnotation.pug",
111
+ "histomicsui/web_client/templates/elementProps.pug",
120
112
  "histomicsui/web_client/templates/layout/header.pug",
121
113
  "histomicsui/web_client/templates/layout/headerAnalyses.pug",
122
114
  "histomicsui/web_client/templates/layout/headerImage.pug",
@@ -136,57 +128,69 @@
136
128
  "histomicsui/web_client/templates/popover/annotationPopover.pug",
137
129
  "histomicsui/web_client/templates/popover/annotationPopoverMetadata.pug",
138
130
  "histomicsui/web_client/templates/popover/pixelmapContextMenu.pug",
131
+ "histomicsui/web_client/tests/server.ts",
132
+ "histomicsui/web_client/tests/spec/viewer.spec.ts",
133
+ "histomicsui/web_client/tests/util.ts",
134
+ "histomicsui/web_client/tsconfig.json",
135
+ "histomicsui/web_client/utilities/allowedGroups.js",
136
+ "histomicsui/web_client/vendor/localStorage/driver.js",
137
+ "histomicsui/web_client/vendor/localStorage/localstorage.js",
138
+ "histomicsui/web_client/vendor/localStorage/sync.js",
139
+ "histomicsui/web_client/vendor/localStorage/utils.js",
139
140
  "histomicsui/web_client/views/HierarchyWidget.js",
140
141
  "histomicsui/web_client/views/JobDetailsWidget.js",
141
142
  "histomicsui/web_client/views/View.js",
143
+ "histomicsui/web_client/views/body/ConfigView.js",
144
+ "histomicsui/web_client/views/body/ImageView.js",
145
+ "histomicsui/web_client/views/body/index.js",
142
146
  "histomicsui/web_client/views/index.js",
143
147
  "histomicsui/web_client/views/itemList.js",
144
148
  "histomicsui/web_client/views/itemPage.js",
145
- "histomicsui/web_client/views/searchResultsView.js",
146
- "histomicsui/web_client/views/utils.js",
147
- "histomicsui/web_client/views/body/index.js",
148
- "histomicsui/web_client/views/body/ConfigView.js",
149
- "histomicsui/web_client/views/body/ImageView.js",
150
- "histomicsui/web_client/views/layout/HeaderView.js",
151
149
  "histomicsui/web_client/views/layout/HeaderAnalysesView.js",
152
150
  "histomicsui/web_client/views/layout/HeaderImageView.js",
153
151
  "histomicsui/web_client/views/layout/HeaderUserView.js",
152
+ "histomicsui/web_client/views/layout/HeaderView.js",
154
153
  "histomicsui/web_client/views/layout/LoginView.js",
155
154
  "histomicsui/web_client/views/layout/index.js",
156
- "histomicsui/web_client/views/popover/PixelmapContextMenu.js",
157
- "histomicsui/web_client/views/popover/index.js",
158
155
  "histomicsui/web_client/views/popover/AnnotationContextMenu.js",
159
156
  "histomicsui/web_client/views/popover/AnnotationPopover.js",
160
- "histomicsui/web_client/vue/index.js",
157
+ "histomicsui/web_client/views/popover/PixelmapContextMenu.js",
158
+ "histomicsui/web_client/views/popover/index.js",
159
+ "histomicsui/web_client/views/searchResultsView.js",
160
+ "histomicsui/web_client/views/utils.js",
161
+ "histomicsui/web_client/vite-env.d.ts",
162
+ "histomicsui/web_client/vite.config.ts",
161
163
  "histomicsui/web_client/vue/components/AnnotationHistoryBrowser.vue",
162
- "histomicsui/web_client/vue/components/ColorPickerInput.vue",
163
164
  "histomicsui/web_client/vue/components/AnnotationHistoryBrowserContainer.vue",
164
165
  "histomicsui/web_client/vue/components/AnnotationHistoryGroup.vue",
166
+ "histomicsui/web_client/vue/components/ColorPickerInput.vue",
165
167
  "histomicsui/web_client/vue/components/EditHeatmapOrGridData.vue",
166
168
  "histomicsui/web_client/vue/components/EditHeatmapOrGridDataContainer.vue",
167
- "histomicsui/web_client/tests/server.ts",
168
- "histomicsui/web_client/tests/util.ts",
169
- "histomicsui/web_client/tests/spec/viewer.spec.ts",
170
- "histomicsui/web_client/vendor/localStorage/driver.js",
171
- "histomicsui/web_client/vendor/localStorage/localstorage.js",
172
- "histomicsui/web_client/vendor/localStorage/sync.js",
173
- "histomicsui/web_client/vendor/localStorage/utils.js",
169
+ "histomicsui/web_client/vue/index.js",
170
+ "pyproject.toml",
171
+ "requirements-dev.txt",
172
+ "ruff.toml",
173
+ "sample_data/Easy1.png",
174
+ "sample_data/sample_svs_image.TCGA-DU-6399-01A-01-TS1.e8eb65de-d63e-42db-af6f-14fefbbdf7bd.svs",
175
+ "setup.cfg",
176
+ "setup.py",
174
177
  "tests/__init__.py",
178
+ "tests/conftest.py",
175
179
  "tests/datastore.py",
176
180
  "tests/girder_utilities.py",
177
- "tests/test_image_browse_endpoints.py",
178
- "tests/test_load.py",
179
- "tests/test_tcga.py",
180
- "tests/conftest.py",
181
- "tests/test_handlers.py",
182
- "tests/test_hui_rest.py",
183
181
  "tests/test_files/.histomicsui_config.yaml",
184
182
  "tests/test_files/sample.anot",
185
183
  "tests/test_files/sample.meta",
186
184
  "tests/test_files/sample_girder_id.anot",
187
185
  "tests/test_files/test_analysis_detection.xml",
188
186
  "tests/test_files/test_analysis_features.xml",
187
+ "tests/test_handlers.py",
188
+ "tests/test_hui_rest.py",
189
+ "tests/test_image_browse_endpoints.py",
190
+ "tests/test_load.py",
191
+ "tests/test_tcga.py",
189
192
  "tests/web_client_specs/.eslintrc",
193
+ "tests/web_client_specs/allowedGroupsSpec.js",
190
194
  "tests/web_client_specs/analysisSpec.js",
191
195
  "tests/web_client_specs/annotationSpec.js",
192
196
  "tests/web_client_specs/girderUISpec.js",
@@ -198,7 +202,6 @@
198
202
  "tests/web_client_specs/overviewPanelSpec.js",
199
203
  "tests/web_client_specs/panelLayoutSpec.js",
200
204
  "tests/web_client_specs/pixelmapCategorySpec.js",
201
- "sample_data/Easy1.png",
202
- "sample_data/sample_svs_image.TCGA-DU-6399-01A-01-TS1.e8eb65de-d63e-42db-af6f-14fefbbdf7bd.svs"
205
+ "tox.ini"
203
206
  ]
204
207
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.8.0",
3
+ "distance": 194,
4
+ "node": "g639dbd6339b7b24f5ba7ea6c20a827632d7ec5c2",
5
+ "dirty": false,
6
+ "branch": "girder-5",
7
+ "node_date": "2026-08-11"
8
+ }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ unselectedOpacityMultiplier: 0.5
2
3
  annotationGroups:
3
4
  # If replaceGroups isn't present or false, groups will be merged with
4
5
  # existing groups. Those that have the same name will be updated. If
@@ -0,0 +1,371 @@
1
+ /* globals describe, it, expect, waitsFor, runs, huiTest, girderTest */
2
+
3
+ girderTest.importPlugin(
4
+ 'jobs', 'large_image', 'large_image_annotation', 'slicer_cli_web', 'histomicsui'
5
+ );
6
+ girderTest.addScripts([
7
+ '/static/built/plugins/histomicsui/huiTest.js',
8
+ '/static/built/plugins/histomicsui/extra/sinon.js'
9
+ ]);
10
+
11
+ girderTest.promise.done(function () {
12
+ huiTest.startApp();
13
+
14
+ describe('allowed_groups annotation metadata tests', function () {
15
+ var girder, largeImageAnnotation, histomicsUI;
16
+
17
+ /**
18
+ * POST a new annotation with the given name/attributes/elements and fetch it back into a
19
+ * fresh AnnotationModel, storing the result on `result.annotation`.
20
+ */
21
+ function createAnnotation(name, attributes, elements, result) {
22
+ var annotationId;
23
+ runs(function () {
24
+ girder.rest.restRequest({
25
+ url: 'annotation?itemId=' + huiTest.imageId(),
26
+ contentType: 'application/json',
27
+ processData: false,
28
+ type: 'POST',
29
+ data: JSON.stringify({
30
+ name: name,
31
+ attributes: attributes,
32
+ elements: elements
33
+ })
34
+ }).then(function (resp) {
35
+ annotationId = resp._id;
36
+ return null;
37
+ });
38
+ });
39
+ waitsFor(function () {
40
+ return annotationId !== undefined;
41
+ });
42
+ girderTest.waitForLoad();
43
+ runs(function () {
44
+ result.annotation = new largeImageAnnotation.models.AnnotationModel({
45
+ _id: annotationId
46
+ });
47
+ result.fetched = false;
48
+ result.annotation.fetch().then(function () {
49
+ result.fetched = true;
50
+ return null;
51
+ });
52
+ });
53
+ waitsFor(function () {
54
+ return result.fetched;
55
+ });
56
+ }
57
+
58
+ function rectangleElement(x, y) {
59
+ return {type: 'rectangle', center: [x, y, 0], width: 4, height: 4};
60
+ }
61
+
62
+ describe('setup', function () {
63
+ it('login', function () {
64
+ huiTest.login();
65
+ });
66
+
67
+ it('open image', function () {
68
+ huiTest.openImage('image');
69
+ });
70
+
71
+ it('access plugin namespaces', function () {
72
+ girder = window.girder;
73
+ largeImageAnnotation = girder.plugins.large_image_annotation;
74
+ histomicsUI = girder.plugins.histomicsui;
75
+ });
76
+ });
77
+
78
+ describe('#1/#2: no restriction (missing or empty allowed_groups)', function () {
79
+ var result = {};
80
+ var drawWidget;
81
+
82
+ it('creates an annotation with no allowed_groups key', function () {
83
+ createAnnotation('unrestricted annotation', {}, [rectangleElement(0, 0)], result);
84
+ });
85
+
86
+ it('offers every existing group in the Draw panel dropdown', function () {
87
+ var bodyView = huiTest.app.bodyView;
88
+ bodyView.annotations.add(result.annotation);
89
+ bodyView._editAnnotation(result.annotation);
90
+ drawWidget = bodyView.drawWidget;
91
+ waitsFor(function () {
92
+ return !!drawWidget._groups.length;
93
+ });
94
+ runs(function () {
95
+ expect(drawWidget._getAllowedGroups()).toBe(null);
96
+ var expectedIds = drawWidget._groups.map(function (m) { return m.id; }).sort();
97
+ var values = drawWidget.$('.h-style-group option').map(function () {
98
+ return this.value;
99
+ }).get().sort();
100
+ expect(values).toEqual(expectedIds);
101
+ });
102
+ });
103
+
104
+ it('offers every existing group in the context menu', function () {
105
+ var bodyView = huiTest.app.bodyView;
106
+ var element = result.annotation.elements().first();
107
+ bodyView._resetSelection();
108
+ bodyView._selectElement(element);
109
+ var groups = bodyView.contextMenu._getAnnotationGroups();
110
+ var expectedIds = bodyView.contextMenu.styles.map(function (m) { return m.id; });
111
+ expect(groups.sort()).toEqual(expectedIds.sort());
112
+ });
113
+
114
+ it('treats an empty allowed_groups list the same as no restriction', function () {
115
+ result.annotation.get('annotation').attributes = {allowed_groups: []};
116
+ expect(drawWidget._getAllowedGroups()).toBe(null);
117
+ result.annotation.get('annotation').attributes = {allowed_groups: 'not-an-array'};
118
+ expect(drawWidget._getAllowedGroups()).toBe(null);
119
+ });
120
+
121
+ it('accepts a JSON-stringified array as allowed_groups', function () {
122
+ result.annotation.get('annotation').attributes = {
123
+ allowed_groups: '["groupA", "groupB"]'
124
+ };
125
+ expect(drawWidget._getAllowedGroups()).toEqual(['groupA', 'groupB']);
126
+ result.annotation.get('annotation').attributes = {allowed_groups: '[]'};
127
+ expect(drawWidget._getAllowedGroups()).toBe(null);
128
+ });
129
+ });
130
+
131
+ describe('#3/#4: restricted annotation, including auto-created groups', function () {
132
+ var result = {};
133
+ var drawWidget;
134
+ var defaultStyle;
135
+
136
+ it('records the default group style for comparison', function () {
137
+ // StyleModel has no url/urlRoot; styles are only persisted via
138
+ // backbone.localStorage, which is patched onto the *collection*. So a
139
+ // bare model must be fetched through a StyleCollection, not directly.
140
+ var styles = new histomicsUI.collections.StyleCollection();
141
+ var fetched = false;
142
+ runs(function () {
143
+ styles.fetch().always(function () {
144
+ fetched = true;
145
+ });
146
+ });
147
+ waitsFor(function () {
148
+ return fetched;
149
+ });
150
+ runs(function () {
151
+ defaultStyle = styles.get('default');
152
+ });
153
+ });
154
+
155
+ it('creates an annotation restricted to a mix of missing groups', function () {
156
+ createAnnotation('restricted annotation', {
157
+ allowed_groups: ['groupA', 'groupB']
158
+ }, [rectangleElement(10, 10)], result);
159
+ });
160
+
161
+ it('#4a/#4b: auto-creates every missing allowed group using the default style', function () {
162
+ var bodyView = huiTest.app.bodyView;
163
+ bodyView.annotations.add(result.annotation);
164
+ bodyView._editAnnotation(result.annotation);
165
+ drawWidget = bodyView.drawWidget;
166
+ waitsFor(function () {
167
+ return drawWidget._groups.has('groupA') && drawWidget._groups.has('groupB');
168
+ });
169
+ runs(function () {
170
+ ['groupA', 'groupB'].forEach(function (groupId) {
171
+ var created = drawWidget._groups.get(groupId).toJSON();
172
+ expect(created.fillColor).toBe(defaultStyle.get('fillColor'));
173
+ expect(created.lineColor).toBe(defaultStyle.get('lineColor'));
174
+ expect(created.lineWidth).toBe(defaultStyle.get('lineWidth'));
175
+ expect(created.pattern).toBe(defaultStyle.get('pattern'));
176
+ });
177
+ // the active style switches to the first allowed group
178
+ expect(drawWidget._style.id).toBe('groupA');
179
+ });
180
+ });
181
+
182
+ it('#3/#4c: restriction is immediately reflected in the Draw panel dropdown', function () {
183
+ var values = drawWidget.$('.h-style-group option').map(function () {
184
+ return this.value;
185
+ }).get();
186
+ expect(values).toEqual(['groupA', 'groupB']);
187
+ });
188
+
189
+ it('#3/#4c: restriction is immediately reflected in the context menu', function () {
190
+ var bodyView = huiTest.app.bodyView;
191
+ // select an element of the restricted annotation so the context menu's group
192
+ // list reflects it, rather than a stale selection left over from a previous
193
+ // describe block
194
+ var element = result.annotation.elements().first();
195
+ bodyView._resetSelection();
196
+ bodyView._selectElement(element);
197
+
198
+ var groups = bodyView.contextMenu._getAnnotationGroups();
199
+ expect(groups.sort()).toEqual(['groupA', 'groupB']);
200
+ });
201
+ });
202
+
203
+ describe('#5: context menu ignores active selection, uses the clicked annotation', function () {
204
+ var restricted = {};
205
+ var unrestricted = {};
206
+
207
+ it('creates a restricted and an unrestricted annotation', function () {
208
+ createAnnotation('restricted annotation for #5', {
209
+ allowed_groups: ['groupE', 'groupF']
210
+ }, [rectangleElement(30, 30)], restricted);
211
+ });
212
+
213
+ it('creates the unrestricted annotation', function () {
214
+ createAnnotation('unrestricted annotation for #5', {}, [rectangleElement(40, 40)], unrestricted);
215
+ });
216
+
217
+ it('auto-creates the restricted annotation\'s allowed groups (groupE/groupF) as styles', function () {
218
+ // briefly edit the restricted annotation in the Draw panel to create groupE and
219
+ // groupF before switching the active annotation below.
220
+ var bodyView = huiTest.app.bodyView;
221
+ bodyView.annotations.add(restricted.annotation);
222
+ bodyView._editAnnotation(restricted.annotation);
223
+ waitsFor(function () {
224
+ return !!bodyView.drawWidget &&
225
+ bodyView.drawWidget._groups.has('groupE') &&
226
+ bodyView.drawWidget._groups.has('groupF');
227
+ });
228
+ });
229
+
230
+ it('restricts the context menu to the clicked element\'s annotation regardless of the active annotation/group/shape', function () {
231
+ var bodyView = huiTest.app.bodyView;
232
+
233
+ runs(function () {
234
+ bodyView.annotations.add(unrestricted.annotation);
235
+ bodyView._editAnnotation(unrestricted.annotation);
236
+ });
237
+ waitsFor(function () {
238
+ return !!bodyView.drawWidget && !!bodyView.drawWidget._groups.length;
239
+ });
240
+ runs(function () {
241
+ // pick an arbitrary style group in the Draw panel to prove the context menu
242
+ // restriction is independent of it
243
+ bodyView.drawWidget.setStyleGroupById(bodyView.drawWidget._groups.first().id);
244
+
245
+ var element = restricted.annotation.elements().first();
246
+ bodyView._resetSelection();
247
+ bodyView._selectElement(element);
248
+
249
+ var groups = bodyView.contextMenu._getAnnotationGroups();
250
+ expect(groups.sort()).toEqual(['groupE', 'groupF']);
251
+ });
252
+ });
253
+
254
+ it('renders the clicked annotation\'s restriction on selection, not the active annotation\'s (regression)', function () {
255
+ var bodyView = huiTest.app.bodyView;
256
+
257
+ runs(function () {
258
+ // keep the *unrestricted* annotation active in the panel
259
+ bodyView._editAnnotation(unrestricted.annotation);
260
+ });
261
+ waitsFor(function () {
262
+ return !!bodyView.drawWidget && !!bodyView.drawWidget._groups.length;
263
+ });
264
+ runs(function () {
265
+ // Selecting an element fires the context menu's render synchronously.
266
+ // This asserts on the DOM produced by that render, which is the path that
267
+ // regressed. Previously `originalAnnotation` was assigned only after `add`,
268
+ // so the render fell back to the active annotation and offered the wrong
269
+ // groups when the active annotation did not match the selected one.
270
+ var element = restricted.annotation.elements().first();
271
+ bodyView._resetSelection();
272
+ bodyView._selectElement(element);
273
+
274
+ var renderedGroups = bodyView.contextMenu.$('.h-set-group').map(function () {
275
+ return window.$(this).data('group');
276
+ }).get();
277
+ expect(renderedGroups.sort()).toEqual(['groupE', 'groupF']);
278
+ });
279
+ });
280
+ });
281
+
282
+ describe('#3/#5: live metadata updates without reselecting or reloading', function () {
283
+ var result = {};
284
+ var drawWidget;
285
+
286
+ it('creates an annotation with no restriction', function () {
287
+ createAnnotation('live-update annotation', {}, [rectangleElement(50, 50)], result);
288
+ });
289
+
290
+ it('opens the annotation while unrestricted', function () {
291
+ var bodyView = huiTest.app.bodyView;
292
+ bodyView.annotations.add(result.annotation);
293
+ bodyView._editAnnotation(result.annotation);
294
+ drawWidget = bodyView.drawWidget;
295
+ waitsFor(function () {
296
+ return !!drawWidget._groups.length;
297
+ });
298
+ runs(function () {
299
+ expect(drawWidget._getAllowedGroups()).toBe(null);
300
+ });
301
+ });
302
+
303
+ it('immediately restricts the Draw panel and context menu when allowed_groups is added, without reselecting', function () {
304
+ var bodyView = huiTest.app.bodyView;
305
+
306
+ var element = result.annotation.elements().first();
307
+ bodyView._resetSelection();
308
+ bodyView._selectElement(element);
309
+
310
+ // simulate editing the annotation's metadata via the "Edit annotation" dialog,
311
+ // which mutates attributes directly and triggers 'change:annotation' rather than
312
+ // calling .set()
313
+ result.annotation.get('annotation').attributes = {allowed_groups: ['groupG', 'groupH']};
314
+ result.annotation.trigger('change:annotation', result.annotation, {});
315
+
316
+ waitsFor(function () {
317
+ // the Draw panel's `_groups` collection updates synchronously, but its dropdown
318
+ // DOM is refreshed via a debounced render, so also wait for the DOM to catch up
319
+ // before asserting on it
320
+ return drawWidget._groups.has('groupG') && drawWidget._groups.has('groupH') &&
321
+ drawWidget.$('.h-style-group option').length === 2;
322
+ });
323
+ runs(function () {
324
+ expect(drawWidget._style.id).toBe('groupG');
325
+ var drawValues = drawWidget.$('.h-style-group option').map(function () {
326
+ return this.value;
327
+ }).get();
328
+ expect(drawValues).toEqual(['groupG', 'groupH']);
329
+ });
330
+ // the context menu keeps its own StyleCollection, refetched asynchronously via the
331
+ // 'h:styleGroupsEdited' event once the Draw panel auto-creates the newly allowed
332
+ // groups; wait for that refetch to complete before checking it
333
+ waitsFor(function () {
334
+ return bodyView.contextMenu.styles.get('groupG') && bodyView.contextMenu.styles.get('groupH');
335
+ });
336
+ runs(function () {
337
+ var contextGroups = bodyView.contextMenu._getAnnotationGroups();
338
+ expect(contextGroups.sort()).toEqual(['groupG', 'groupH']);
339
+ });
340
+ });
341
+ });
342
+
343
+ describe('#3(consistency): the context menu auto-creates missing allowed groups', function () {
344
+ var restricted = {};
345
+
346
+ it('creates a restricted annotation that is never opened in the Draw panel', function () {
347
+ createAnnotation('context-only restricted annotation', {
348
+ allowed_groups: ['groupK', 'groupL']
349
+ }, [rectangleElement(70, 70)], restricted);
350
+ });
351
+
352
+ it('auto-creates the allowed groups the first time the context menu sees them', function () {
353
+ var bodyView = huiTest.app.bodyView;
354
+ // the groups must not already exist from an earlier spec
355
+ expect(bodyView.contextMenu.styles.get('groupK')).toBe(undefined);
356
+ expect(bodyView.contextMenu.styles.get('groupL')).toBe(undefined);
357
+
358
+ // select an element of the restricted annotation without ever opening it in the
359
+ // Draw panel, so the context menu is the only code path that can create its groups
360
+ var element = restricted.annotation.elements().first();
361
+ bodyView._resetSelection();
362
+ bodyView._selectElement(element);
363
+
364
+ var groups = bodyView.contextMenu._getAnnotationGroups();
365
+ expect(groups.sort()).toEqual(['groupK', 'groupL']);
366
+ expect(bodyView.contextMenu.styles.get('groupK')).toBeTruthy();
367
+ expect(bodyView.contextMenu.styles.get('groupL')).toBeTruthy();
368
+ });
369
+ });
370
+ });
371
+ });
@@ -276,13 +276,21 @@ girderTest.promise.done(function () {
276
276
  return $('.h-elements-container .h-element').length === 2;
277
277
  }, 'point to be created');
278
278
  runs(function () {
279
- expect($('.h-elements-container .h-element:last .h-element-label').text()).toBe('default point 2');
279
+ // Elements are sorted lexically by label, so target the newly drawn point by
280
+ // its label rather than by position.
281
+ expect($('.h-elements-container .h-element .h-element-label').filter(function () {
282
+ return $(this).text() === 'default point 2';
283
+ }).length).toBe(1);
280
284
  });
281
285
  checkAutoSave('drawn 1', 2, annotationInfo);
282
286
  });
283
287
 
284
288
  it('delete the second point', function () {
285
- $('.h-elements-container .h-element:last .h-delete-element').click();
289
+ // Elements are sorted lexically by label, so delete the just drawn point by its
290
+ // label rather than by position.
291
+ $('.h-elements-container .h-element').filter(function () {
292
+ return $(this).find('.h-element-label').text() === 'default point 2';
293
+ }).find('.h-delete-element').click();
286
294
  expect($('.h-elements-container .h-element').length).toBe(1);
287
295
  checkAutoSave('drawn 1', 1, annotationInfo);
288
296
  });
@@ -312,7 +320,11 @@ girderTest.promise.done(function () {
312
320
  return $('.h-elements-container .h-element').length === 2;
313
321
  }, 'point to be created');
314
322
  runs(function () {
315
- expect($('.h-elements-container .h-element:last .h-element-label').text()).toBe('default point 2');
323
+ // Elements are sorted lexically by label, so target the newly drawn point by
324
+ // its label rather than by position.
325
+ expect($('.h-elements-container .h-element .h-element-label').filter(function () {
326
+ return $(this).text() === 'default point 2';
327
+ }).length).toBe(1);
316
328
  });
317
329
  checkAutoSave('drawn 1', 2, annotationInfo);
318
330
  });
@@ -349,7 +361,11 @@ girderTest.promise.done(function () {
349
361
  });
350
362
 
351
363
  it('delete the last point', function () {
352
- $('.h-elements-container .h-element:last .h-delete-element').click();
364
+ // Elements are sorted lexically by label, so delete the just drawn point by its
365
+ // label rather than by position.
366
+ $('.h-elements-container .h-element').filter(function () {
367
+ return $(this).find('.h-element-label').text() === 'default point 2';
368
+ }).find('.h-delete-element').click();
353
369
  expect($('.h-elements-container .h-element').length).toBe(1);
354
370
 
355
371
  // reset the draw state
@@ -609,6 +625,106 @@ girderTest.promise.done(function () {
609
625
  });
610
626
  });
611
627
 
628
+ describe('Sort By', function () {
629
+ var drawWidget;
630
+ var testLabels = ['Alpha', 'Bravo', 'Charlie', 'Delta'];
631
+ var testIds = ['sort-test-alpha', 'sort-test-bravo', 'sort-test-charlie', 'sort-test-delta'];
632
+
633
+ function testElementLabels() {
634
+ return $('.h-elements-container .h-element-label').map(function () {
635
+ return $(this).text();
636
+ }).get().filter(function (text) {
637
+ return testLabels.indexOf(text) !== -1;
638
+ });
639
+ }
640
+
641
+ it('add elements with distinct labels, groups, and shapes', function () {
642
+ runs(function () {
643
+ drawWidget = huiTest.app.bodyView.drawWidget;
644
+ drawWidget.collection.add([
645
+ {id: 'sort-test-charlie', type: 'point', label: {value: 'Charlie'}, group: 'rareGroup'},
646
+ {id: 'sort-test-alpha', type: 'point', label: {value: 'Alpha'}, group: 'commonGroup'},
647
+ {id: 'sort-test-bravo', type: 'rectangle', label: {value: 'Bravo'}, group: 'commonGroup'},
648
+ {id: 'sort-test-delta', type: 'polyline', closed: false, label: {value: 'Delta'}, group: 'commonGroup'}
649
+ ]);
650
+ });
651
+ waitsFor(function () {
652
+ return testElementLabels().length === testLabels.length;
653
+ }, 'test elements to be added');
654
+ });
655
+
656
+ it('defaults to sorting by label, ascending', function () {
657
+ runs(function () {
658
+ expect($('.h-sort-mode').val()).toBe('label');
659
+ expect($('.h-sort-order i').hasClass('icon-sort-alt-up')).toBe(true);
660
+ expect(testElementLabels()).toEqual(['Alpha', 'Bravo', 'Charlie', 'Delta']);
661
+ });
662
+ });
663
+
664
+ it('toggles to descending order', function () {
665
+ runs(function () {
666
+ $('.h-sort-order').click();
667
+ });
668
+ runs(function () {
669
+ expect($('.h-sort-order i').hasClass('icon-sort-alt-down')).toBe(true);
670
+ expect($('.h-sort-order').attr('title')).toMatch(/^Sort descending/);
671
+ expect(testElementLabels()).toEqual(['Delta', 'Charlie', 'Bravo', 'Alpha']);
672
+ });
673
+ });
674
+
675
+ it('toggles back to ascending order', function () {
676
+ runs(function () {
677
+ $('.h-sort-order').click();
678
+ });
679
+ runs(function () {
680
+ expect($('.h-sort-order i').hasClass('icon-sort-alt-up')).toBe(true);
681
+ expect($('.h-sort-order').attr('title')).toMatch(/^Sort ascending/);
682
+ expect(testElementLabels()).toEqual(['Alpha', 'Bravo', 'Charlie', 'Delta']);
683
+ });
684
+ });
685
+
686
+ it('sorts by group', function () {
687
+ runs(function () {
688
+ $('.h-sort-mode').val('group').trigger('change');
689
+ });
690
+ runs(function () {
691
+ var order = testElementLabels();
692
+ // "commonGroup" sorts before "rareGroup" lexically.
693
+ expect(order.indexOf('Charlie')).toBe(order.length - 1);
694
+ });
695
+ });
696
+
697
+ it('sorts by shape', function () {
698
+ runs(function () {
699
+ $('.h-sort-mode').val('shape').trigger('change');
700
+ });
701
+ runs(function () {
702
+ var order = testElementLabels();
703
+ expect(order.indexOf('Delta')).toBeLessThan(order.indexOf('Bravo'));
704
+ });
705
+ });
706
+
707
+ it('sorts by count, rare groups first', function () {
708
+ runs(function () {
709
+ $('.h-sort-mode').val('count').trigger('change');
710
+ });
711
+ runs(function () {
712
+ var order = testElementLabels();
713
+ expect(order[0]).toBe('Charlie');
714
+ });
715
+ });
716
+
717
+ it('removes the test elements and restores label sort', function () {
718
+ runs(function () {
719
+ drawWidget.collection.remove(testIds);
720
+ $('.h-sort-mode').val('label').trigger('change');
721
+ });
722
+ waitsFor(function () {
723
+ return testElementLabels().length === 0;
724
+ }, 'test elements to be removed');
725
+ });
726
+ });
727
+
612
728
  describe('Annotation styles', function () {
613
729
  it('create a new annotation', function () {
614
730
  $('.h-create-annotation').click();
@@ -1812,6 +1928,11 @@ girderTest.promise.done(function () {
1812
1928
  it('open image', function () {
1813
1929
  huiTest.openImage('image.svs', ['subfolder']);
1814
1930
  });
1931
+ it('respects unselected opacity config', function () {
1932
+ girderTest.waitForLoad();
1933
+ expect(huiTest.app.bodyView._unselectedOpacityMultiplier).toBe(0.5);
1934
+ expect(huiTest.app.bodyView.viewerWidget._unselectedOpacityMultiplier).toBe(0.5);
1935
+ });
1815
1936
  it('create a new annotation', function () {
1816
1937
  girderTest.waitForLoad();
1817
1938
  runs(function () {
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "1.8.0",
3
- "distance": 163,
4
- "node": "g257221266a4c1e53190c222b5ef7aa81a30b5a1b",
5
- "dirty": false,
6
- "branch": "girder-5",
7
- "node_date": "2026-07-06"
8
- }