plexora 0.0.1__py3-none-any.whl
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.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- plexora-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* requirementsModal.js -- asking for what a plugin needs and the project lacks.
|
|
3
|
+
*
|
|
4
|
+
* A plugin declares its requirements (plexora/api/plugin.py's Requires); the
|
|
5
|
+
* server works out which are unmet and hands over a list of typed descriptors.
|
|
6
|
+
* This renders a form from that list and posts the answers back, which the
|
|
7
|
+
* server stores on the project -- so the next plugin wanting the same thing
|
|
8
|
+
* never asks again.
|
|
9
|
+
*
|
|
10
|
+
* Core knows nothing about any particular plugin here. Every field is built
|
|
11
|
+
* from a requirement's `kind`, which is why adding a requirement to a plugin
|
|
12
|
+
* needs no change in this file.
|
|
13
|
+
*
|
|
14
|
+
* Three properties the design turns on:
|
|
15
|
+
*
|
|
16
|
+
* - **Nothing already answered is shown.** A requirement the user has answered
|
|
17
|
+
* is absent from the payload rather than rendered as a filled-in field, so
|
|
18
|
+
* they are never asked to re-confirm what they already gave.
|
|
19
|
+
* - **A guess is not an answer.** Most of a conventionally-named table is
|
|
20
|
+
* filled in by the column predictor, and those arrive in `confirm`:
|
|
21
|
+
* prefilled, shown once, and never again once saved. Without this tier the
|
|
22
|
+
* first launch of a tool would run silently on five guesses, which is what a
|
|
23
|
+
* well-named file used to produce.
|
|
24
|
+
* - **It loops.** Attaching a data file makes new questions askable (which
|
|
25
|
+
* column is the cell id — meaningless before there were columns), so after
|
|
26
|
+
* each save it re-asks the server what remains and re-renders. That is why
|
|
27
|
+
* the server reports roles and markers only once a table exists.
|
|
28
|
+
*/
|
|
29
|
+
window.PlexoraRequirements = (function () {
|
|
30
|
+
let openDialog = null;
|
|
31
|
+
|
|
32
|
+
// The image-id select's "only one image" choice. Written as an escape so
|
|
33
|
+
// this file stays plain ASCII: a literal NUL byte makes grep and diff
|
|
34
|
+
// treat the whole source as binary. A leading NUL cannot occur in a real
|
|
35
|
+
// obs column name, so the option can never collide with one -- and the
|
|
36
|
+
// value is client-only regardless, translated into the payload's
|
|
37
|
+
// `single_image` flag rather than sent as it stands.
|
|
38
|
+
const SINGLE_IMAGE = "\u0000single-image";
|
|
39
|
+
|
|
40
|
+
// The cell-id select's "number the rows" choice, on the same terms and for
|
|
41
|
+
// the same reason. It used to be the wording on the blank option -- which
|
|
42
|
+
// made a select nobody touched indistinguishable from one deliberately
|
|
43
|
+
// left on the row number, and is how a project kept a positional cell id
|
|
44
|
+
// while its mask carried the label values from an obs column. Translated
|
|
45
|
+
// into the payload's `row_number_ids` flag, never sent as it stands.
|
|
46
|
+
const ROW_NUMBER = "\u0000row-number";
|
|
47
|
+
|
|
48
|
+
function el(tag, className, text) {
|
|
49
|
+
const node = document.createElement(tag);
|
|
50
|
+
if (className) node.className = className;
|
|
51
|
+
if (text) node.textContent = text;
|
|
52
|
+
return node;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One row of the form: its label on the left, its control on the right.
|
|
57
|
+
*
|
|
58
|
+
* Every field is a row rather than a label stacked over a control, because
|
|
59
|
+
* the form is a short list of "which column is this?" questions and reading
|
|
60
|
+
* them down a single column of labels is how it stays scannable. "Optional"
|
|
61
|
+
* rides on the label for the same reason -- as its own line under the
|
|
62
|
+
* control it doubled the height of every field that had it.
|
|
63
|
+
*/
|
|
64
|
+
function fieldRow(requirement) {
|
|
65
|
+
const field = el("div", "requirement-field");
|
|
66
|
+
const label = el("label", null, requirement.label);
|
|
67
|
+
if (requirement.optional) {
|
|
68
|
+
label.appendChild(el("span", "field-hint", "optional"));
|
|
69
|
+
}
|
|
70
|
+
field.appendChild(label);
|
|
71
|
+
return field;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A mask path: one input, one Browse, nothing to ask about it. */
|
|
75
|
+
function maskField(requirement, state) {
|
|
76
|
+
const field = fieldRow(requirement);
|
|
77
|
+
|
|
78
|
+
const row = el("div", "import-field-row");
|
|
79
|
+
const input = el("input", "form-control");
|
|
80
|
+
input.type = "text";
|
|
81
|
+
input.placeholder = "/path/to/segmentation.ome.tif";
|
|
82
|
+
input.addEventListener("input", () => {
|
|
83
|
+
state.segmentation = input.value.trim();
|
|
84
|
+
});
|
|
85
|
+
row.appendChild(input);
|
|
86
|
+
|
|
87
|
+
const button = el("button", "browse-button", "Browse…");
|
|
88
|
+
button.type = "button";
|
|
89
|
+
if (typeof attachBrowseButton === "function") {
|
|
90
|
+
attachBrowseButton(button, input, { mode: "file", filter: "image" });
|
|
91
|
+
}
|
|
92
|
+
row.appendChild(button);
|
|
93
|
+
|
|
94
|
+
field.appendChild(row);
|
|
95
|
+
return field;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The data file, and whatever the file itself leaves open.
|
|
100
|
+
*
|
|
101
|
+
* Rendered by the shared control (dataSourceField.js) rather than as a
|
|
102
|
+
* bare input, because a path is not always enough to read a file by: a
|
|
103
|
+
* .zarr store with several tables, or a table spanning several images,
|
|
104
|
+
* has to be narrowed before anything can be loaded. This used to be an
|
|
105
|
+
* input and two buttons, so picking such a store posted a path the server
|
|
106
|
+
* could only refuse -- with a message telling the user to choose, and
|
|
107
|
+
* nothing anywhere to choose from.
|
|
108
|
+
*/
|
|
109
|
+
function dataField(requirement, state, controls) {
|
|
110
|
+
const field = fieldRow(requirement);
|
|
111
|
+
// The control grows a row per open question, so the label aligns to
|
|
112
|
+
// the path input rather than to the middle of the stack.
|
|
113
|
+
field.classList.add("requirement-field-stacked");
|
|
114
|
+
const mount = el("div", "requirement-stack");
|
|
115
|
+
field.appendChild(mount);
|
|
116
|
+
controls.data = window.PlexoraDataSourceField.mount(mount, {
|
|
117
|
+
onChange: (value) => {
|
|
118
|
+
state.data = value.data;
|
|
119
|
+
state.table = value.table;
|
|
120
|
+
state.subset_column = value.subset_column;
|
|
121
|
+
state.subset_value = value.subset_value;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
return field;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A select over the columns this project answers role questions with.
|
|
129
|
+
*
|
|
130
|
+
* The list arrives ready-made in `roleColumns` and is used exactly as
|
|
131
|
+
* given: for AnnData and SpatialData it is the source file's `obs`,
|
|
132
|
+
* unfiltered, and which of those columns holds the cell id is not
|
|
133
|
+
* something a name heuristic here gets to narrow down. The prefilled
|
|
134
|
+
* answer is the guess; the list is everything.
|
|
135
|
+
*/
|
|
136
|
+
function roleField(requirement, needs, state) {
|
|
137
|
+
const field = fieldRow(requirement);
|
|
138
|
+
|
|
139
|
+
const select = el("select", "form-select");
|
|
140
|
+
select.append(new Option("Choose a column…", ""));
|
|
141
|
+
|
|
142
|
+
// The answers that name no column. Each is offered as its own option
|
|
143
|
+
// rather than carried by the blank, because a blank that satisfies a
|
|
144
|
+
// question is indistinguishable from one the user never looked at --
|
|
145
|
+
// and both of these are the answer a conventionally-shaped file needs,
|
|
146
|
+
// so the blank was reached by default and read as agreement.
|
|
147
|
+
//
|
|
148
|
+
// "There is only one image" cannot be inferred either: deciding it
|
|
149
|
+
// needs to know which column identifies the image, which is this
|
|
150
|
+
// question. "Number the rows" is right only when the mask's labels run
|
|
151
|
+
// in file order, which nothing here can check.
|
|
152
|
+
const isImageId = requirement.role === "image_id";
|
|
153
|
+
if (isImageId) {
|
|
154
|
+
select.append(new Option("This data has only one image", SINGLE_IMAGE));
|
|
155
|
+
}
|
|
156
|
+
// Present only for the formats whose adapter can number the rows
|
|
157
|
+
// itself; a CSV's cell id is always one of its own columns.
|
|
158
|
+
const fallback = (needs.roleDefaults || {})[requirement.role];
|
|
159
|
+
if (fallback) {
|
|
160
|
+
select.append(new Option(fallback, ROW_NUMBER));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const candidates = needs.roleColumns || [];
|
|
164
|
+
candidates.forEach((name) => select.append(new Option(name, name)));
|
|
165
|
+
|
|
166
|
+
// Prefilled with whatever the project holds, which for a `confirm`
|
|
167
|
+
// requirement is the predictor's guess -- the thing the user is here to
|
|
168
|
+
// look at. Seeded into state too, so accepting it unchanged (no change
|
|
169
|
+
// event) still posts the value rather than clearing the role.
|
|
170
|
+
const current = (needs.roleAnswers || {})[requirement.role] || "";
|
|
171
|
+
if (current && candidates.includes(current)) {
|
|
172
|
+
select.value = current;
|
|
173
|
+
state.roles[requirement.role] = current;
|
|
174
|
+
} else if (isImageId && needs.singleImage) {
|
|
175
|
+
select.value = SINGLE_IMAGE;
|
|
176
|
+
state.single_image = true;
|
|
177
|
+
} else if (fallback && needs.rowNumberIds) {
|
|
178
|
+
select.value = ROW_NUMBER;
|
|
179
|
+
state.row_number_ids = true;
|
|
180
|
+
}
|
|
181
|
+
select.addEventListener("change", () => {
|
|
182
|
+
if (isImageId) {
|
|
183
|
+
// Mutually exclusive: each write clears the other, so the
|
|
184
|
+
// payload never claims one image AND names a column.
|
|
185
|
+
state.single_image = select.value === SINGLE_IMAGE;
|
|
186
|
+
state.roles[requirement.role] =
|
|
187
|
+
select.value === SINGLE_IMAGE ? null : (select.value || null);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (fallback) {
|
|
191
|
+
// Same pairing for the cell id.
|
|
192
|
+
state.row_number_ids = select.value === ROW_NUMBER;
|
|
193
|
+
state.roles[requirement.role] =
|
|
194
|
+
select.value === ROW_NUMBER ? null : (select.value || null);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
state.roles[requirement.role] = select.value || null;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
field.appendChild(select);
|
|
201
|
+
return field;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Where each cell's position is read from.
|
|
206
|
+
*
|
|
207
|
+
* One question, not the two column roles it used to be -- see
|
|
208
|
+
* coordinateField.js for why, and for the shape of the answer. Rendered by
|
|
209
|
+
* the shared control so this surface and the project edit page cannot
|
|
210
|
+
* disagree about what they are asking or what they post back.
|
|
211
|
+
*/
|
|
212
|
+
function coordinatesField(requirement, needs, state) {
|
|
213
|
+
const field = fieldRow(requirement);
|
|
214
|
+
field.classList.add("requirement-field-stacked");
|
|
215
|
+
const mount = el("div", "requirement-stack");
|
|
216
|
+
field.appendChild(mount);
|
|
217
|
+
// Seeded up front, like every other field here: accepting a prefilled
|
|
218
|
+
// value fires no change event, and without this a confirmed-unchanged
|
|
219
|
+
// answer would post nothing and be asked again on the next open.
|
|
220
|
+
const control = window.PlexoraCoordinateField.mount(mount, {
|
|
221
|
+
...(needs.coordinateOptions || {}),
|
|
222
|
+
name: `coordinates-${requirement.key}`,
|
|
223
|
+
onChange: (value) => { state.coordinates = value; },
|
|
224
|
+
});
|
|
225
|
+
state.coordinates = control.value();
|
|
226
|
+
return field;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Which matrix the marker values are read from, and whether to log them.
|
|
231
|
+
*
|
|
232
|
+
* Two controls on one row because they are one decision: "what numbers am
|
|
233
|
+
* I thresholding". A file commonly holds raw counts in `X` and a
|
|
234
|
+
* log-transformed copy in a layer, and nothing about the numbers themselves
|
|
235
|
+
* says which is which -- so a threshold set on the wrong one is not a
|
|
236
|
+
* cosmetic mistake, it is a meaningless number. The log switch covers the
|
|
237
|
+
* other half: a file with only raw counts has no layer to pick, and the
|
|
238
|
+
* values still need transforming before a threshold on them reads sensibly.
|
|
239
|
+
*
|
|
240
|
+
* The select is dropped when the file carries only `X` -- one option is not
|
|
241
|
+
* a choice -- but the switch stays, because it is still a real question.
|
|
242
|
+
*/
|
|
243
|
+
function featuresField(requirement, needs, state) {
|
|
244
|
+
const field = fieldRow(requirement);
|
|
245
|
+
const row = el("div", "requirement-inline");
|
|
246
|
+
|
|
247
|
+
const options = needs.featureOptions || [];
|
|
248
|
+
// Seeded into state up front: accepting what is shown fires no change
|
|
249
|
+
// event, and the answer still has to reach the server.
|
|
250
|
+
state.features_layer = needs.featureSource || "X";
|
|
251
|
+
if (options.length > 1) {
|
|
252
|
+
const select = el("select", "form-select");
|
|
253
|
+
options.forEach((option) => select.append(new Option(option.label, option.value)));
|
|
254
|
+
select.value = state.features_layer;
|
|
255
|
+
select.addEventListener("change", () => {
|
|
256
|
+
state.features_layer = select.value;
|
|
257
|
+
});
|
|
258
|
+
row.appendChild(select);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const toggle = el("label", "requirement-inline-check");
|
|
262
|
+
const box = el("input");
|
|
263
|
+
box.type = "checkbox";
|
|
264
|
+
box.checked = Boolean(needs.featureLog);
|
|
265
|
+
box.title = "Apply log1p to the values as they are read.";
|
|
266
|
+
state.features_log = box.checked;
|
|
267
|
+
box.addEventListener("change", () => {
|
|
268
|
+
state.features_log = box.checked;
|
|
269
|
+
});
|
|
270
|
+
toggle.append(box, el("span", null, "log1p"));
|
|
271
|
+
row.appendChild(toggle);
|
|
272
|
+
|
|
273
|
+
field.appendChild(row);
|
|
274
|
+
return field;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The marker/metadata split -- and only that.
|
|
279
|
+
*
|
|
280
|
+
* The classifier can also carry the role selects (the edit page uses it
|
|
281
|
+
* that way), but not here: this form already renders a field per role the
|
|
282
|
+
* plugin asked for, so handing it labels drew every one of them a second
|
|
283
|
+
* time inside the metadata box. It also drew the ones nothing asked for --
|
|
284
|
+
* a CSV import showed a "Cell type column" select that no installed plugin
|
|
285
|
+
* reads and the user had no reason to answer.
|
|
286
|
+
*/
|
|
287
|
+
function classificationField(requirement, needs, state) {
|
|
288
|
+
const field = el("div", "requirement-field requirement-field-wide");
|
|
289
|
+
field.appendChild(el("label", null, requirement.label));
|
|
290
|
+
const mount = el("div");
|
|
291
|
+
field.appendChild(mount);
|
|
292
|
+
|
|
293
|
+
const classifier = PlexoraColumnClassifier.mount(mount, {
|
|
294
|
+
markers: (needs.columns || {}).markers || [],
|
|
295
|
+
metadata: (needs.columns || {}).metadata || [],
|
|
296
|
+
roleLabels: {},
|
|
297
|
+
onChange: () => {
|
|
298
|
+
const value = classifier.value();
|
|
299
|
+
state.columns = { markers: value.markers, metadata: value.metadata };
|
|
300
|
+
Object.assign(state.roles, value.roles);
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
// Seed immediately: a user who accepts the prediction unchanged never
|
|
304
|
+
// fires onChange, and the form would otherwise post nothing.
|
|
305
|
+
const initial = classifier.value();
|
|
306
|
+
state.columns = { markers: initial.markers, metadata: initial.metadata };
|
|
307
|
+
Object.assign(state.roles, initial.roles);
|
|
308
|
+
return field;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Draw the form, and hand back the controls the save step has to consult.
|
|
313
|
+
*
|
|
314
|
+
* Only the data field is in there: it is the one control whose answer is
|
|
315
|
+
* not complete the moment it is typed (see dataSourceField.js), so saving
|
|
316
|
+
* has to ask it whether it is ready. Kept out of `state`, which is posted
|
|
317
|
+
* as it stands.
|
|
318
|
+
*/
|
|
319
|
+
function render(dialog, needs, state) {
|
|
320
|
+
const controls = {};
|
|
321
|
+
const body = dialog.querySelector(".requirements-body");
|
|
322
|
+
body.replaceChildren();
|
|
323
|
+
|
|
324
|
+
// Missing first, then the prefilled ones, then what is merely offered:
|
|
325
|
+
// the empty fields are what block the tool, so they lead.
|
|
326
|
+
const all = [...(needs.missing || []), ...(needs.confirm || []),
|
|
327
|
+
...(needs.optional || [])];
|
|
328
|
+
// Every key rendered is a key the user has now been asked about, blank
|
|
329
|
+
// answer included -- that is what stops an optional field they chose to
|
|
330
|
+
// skip from reappearing on every open.
|
|
331
|
+
state.confirm = all.map((requirement) => requirement.key);
|
|
332
|
+
|
|
333
|
+
all.forEach((requirement) => {
|
|
334
|
+
if (requirement.kind === "data") {
|
|
335
|
+
body.appendChild(dataField(requirement, state, controls));
|
|
336
|
+
} else if (requirement.kind === "segmentation") {
|
|
337
|
+
body.appendChild(maskField(requirement, state));
|
|
338
|
+
} else if (requirement.kind === "role") {
|
|
339
|
+
body.appendChild(roleField(requirement, needs, state));
|
|
340
|
+
} else if (requirement.kind === "coordinates") {
|
|
341
|
+
body.appendChild(coordinatesField(requirement, needs, state));
|
|
342
|
+
} else if (requirement.kind === "features") {
|
|
343
|
+
body.appendChild(featuresField(requirement, needs, state));
|
|
344
|
+
} else if (requirement.kind === "classification") {
|
|
345
|
+
body.appendChild(classificationField(requirement, needs, state));
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const blocking = (needs.missing || []).length;
|
|
350
|
+
dialog.querySelector(".requirements-title").textContent =
|
|
351
|
+
blocking ? "Before this tool can open" : `Set up ${needs.label}`;
|
|
352
|
+
// Three cases, not two. Blocking is core's to word. A prefilled form is
|
|
353
|
+
// core's too -- "we guessed, check it" is true of every plugin. A form
|
|
354
|
+
// made entirely of things nobody has to fill in is the one core cannot
|
|
355
|
+
// word: the generic line claims Plexora filled them in from the data,
|
|
356
|
+
// and for a plugin whose fields are all optional nothing was filled in
|
|
357
|
+
// and nothing is required, so the sentence is false twice over. That
|
|
358
|
+
// one comes from the plugin (Plugin.intro).
|
|
359
|
+
const optionalOnly = !blocking && !(needs.confirm || []).length;
|
|
360
|
+
dialog.querySelector(".requirements-subtitle").textContent = blocking
|
|
361
|
+
? `${needs.label} needs a little more about this project.`
|
|
362
|
+
: (optionalOnly && needs.intro)
|
|
363
|
+
? needs.intro
|
|
364
|
+
: `Check what ${needs.label} will use. Plexora filled these in from the data — you will not be asked again.`;
|
|
365
|
+
|
|
366
|
+
// On a form where nothing is required, "Cancel" is the wrong word for
|
|
367
|
+
// the only button that gets you to the tool. It reads as "do not open
|
|
368
|
+
// this", and taking it literally would leave a plugin that requires
|
|
369
|
+
// nothing permanently unopenable: the caller re-enters on a true
|
|
370
|
+
// result, so declining has to be RECORDED, not just obeyed. `data-skip`
|
|
371
|
+
// routes the button through the same save as Continue, with whatever
|
|
372
|
+
// the user did not fill in left blank -- which is what marks the offer
|
|
373
|
+
// answered and stops it coming back on every open.
|
|
374
|
+
const cancel = dialog.querySelector('[data-action="cancel"]');
|
|
375
|
+
cancel.dataset.skip = optionalOnly ? "true" : "false";
|
|
376
|
+
cancel.textContent = optionalOnly ? "Skip" : "Cancel";
|
|
377
|
+
|
|
378
|
+
return controls;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function buildDialog() {
|
|
382
|
+
const dialog = document.createElement("dialog");
|
|
383
|
+
dialog.className = "requirements-modal";
|
|
384
|
+
dialog.innerHTML = `
|
|
385
|
+
<form method="dialog" class="requirements-form">
|
|
386
|
+
<h2 class="requirements-title">Before this tool can open</h2>
|
|
387
|
+
<p class="requirements-subtitle"></p>
|
|
388
|
+
<div class="requirements-body"></div>
|
|
389
|
+
<div class="requirements-error" role="alert" hidden></div>
|
|
390
|
+
<div class="requirements-actions">
|
|
391
|
+
<button type="button" class="btn btn-secondary" data-action="cancel">Cancel</button>
|
|
392
|
+
<button type="button" class="btn btn-primary" data-action="save">Continue</button>
|
|
393
|
+
</div>
|
|
394
|
+
</form>
|
|
395
|
+
`;
|
|
396
|
+
document.body.appendChild(dialog);
|
|
397
|
+
return dialog;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Ask for everything `needs` reports missing; resolve true once the tool
|
|
402
|
+
* can open.
|
|
403
|
+
*
|
|
404
|
+
* @param datasource the project name
|
|
405
|
+
* @param needs the payload from tool_routes' `_needs()`
|
|
406
|
+
* @returns Promise<boolean> — false if the user cancelled
|
|
407
|
+
*/
|
|
408
|
+
function collect(datasource, needs) {
|
|
409
|
+
if (openDialog) openDialog.remove();
|
|
410
|
+
const dialog = openDialog = buildDialog();
|
|
411
|
+
const error = dialog.querySelector(".requirements-error");
|
|
412
|
+
const save = dialog.querySelector('[data-action="save"]');
|
|
413
|
+
let state = { roles: {} };
|
|
414
|
+
let controls = render(dialog, needs, state);
|
|
415
|
+
dialog.showModal();
|
|
416
|
+
|
|
417
|
+
return new Promise((resolve) => {
|
|
418
|
+
function close(result) {
|
|
419
|
+
dialog.close();
|
|
420
|
+
dialog.remove();
|
|
421
|
+
if (openDialog === dialog) openDialog = null;
|
|
422
|
+
resolve(result);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const cancel = dialog.querySelector('[data-action="cancel"]');
|
|
426
|
+
cancel.addEventListener("click", () => {
|
|
427
|
+
// "Skip" on an all-optional form is an empty Continue: it saves
|
|
428
|
+
// nothing but the list of keys the form showed, which is what
|
|
429
|
+
// records the offer as declined. See render().
|
|
430
|
+
if (cancel.dataset.skip === "true") return submit();
|
|
431
|
+
close(false);
|
|
432
|
+
});
|
|
433
|
+
// Escape stays a plain way out, on every form. It leaves the offer
|
|
434
|
+
// standing for next time, which is the right reading of a keypress
|
|
435
|
+
// that might have been aimed at something else -- and it is safe
|
|
436
|
+
// because a false result means the caller does not re-enter.
|
|
437
|
+
dialog.addEventListener("cancel", () => close(false));
|
|
438
|
+
|
|
439
|
+
save.addEventListener("click", () => submit());
|
|
440
|
+
|
|
441
|
+
async function submit() {
|
|
442
|
+
save.disabled = true;
|
|
443
|
+
cancel.disabled = true;
|
|
444
|
+
error.hidden = true;
|
|
445
|
+
// A data file the user has not finished describing -- a store
|
|
446
|
+
// whose table is still unpicked -- is not something to post.
|
|
447
|
+
// The server would reject it with a message asking for a
|
|
448
|
+
// choice, which reads as nonsense when the control offering
|
|
449
|
+
// that choice is on screen and untouched.
|
|
450
|
+
const waiting = controls.data?.blocking?.();
|
|
451
|
+
if (waiting) {
|
|
452
|
+
error.textContent = waiting;
|
|
453
|
+
error.hidden = false;
|
|
454
|
+
save.disabled = false;
|
|
455
|
+
cancel.disabled = false;
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const task = window.PlexoraStatus?.begin("Saving");
|
|
459
|
+
try {
|
|
460
|
+
const payload = { tool: needs.tool, ...state };
|
|
461
|
+
const response = await fetch(
|
|
462
|
+
plexoraUrl(`${encodeURIComponent(datasource)}/requirements`),
|
|
463
|
+
{
|
|
464
|
+
method: "POST",
|
|
465
|
+
headers: { "Content-Type": "application/json" },
|
|
466
|
+
body: JSON.stringify(payload),
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
const result = await response.json();
|
|
470
|
+
if (!response.ok || !result.success) {
|
|
471
|
+
throw new Error(result.error || "Could not save");
|
|
472
|
+
}
|
|
473
|
+
task?.done();
|
|
474
|
+
|
|
475
|
+
// An answer that changed what gets read means the page is
|
|
476
|
+
// holding the previous table's statistics, and the tool
|
|
477
|
+
// about to open is drawn from them. Re-fetch before handing
|
|
478
|
+
// back, so the caller never renders a panel of the numbers
|
|
479
|
+
// the user just stopped reading.
|
|
480
|
+
if (result.reloaded) {
|
|
481
|
+
await window.__plexora?.refreshDataset?.();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Both lists, because they are different questions and the
|
|
485
|
+
// second one only becomes askable once the first is
|
|
486
|
+
// answered: naming a data file is what makes "which column
|
|
487
|
+
// is the cell id" a question with answers, and for a plugin
|
|
488
|
+
// that merely OFFERS that column it arrives as optional
|
|
489
|
+
// rather than missing. Closing on `stillMissing` alone shut
|
|
490
|
+
// the form the instant the file was attached, one question
|
|
491
|
+
// short of the point.
|
|
492
|
+
if (!(result.stillMissing || []).length
|
|
493
|
+
&& !(result.stillOptional || []).length) {
|
|
494
|
+
return close(true);
|
|
495
|
+
}
|
|
496
|
+
// Answering one question can reveal others -- naming the
|
|
497
|
+
// data file is what makes "which column is the cell id"
|
|
498
|
+
// answerable. Re-ask rather than guessing what is next.
|
|
499
|
+
needs = await fetchNeeds(datasource, needs.tool);
|
|
500
|
+
if (!needs) return close(true);
|
|
501
|
+
state = { roles: {} };
|
|
502
|
+
controls = render(dialog, needs, state);
|
|
503
|
+
// Say why the form came back. Leaving a blocking select on
|
|
504
|
+
// "Choose a column…" lands here, and re-rendering the same
|
|
505
|
+
// fields with nothing said reads as the button doing
|
|
506
|
+
// nothing at all.
|
|
507
|
+
const open = (needs.missing || []).map((r) => r.label);
|
|
508
|
+
if (open.length) {
|
|
509
|
+
error.textContent = `Still needed: ${open.join(", ")}.`;
|
|
510
|
+
error.hidden = false;
|
|
511
|
+
}
|
|
512
|
+
save.disabled = false;
|
|
513
|
+
cancel.disabled = false;
|
|
514
|
+
} catch (e) {
|
|
515
|
+
task?.fail("Save failed");
|
|
516
|
+
error.textContent = e.message;
|
|
517
|
+
error.hidden = false;
|
|
518
|
+
save.disabled = false;
|
|
519
|
+
cancel.disabled = false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** The tool's outstanding requirements, or null when it can just open. */
|
|
526
|
+
async function fetchNeeds(datasource, tool) {
|
|
527
|
+
const response = await fetch(plexoraUrl(
|
|
528
|
+
`${encodeURIComponent(datasource)}/tools/${encodeURIComponent(tool)}/requirements`));
|
|
529
|
+
const payload = await response.json();
|
|
530
|
+
if (!payload.success) return null;
|
|
531
|
+
// `optional` counts. It is the only list a plugin that requires nothing
|
|
532
|
+
// ever fills, so leaving it out made "outstanding" mean "blocking" and
|
|
533
|
+
// handed back null the moment a data file was attached -- closing the
|
|
534
|
+
// form on the pass that was about to ask which column holds the cell id.
|
|
535
|
+
const outstanding = (payload.missing || []).length
|
|
536
|
+
+ (payload.confirm || []).length
|
|
537
|
+
+ (payload.optional || []).length;
|
|
538
|
+
return outstanding ? payload : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Ask for named requirements mid-session, after a tool is already open.
|
|
543
|
+
*
|
|
544
|
+
* Gating uses this: an image-id column only matters when the user chooses
|
|
545
|
+
* to write gates back to the source file, long after the panel opened.
|
|
546
|
+
* Resolves true once the named keys are satisfied.
|
|
547
|
+
*/
|
|
548
|
+
async function require(datasource, tool, keys) {
|
|
549
|
+
// `keys` asks a different question from the ordinary lists: not "what
|
|
550
|
+
// has the user not been asked yet" but "what does this action need
|
|
551
|
+
// right now". An optional field they were offered and skipped is
|
|
552
|
+
// absent from the first and present in the second, which is the whole
|
|
553
|
+
// reason this takes the explicit route rather than filtering `optional`.
|
|
554
|
+
const url = `${encodeURIComponent(datasource)}/tools/${encodeURIComponent(tool)}`
|
|
555
|
+
+ `/requirements?keys=${encodeURIComponent(keys.join(","))}`;
|
|
556
|
+
const full = await fetch(plexoraUrl(url)).then((r) => r.json());
|
|
557
|
+
if (!full.success) return false;
|
|
558
|
+
|
|
559
|
+
const wanted = full.requested || [];
|
|
560
|
+
if (!wanted.length) return true;
|
|
561
|
+
// Shown as required here even for an optional input: the caller cannot
|
|
562
|
+
// do what the user just asked for without it.
|
|
563
|
+
return collect(datasource, {
|
|
564
|
+
...full,
|
|
565
|
+
missing: wanted.map((r) => ({ ...r, optional: false })),
|
|
566
|
+
confirm: [],
|
|
567
|
+
optional: [],
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return { collect, require };
|
|
572
|
+
})();
|