mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge remote-tracking branch 'upstream/dev' into ts-config-tweaks
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
name: Deployment Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: 'Galaxy Deployment to target'
|
||||
required: true
|
||||
default: 'usegalaxymain'
|
||||
type: choice
|
||||
options:
|
||||
- usegalaxytest
|
||||
- usegalaxymain
|
||||
- usegalaxyeu
|
||||
type:
|
||||
description: 'Test type'
|
||||
required: true
|
||||
default: 'all'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- api
|
||||
- selenium
|
||||
branch:
|
||||
description: 'Branch of code to run from'
|
||||
default: 'dev'
|
||||
type: string
|
||||
debug:
|
||||
required: true
|
||||
description: 'Run deployment tests with debug mode on'
|
||||
type: boolean
|
||||
jobs:
|
||||
testdeployment:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.7']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ inputs.branch }}
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Cache pip dir
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
|
||||
- uses: nanasess/setup-chromedriver@v1
|
||||
- name: Run tests
|
||||
run: bash ./test/deployment/usegalaxystar.bash
|
||||
env:
|
||||
GALAXY_TEST_DEPLOYMENT_TARGET: ${{ inputs.target }}
|
||||
GALAXY_TEST_DEPLOYMENT_DEBUG: ${{ inputs.debug }}
|
||||
GALAXY_TEST_DEPLOYMENT_TEST_TYPE: ${{ inputs.type }}
|
||||
GALAXY_TEST_USEGALAXYMAIN_USER_EMAIL: "jmchilton+test@gmail.com"
|
||||
GALAXY_TEST_USEGALAXYMAIN_USER_PASSWORD: ${{ secrets.USEGALAXYMAIN_USER_PASSWORD }}
|
||||
GALAXY_TEST_USEGALAXYMAIN_USER_KEY: ${{ secrets.USEGALAXYMAIN_USER_KEY }}
|
||||
GALAXY_TEST_USEGALAXYTEST_USER_EMAIL: "jmchilton+test@gmail.com"
|
||||
GALAXY_TEST_USEGALAXYTEST_USER_PASSWORD: ${{ secrets.USEGALAXYTEST_USER_PASSWORD }}
|
||||
GALAXY_TEST_USEGALAXYTEST_USER_KEY: ${{ secrets.USEGALAXYTEST_USER_KEY }}
|
||||
GALAXY_TEST_USEGALAXYEU_USER_EMAIL: "jmchilton+test@gmail.com"
|
||||
GALAXY_TEST_USEGALAXYEU_USER_PASSWORD: ${{ secrets.USEGALAXYEU_USER_PASSWORD }}
|
||||
GALAXY_TEST_USEGALAXYEU_USER_KEY: ${{ secrets.USEGALAXYEU_USER_KEY }}
|
||||
GALAXY_TEST_TIMEOUT_MULTIPLIER: 10
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: Deployment test results (${{ inputs.target }}, ${{ inputs.type }})
|
||||
path: 'deployment_tests.html'
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
if: github.repository_owner == 'galaxyproject'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MILESTONE_NUMBER: 23
|
||||
MILESTONE_NUMBER: 24
|
||||
steps:
|
||||
- name: Get latest pull request labels
|
||||
id: get_pr_labels
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
GALAXY_TEST_TOOL_CONF="lib/galaxy/config/sample/tool_conf.xml.sample,test/functional/tools/samples_tool_conf.xml"
|
||||
GALAXY_TEST_TOOL_CONF="lib/galaxy/config/sample/tool_conf.xml.sample,test/functional/tools/sample_tool_conf.xml"
|
||||
|
||||
Vendored
+1
-1
@@ -10,7 +10,7 @@
|
||||
"env": {
|
||||
"GALAXY_CONFIG_FILE": "${workspaceFolder}/config/galaxy.yml",
|
||||
"GALAXY_CONDA_AUTO_INIT": "false",
|
||||
"GALAXY_CONFIG_TOOL_CONFIG_FILE": "lib/galaxy/config/sample/tool_conf.xml.sample,test/functional/tools/samples_tool_conf.xml",
|
||||
"GALAXY_CONFIG_TOOL_CONFIG_FILE": "lib/galaxy/config/sample/tool_conf.xml.sample,test/functional/tools/sample_tool_conf.xml",
|
||||
"GALAXY_CONFIG_DATABASE_CONNECTION": "postgresql://localhost/galaxy"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
VENV?=.venv
|
||||
# Source virtualenv to execute command (darker, sphinx, twine, etc...)
|
||||
IN_VENV=if [ -f "$(VENV)/bin/activate" ]; then . "$(VENV)/bin/activate"; fi;
|
||||
RELEASE_CURR:=23.0
|
||||
RELEASE_CURR:=23.1
|
||||
RELEASE_UPSTREAM:=upstream
|
||||
CONFIG_MANAGE=$(IN_VENV) python lib/galaxy/config/config_manage.py
|
||||
PROJECT_URL?=https://github.com/galaxyproject/galaxy
|
||||
@@ -162,6 +162,14 @@ skip-client: ## Run only the server, skipping the client build.
|
||||
GALAXY_SKIP_CLIENT_BUILD=1 sh run.sh
|
||||
|
||||
node-deps: ## Install NodeJS dependencies.
|
||||
ifndef YARN
|
||||
@echo "Could not find yarn, which is required to install the Galaxy client.\nTo install yarn, please visit \033[0;34mhttps://yarnpkg.com/en/docs/install\033[0m for instructions, and package information for all platforms.\n"
|
||||
false;
|
||||
else
|
||||
yarn install $(YARN_INSTALL_OPTS)
|
||||
endif
|
||||
|
||||
client-node-deps: ## Install NodeJS dependencies for the client.
|
||||
ifndef YARN
|
||||
@echo "Could not find yarn, which is required to build the Galaxy client.\nTo install yarn, please visit \033[0;34mhttps://yarnpkg.com/en/docs/install\033[0m for instructions, and package information for all platforms.\n"
|
||||
false;
|
||||
@@ -176,7 +184,7 @@ build-api-schema:
|
||||
remove-api-schema:
|
||||
rm _schema.yaml
|
||||
|
||||
update-client-api-schema: node-deps build-api-schema
|
||||
update-client-api-schema: client-node-deps build-api-schema
|
||||
$(IN_VENV) cd client && node openapi_to_schema.mjs ../_schema.yaml > src/schema/schema.ts && npx prettier --write src/schema/schema.ts
|
||||
$(MAKE) remove-api-schema
|
||||
|
||||
@@ -185,34 +193,34 @@ lint-api-schema: build-api-schema
|
||||
$(IN_VENV) codespell -I .ci/ignore-spelling.txt _schema.yaml
|
||||
$(MAKE) remove-api-schema
|
||||
|
||||
client: node-deps ## Rebuild client-side artifacts for local development.
|
||||
install-client: node-deps ## Install prebuilt client as defined in root package.json
|
||||
yarn install && yarn run stage
|
||||
|
||||
client: client-node-deps ## Rebuild client-side artifacts for local development.
|
||||
cd client && yarn run build
|
||||
|
||||
client-production: node-deps ## Rebuild client-side artifacts for a production deployment without sourcemaps.
|
||||
client-production: client-node-deps ## Rebuild client-side artifacts for a production deployment without sourcemaps.
|
||||
cd client && yarn run build-production
|
||||
|
||||
client-production-maps: node-deps ## Rebuild client-side artifacts for a production deployment with sourcemaps.
|
||||
client-production-maps: client-node-deps ## Rebuild client-side artifacts for a production deployment with sourcemaps.
|
||||
cd client && yarn run build-production-maps
|
||||
|
||||
client-format: node-deps ## Reformat client code
|
||||
client-format: client-node-deps ## Reformat client code
|
||||
cd client && yarn run format
|
||||
|
||||
client-watch: node-deps ## A useful target for parallel development building. See also client-dev-server.
|
||||
cd client && yarn run watch
|
||||
client-dev-server: client-node-deps ## Starts a webpack dev server for client development (HMR enabled)
|
||||
cd client && yarn run develop
|
||||
|
||||
client-dev-server: node-deps ## Starts a webpack dev server for client development (HMR enabled)
|
||||
cd client && yarn run serve
|
||||
|
||||
client-test: node-deps ## Run JS unit tests
|
||||
client-test: client-node-deps ## Run JS unit tests
|
||||
cd client && yarn run test
|
||||
|
||||
client-eslint-precommit: node-deps # Client linting for pre-commit hook; skips glob input and takes specific paths
|
||||
client-eslint-precommit: client-node-deps # Client linting for pre-commit hook; skips glob input and takes specific paths
|
||||
cd client && yarn run eslint-precommit
|
||||
|
||||
client-eslint: node-deps # Run client linting
|
||||
client-eslint: client-node-deps # Run client linting
|
||||
cd client && yarn run eslint
|
||||
|
||||
client-format-check: node-deps # Run client formatting check
|
||||
client-format-check: client-node-deps # Run client formatting check
|
||||
cd client && yarn run format-check
|
||||
|
||||
client-lint: client-eslint client-format-check ## ES lint and check format of client
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"vuejs-accessibility/no-autofocus": "error",
|
||||
"vuejs-accessibility/tabindex-no-positive": "error"
|
||||
},
|
||||
"ignorePatterns": ["src/qunit", "src/mocha", "src/libs", "src/nls", "src/legacy"],
|
||||
"ignorePatterns": ["dist", "src/qunit", "src/mocha", "src/libs", "src/nls", "src/legacy"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.vue"],
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Standard ignores
|
||||
**/.git
|
||||
**/.svn
|
||||
**/.hg
|
||||
**/node_modules
|
||||
# Ignore the dist folder
|
||||
**/dist
|
||||
# We will always defer to package manager for formatting of package.json files.
|
||||
**/package.json
|
||||
# Skip formatting of explicitly included libs (to stay in sync with upstream, and they should go away eventually)
|
||||
src/libs
|
||||
# Don't reformat .json or .yml files included in the project
|
||||
*.json
|
||||
*.yml
|
||||
+47
-50
@@ -1,19 +1,19 @@
|
||||
# Client Build System
|
||||
|
||||
Installs, stages, and builds the client-side scripts necessary for running the
|
||||
Galaxy webapp. When started through `run.sh` or any other method that utilizes
|
||||
`scripts/common_startup.sh`, Galaxy will (since 18.09) _automatically_ build
|
||||
the client as a part of server startup, when it detects changes, unless that
|
||||
Galaxy web client. When started through `run.sh` or any other method that
|
||||
utilizes `scripts/common_startup.sh`, Galaxy will (since 18.09) _automatically_
|
||||
build the client as a part of server startup when it detects changes unless that
|
||||
functionality is explicitly disabled.
|
||||
|
||||
The base dependencies used are Node.js and Yarn. Galaxy now includes these in
|
||||
the virtual environment, and they can be accessed by activating that with `.
|
||||
.venv/bin/activate` from the Galaxy root directory.
|
||||
The base dependencies used are Node.js and Yarn. Galaxy includes appropriate
|
||||
versions of these in the virtual environment, and they can be accessed by
|
||||
activating that with `. .venv/bin/activate` from the Galaxy root directory.
|
||||
|
||||
If you'd like to install your own dependencies, on OSX the easiest way to get
|
||||
set up is using `homebrew` and the command `brew install nodejs yarn`. More
|
||||
information, including instructions for other platforms, is available at
|
||||
[https://nodejs.org](https://nodejs.org) and
|
||||
If you'd like to install your dependencies external to Galaxy, on OSX the
|
||||
easiest way to get set up is using `homebrew` and the command `brew install
|
||||
nodejs yarn`. More information, including instructions for other platforms, is
|
||||
available at [https://nodejs.org](https://nodejs.org) and
|
||||
[https://yarnpkg.com/](https://yarnpkg.com).
|
||||
|
||||
The Galaxy client build has necessarily grown more complex in the past several
|
||||
@@ -27,11 +27,11 @@ directly on Gitter at
|
||||
|
||||
There are many moving parts to the client build system, but the entry point for
|
||||
most people is the 'client' rule in the Makefile at the root of the Galaxy
|
||||
repository. Execute the following to perform a complete build suitable for
|
||||
local development, including dependency staging, style building, script
|
||||
processing, and bundling. This is a development-specific build which includes
|
||||
extra debugging features, and excludes several production optimizations made
|
||||
during the build process.
|
||||
repository. Execute the following to perform a complete build suitable for local
|
||||
development, including dependency staging, style building, script processing,
|
||||
and bundling. This is a development-specific build that includes extra debugging
|
||||
features and excludes several production optimizations made during the build
|
||||
process.
|
||||
|
||||
make client
|
||||
|
||||
@@ -53,39 +53,31 @@ and these should not be committed.
|
||||
When you're actively developing, it is convenient to have the client
|
||||
automatically rebuild every time you save a file. You can do this using:
|
||||
|
||||
make client-watch
|
||||
|
||||
This will first stage client dependencies, initiate a build, and then will
|
||||
watch for changes in any of the galaxy client source files. When a file is
|
||||
changed, the client will automatically rebuild, after which you can refresh
|
||||
your browser to see changes.
|
||||
|
||||
For even more rapid development you can use the webpack development server,
|
||||
which takes advantage of hot module replacement (HMR). This technique allows
|
||||
swapping out of javascript modules while the application is running without
|
||||
requiring a full page reload most of the time, at least in the more modern
|
||||
parts of the application.
|
||||
|
||||
The command below starts a special webpack dev server after a client
|
||||
build.
|
||||
|
||||
make client-dev-server
|
||||
|
||||
This will start up an extra client development server running on port 8081.
|
||||
Open your browser to http://localhost:8081 (instead of the default 8080 that
|
||||
Galaxy would run on), and you should see Galaxy like normal. Except now, when
|
||||
you change client code it'll automatically rebuild *and* reload the relevant
|
||||
portion of the application for you. Lastly, if you
|
||||
are running Galaxy at a location other than the default, you can specify a
|
||||
different proxy target (in this example, port 8000) using the GALAXY_URL
|
||||
environment variable:
|
||||
Or, with the package scripts from this `client` directory:
|
||||
|
||||
yarn run develop
|
||||
|
||||
This will start up an extra client development server running on port 8081. Open
|
||||
your browser to `http://localhost:8081` (instead of the default 8080 that Galaxy
|
||||
would run on), and you should see Galaxy like normal. Except now, when you
|
||||
change client code it'll automatically rebuild _and_ reload the relevant portion
|
||||
of the application for you. Lastly, if you are running Galaxy at a location
|
||||
other than the default, you can specify a different proxy target (in this
|
||||
example, port 8000) using the GALAXY_URL environment variable:
|
||||
|
||||
GALAXY_URL="http://localhost:8000" make client-dev-server
|
||||
|
||||
Sometimes you want to run your local UI against a remote Galaxy server. This is also possible, if you enable `CHANGE_ORIGIN` flag
|
||||
Sometimes you want to run your local UI against a remote Galaxy server. This is
|
||||
also possible if you set the `CHANGE_ORIGIN` environment variable:
|
||||
|
||||
CHANGE_ORIGIN=true GALAXY_URL="https://usegalaxy.org/" make client-dev-server
|
||||
|
||||
You can also specify a particular port to bind the dev server to:
|
||||
|
||||
WEBPACK_PORT=8083 yarn run develop
|
||||
|
||||
## Running a Separate Server
|
||||
|
||||
When developing the client it can be helpful to run a local server for the
|
||||
@@ -94,17 +86,22 @@ commands. This command will run galaxy without building the client:
|
||||
|
||||
make skip-client
|
||||
|
||||
Or by setting the following environment variable and running Galaxy however you
|
||||
prefer:
|
||||
|
||||
GALAXY_SKIP_CLIENT_BUILD=1 ./run.sh
|
||||
|
||||
## Changing Styles/CSS
|
||||
|
||||
Galaxy uses Sass for its styling, which is a superset of CSS that compiles down
|
||||
to regular CSS. Most Galaxy styling source (.scss) files are kept in
|
||||
`client/src/style/scss`. There are additionally style blocks alongside some Vue
|
||||
components -- styles that are particular to that individual component and do
|
||||
not apply site-wide.
|
||||
Galaxy uses Sass for globally applied styling, which is a superset of CSS that
|
||||
compiles down to regular CSS. Most Galaxy styling source (.scss) files are kept
|
||||
in `client/src/style/scss`. Many components will also have local style blocks
|
||||
containing styles that are particular to that individual component and do not
|
||||
apply site-wide.
|
||||
|
||||
On build, the compiled css bundle is served at `/static/style/base.css`.
|
||||
|
||||
As mentioned above, `make client` will rebuild styles, as a part of the webpack
|
||||
As mentioned above, `make client` will rebuild styles as a part of the webpack
|
||||
build. For iterative development, "Watch Mode" rebuilds as described above do
|
||||
include style changes.
|
||||
|
||||
@@ -122,7 +119,7 @@ framework.
|
||||
|
||||
For testing Vue components, we use the [Vue testing
|
||||
utils](https://vue-test-utils.vuejs.org/) to mount individual components in a
|
||||
test bed and check them for rendered features. Please use jest-based mocking
|
||||
test bed and check them for rendered features. Please use jest-based mocking
|
||||
for isolating test functionality.
|
||||
|
||||
A set of older qUnit tests also exist which will be phased-out as the code they
|
||||
@@ -142,8 +139,8 @@ directory. This is what happens during a complete client build.
|
||||
|
||||
During client-side development, it is more convenient to have granular testing
|
||||
options. The various testing scripts are defined inside package.json within the
|
||||
client folder, and are called with `yarn` as demonstrated in the
|
||||
following commands.
|
||||
client folder and are called with `yarn` as demonstrated in the following
|
||||
commands.
|
||||
|
||||
This is what CI is going to run, and also what 'make client-test' invokes,
|
||||
executing all the client tests:
|
||||
@@ -154,7 +151,7 @@ You can also bypass qunit and single-run all of the jest tests like so:
|
||||
|
||||
yarn run jest
|
||||
|
||||
Or, if you really want to run just the qunit tests:
|
||||
Or, if you want to run just the qunit tests:
|
||||
|
||||
yarn run qunit
|
||||
|
||||
|
||||
@@ -9,17 +9,17 @@ you access to the Galaxy.config, current user, and current user histories. Pleas
|
||||
retrieve your values, and bypass importing Galaxy altogether.
|
||||
|
||||
#### Sometimes you still need to update Vue from backbone as the legacy environment changes
|
||||
|
||||
There are definitely use-cases where the Backbone models update over time and we need to update some
|
||||
value inside Vue. Instead of importing backbone models directly into Vue components, try building a
|
||||
backbone event listener that updates some relevant Vuex store.
|
||||
|
||||
* [Keeping Vuex in Sync with
|
||||
Galaxy](https://github.com/galaxyproject/galaxy/blob/dev/client/src/store/syncVuexToGalaxy.js)
|
||||
- [Keeping Vuex in Sync with
|
||||
Galaxy](https://github.com/galaxyproject/galaxy/blob/dev/client/src/store/syncVuexToGalaxy.js)
|
||||
|
||||
These issues should disappear over time as the all of the old client is rebuilt in the new
|
||||
ecosystem.
|
||||
|
||||
|
||||
## Mount Functions
|
||||
|
||||
In what most people think of as a "standard" Vue application there would be only one place that Vue
|
||||
@@ -30,7 +30,6 @@ However, we are incrementally replacing old Backbone views, so in its current st
|
||||
several mounting functions for various components depending on where that component is intended to
|
||||
fit into the existing Backbone layouts.
|
||||
|
||||
|
||||
### Using the standard mount to pass in Galaxy variables as props
|
||||
|
||||
A standard mount function has been provided in src/utils. This mount function accepts a component
|
||||
@@ -46,28 +45,26 @@ import MyComponent from "components/MyComponent";
|
||||
import { mountVueComponent } from "utils/mountVueComponent";
|
||||
|
||||
const OldBackboneView = {
|
||||
|
||||
someInitMethodYouMake() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const mounter = mountVueComponent(MyComponent);
|
||||
|
||||
// pass in required props
|
||||
const props = {
|
||||
|
||||
const props = {
|
||||
// Something peeled off the global galaxy
|
||||
somePropVal: Galaxy.someDealie,
|
||||
somePropVal: Galaxy.someDealie,
|
||||
|
||||
// ...or the current history
|
||||
name: Galaxy.currentHistory.name,
|
||||
|
||||
// or maybe from the backbone model for this view
|
||||
shoeSize: this.model.shoeSize
|
||||
shoeSize: this.model.shoeSize,
|
||||
};
|
||||
|
||||
// VM is a Vue instance.
|
||||
// this.$el is some jquery selection, first item is the actual DOM object
|
||||
const container = this.$el[0];
|
||||
const vm = mounter(props, container);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -6,9 +6,9 @@ Using them effectively can make your code more reusable, decoupled, and easier t
|
||||
|
||||
**More about Composables:**
|
||||
|
||||
* [Composables Overview](https://vuejs.org/guide/reusability/composables.html)
|
||||
* [Composition API](https://vuejs.org/api/composition-api-setup.html)
|
||||
* [\<script setup\>](https://vuejs.org/api/sfc-script-setup.html)
|
||||
- [Composables Overview](https://vuejs.org/guide/reusability/composables.html)
|
||||
- [Composition API](https://vuejs.org/api/composition-api-setup.html)
|
||||
- [\<script setup\>](https://vuejs.org/api/sfc-script-setup.html)
|
||||
|
||||
## Using Composables in the Composition API
|
||||
|
||||
@@ -36,8 +36,8 @@ export default {
|
||||
setup() {
|
||||
const { currentUser } = useCurrentUser();
|
||||
return { currentUser };
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -79,7 +79,7 @@ import { useCurrentUser } from "composables/user";
|
||||
|
||||
jest.mock("composables/user");
|
||||
useCurrentUser.mockReturnValue({
|
||||
currentUser: {}
|
||||
currentUser: {},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -95,11 +95,7 @@ Usage:
|
||||
<script setup>
|
||||
import { useFilterObjectArray } from "composables/utils/filter";
|
||||
|
||||
const filteredArray = useFilterObjectArray(
|
||||
someReactiveArray,
|
||||
searchValue,
|
||||
["name", "description"]
|
||||
);
|
||||
const filteredArray = useFilterObjectArray(someReactiveArray, searchValue, ["name", "description"]);
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
+14
-14
@@ -6,23 +6,23 @@ Heading Levels are used by screen readers, and other software, to get a rough id
|
||||
|
||||
## Heading Level best practices
|
||||
|
||||
- Make sure every route has exactly one `<h1>` element, which best describes the content of the current page.
|
||||
- Make sure every route has exactly one `<h1>` element, which best describes the content of the current page.
|
||||
|
||||
- When increasing a heading level, do not skip Levels.
|
||||
- When increasing a heading level, do not skip Levels.
|
||||
|
||||
- Do not use a headings level to determine it's size. Use one of the heading utility classes (e.g. `h-lg`) instead.
|
||||
- Do not use a headings level to determine it's size. Use one of the heading utility classes (e.g. `h-lg`) instead.
|
||||
|
||||
- Do not use a heading tag to make a non-heading text large. Use a heading utility class on a `<span>` instead.
|
||||
- Do not use a heading tag to make a non-heading text large. Use a heading utility class on a `<span>` instead.
|
||||
|
||||
## Sizing Headings
|
||||
|
||||
There are several utility classes which size headings:
|
||||
|
||||
- `h-xl` - Extra large headings
|
||||
- `h-lg` - Large headings (most main headings should have this size)
|
||||
- `h-md` - Medium headings
|
||||
- `h-sm` - Small headings (many sub-headings have this size)
|
||||
- `h-text` - Text sized headings
|
||||
- `h-xl` - Extra large headings
|
||||
- `h-lg` - Large headings (most main headings should have this size)
|
||||
- `h-md` - Medium headings
|
||||
- `h-sm` - Small headings (many sub-headings have this size)
|
||||
- `h-text` - Text sized headings
|
||||
|
||||
Galaxy uses `h-lg` for most top-level (`<h1>`) headings.
|
||||
|
||||
@@ -45,8 +45,8 @@ Set the headings level, by setting aa `h1 ... h6` prop:
|
||||
|
||||
Following properties allow for further styling the component:
|
||||
|
||||
- `size=["xl", "lg", "md", "sm", "text"]` - sets the headings size class
|
||||
- `bold` - makes a heading bold
|
||||
- `inline` - displays the heading inline
|
||||
- `separator` - draws a separating line, to better distinguish sections
|
||||
- `icon="..."` - adds a font-awesome icon decoration to the left of the heading. Make sure to also load the icon with `library.add(...)`.
|
||||
- `size=["xl", "lg", "md", "sm", "text"]` - sets the headings size class
|
||||
- `bold` - makes a heading bold
|
||||
- `inline` - displays the heading inline
|
||||
- `separator` - draws a separating line, to better distinguish sections
|
||||
- `icon="..."` - adds a font-awesome icon decoration to the left of the heading. Make sure to also load the icon with `library.add(...)`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Did you know, jQuery is old enough to drive? It's old enough to get a driver's license. jQuery is a
|
||||
tool that was built to deal with inconsistencies in browsers that NO LONGER EXIST. In a couple
|
||||
years, jQuery will be voting, drinking, and capable of being tried as an adult.
|
||||
years, jQuery will be voting, drinking, and capable of being tried as an adult.
|
||||
|
||||
If you think you need jQuery, you are mistaken. Please seek help from somebody in the wg-ui-ux
|
||||
workgroup. There is nothing jQuery can provide you that isn't already part of vanilla javascript or
|
||||
@@ -14,5 +14,6 @@ One of our most important goals in redesigning Galaxy is the complete eliminatio
|
||||
from our source, along with all its invasive plugins.
|
||||
|
||||
### References
|
||||
* [You Don't Need jQuery](https://github.com/nefe/You-Dont-Need-jQuery)
|
||||
* [document.querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
|
||||
|
||||
- [You Don't Need jQuery](https://github.com/nefe/You-Dont-Need-jQuery)
|
||||
- [document.querySelector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
|
||||
|
||||
@@ -22,34 +22,27 @@ component we previously made, but you are free to putput whatever you want in th
|
||||
doodad and saveDoddad properties as desired, as well as any other local data with the only
|
||||
restriction that Vue needs a single root element in which to render.
|
||||
|
||||
|
||||
## The Renderer
|
||||
|
||||
```html static
|
||||
<!-- DoodadEditor.vue, a simple "rendering" component -->
|
||||
|
||||
<template>
|
||||
<AutoComplete
|
||||
:options="options"
|
||||
:value="doodad.category"
|
||||
@select="saveCategory"
|
||||
/>
|
||||
<AutoComplete :options="options" :value="doodad.category" @select="saveCategory" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
doodad: { type: Object, required: true },
|
||||
options: { type: Array, required: true },
|
||||
},
|
||||
methods: {
|
||||
saveCategory(newCategory) {
|
||||
this.$emit('update:doodad', { ...this.doodad, category: newCategory });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
doodad: { type: Object, required: true },
|
||||
options: { type: Array, required: true },
|
||||
},
|
||||
methods: {
|
||||
saveCategory(newCategory) {
|
||||
this.$emit("update:doodad", { ...this.doodad, category: newCategory });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -66,7 +59,6 @@ binds](https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)).
|
||||
Whatever happens to that new object is somebody else's job. As soon as you tie the data management
|
||||
to the rendering, the re-usability of your components craters.
|
||||
|
||||
|
||||
## The Provider
|
||||
|
||||
As the opposite of the rendering component, a provider or renderless component, is pure logic. It
|
||||
@@ -76,7 +68,6 @@ functionality available in Vue. Some others are [Mixins](https://vuejs.org/v2/gu
|
||||
[Provide/Inject](https://v3.vuejs.org/guide/component-provide-inject.html) and (in Vue3) [the
|
||||
composition API](https://v3.vuejs.org/guide/composition-api-introduction.html).
|
||||
|
||||
|
||||
```js static
|
||||
// DoodadProvider.js
|
||||
|
||||
@@ -134,7 +125,7 @@ mandatory markup, just one big empty slot.
|
||||
// Testing a renderless component
|
||||
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue, waitForLifecyleEvent } from "tests/jest/helpers"
|
||||
import { getLocalVue, waitForLifecyleEvent } from "tests/jest/helpers";
|
||||
import DoodadProvider from "./DoodadProvider";
|
||||
|
||||
describe("A renderless component", () => {
|
||||
@@ -157,12 +148,12 @@ describe("A renderless component", () => {
|
||||
// component. This is often good enough for waiting for
|
||||
// an initial ajax load to finish, for example
|
||||
await waitForLifecyleEvent(wrapper.vm, "updated");
|
||||
})
|
||||
});
|
||||
|
||||
test("someProp", () => {
|
||||
const { someProp } = slotProps;
|
||||
expect(someProp).toExist();
|
||||
// ...more tests
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ I'm not talking about how webpack turns it into a rendering function. That's obv
|
||||
I mean conceptually, props come in (like arguments) and events go out (like the return statements).
|
||||
A component is a fancy kind of function that can keep emitting results and accept changing inputs
|
||||
over time. In truth it more closely resembles an Observable, but an observable is ALSO a slightly
|
||||
fancier kind of function.
|
||||
fancier kind of function.
|
||||
|
||||
If you just think of a component as thing that takes input props and emits output events you're well
|
||||
on your way to using them well.
|
||||
@@ -19,22 +19,22 @@ problems of the old imperative class-based legacy code.
|
||||
New vue programmers are ok at handing props to components, but they rarely use events effectively
|
||||
(at first). As a result they end up using a lot of global state, a million little data props and
|
||||
relying on imperfect globalized tools like Vuex or other imported dependencies for every little
|
||||
variable.
|
||||
variable.
|
||||
|
||||
Vuex definitely has its uses, but not as many as you might expect given the way it is
|
||||
overly-emphasized in common tutorials. It's easy to walk away from an "Intro to Vue" video with the
|
||||
idea that all data must live in Vuex all the time. That's a really undesirable situation.
|
||||
idea that all data must live in Vuex all the time. That's a really undesirable situation.
|
||||
|
||||
Although vuex is a well-organized (many would say over-organized) state machine, it is important
|
||||
to remember that it is still a kind of global injection and deserves to be considered as such.
|
||||
|
||||
* [Should I Store This Data in
|
||||
Vuex](https://markus.oberlehner.net/blog/should-i-store-this-data-in-vuex/)
|
||||
* [Vuex getters are great, but don’t overuse
|
||||
them](https://codeburst.io/vuex-getters-are-great-but-dont-overuse-them-9c946689b414)
|
||||
- [Should I Store This Data in
|
||||
Vuex](https://markus.oberlehner.net/blog/should-i-store-this-data-in-vuex/)
|
||||
- [Vuex getters are great, but don’t overuse
|
||||
them](https://codeburst.io/vuex-getters-are-great-but-dont-overuse-them-9c946689b414)
|
||||
|
||||
Data persistence should be something that happens near the top of your component tree, not down in
|
||||
the guts.
|
||||
the guts.
|
||||
|
||||
Your first thought with a component should be: "How can I offload the handling of the results of
|
||||
this component to my caller?" The answer is usually going to be events. A component that simply
|
||||
@@ -46,7 +46,6 @@ global state to operate.
|
||||
They're just fancy shorthands for a prop / event handler combination. They are fundamentally no
|
||||
different from props and events, but the syntax is important to understand.
|
||||
|
||||
|
||||
### Think carefully about what should really be in "data".
|
||||
|
||||
Most of good component design boils down to answering the following question: What do I want to put
|
||||
|
||||
@@ -44,7 +44,7 @@ const myFunction = (param) => {
|
||||
};
|
||||
|
||||
// anonymous functions
|
||||
const myFunction = function(param) {
|
||||
const myFunction = function (param) {
|
||||
//do stuff
|
||||
};
|
||||
```
|
||||
@@ -84,10 +84,9 @@ When possible, use arrow functions instead.
|
||||
> export function myFunction(parameter) {
|
||||
> const addOne = (value) => {
|
||||
> return value + 1;
|
||||
> }
|
||||
> };
|
||||
> // do more stuff...
|
||||
> }
|
||||
>
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
@@ -96,12 +95,11 @@ When possible, use arrow functions instead.
|
||||
> // in myModules.js
|
||||
>
|
||||
> export const myFunction = (parameter) => {
|
||||
> const addOne = function(value) {
|
||||
> const addOne = function (value) {
|
||||
> return value + 1;
|
||||
> }
|
||||
> };
|
||||
> // do more stuff...
|
||||
> }
|
||||
>
|
||||
> };
|
||||
> ```
|
||||
|
||||
## HTML Multi-Line Layout
|
||||
@@ -115,9 +113,7 @@ Prettier tries to respect whitespace when formatting your HTML templates, even w
|
||||
Might get turned into:
|
||||
|
||||
```vue
|
||||
<b-button class="danger-button mb-4" variant="danger" @click="onDangerButtonClick"
|
||||
>A very Long Button Text</b-button
|
||||
>
|
||||
<b-button class="danger-button mb-4" variant="danger" @click="onDangerButtonClick">A very Long Button Text</b-button>
|
||||
```
|
||||
|
||||
Notice the strange positioning of the `>` brackets.
|
||||
@@ -277,7 +273,7 @@ Do not add space between elements connected by conditionals.
|
||||
> <span v-if="conditional">
|
||||
> condition met
|
||||
> </span>
|
||||
>
|
||||
>
|
||||
> <span v-else>
|
||||
> condition not met
|
||||
> </span>
|
||||
@@ -293,7 +289,7 @@ Add space between non-connected elements.
|
||||
> <span>
|
||||
> First span.
|
||||
> </span>
|
||||
>
|
||||
>
|
||||
> <span>
|
||||
> Second span.
|
||||
> </span>
|
||||
@@ -325,7 +321,7 @@ Add space between logical blocks of elements.
|
||||
> <span v-else>
|
||||
> condition 1 not met
|
||||
> </span>
|
||||
>
|
||||
>
|
||||
> <span v-if="otherConditional">
|
||||
> condition 2 met
|
||||
> </span>
|
||||
|
||||
@@ -9,7 +9,7 @@ code updates its test extensions again.
|
||||
|
||||
#### To debug a single Jest test:
|
||||
|
||||
1. Open a jest test (a file that ends with *.test.js)
|
||||
1. Open a jest test (a file that ends with \*.test.js)
|
||||
|
||||
1. Make sure the test file is selected, especially if you have multiple files open. This process
|
||||
will fail confusingly and without obvious error if you have not launched the debugger with the
|
||||
@@ -34,8 +34,6 @@ code updates its test extensions again.
|
||||
variables near your breakpoint and see their values in the "Variables" section of the Run and
|
||||
Debug pane.
|
||||
|
||||
|
||||
|
||||
```json
|
||||
// sample launch.json
|
||||
{
|
||||
@@ -45,16 +43,16 @@ code updates its test extensions again.
|
||||
"type": "node",
|
||||
"name": "debug selected jest test",
|
||||
"request": "launch",
|
||||
|
||||
|
||||
// launches version of jest from inside the node_modules
|
||||
// this means you need to have run yarn first
|
||||
"program": "${workspaceFolder}/client/node_modules/jest/bin/jest",
|
||||
"args": [
|
||||
// Alias -i.
|
||||
// Alias -i.
|
||||
// Normally jest opens up a bunch of workers to run all your tests faster
|
||||
// but we don't want that right now.
|
||||
"--runInBand",
|
||||
|
||||
|
||||
// finds jest config
|
||||
"--config",
|
||||
"${workspaceFolder}/client/tests/jest/jest.config.js",
|
||||
@@ -68,9 +66,8 @@ code updates its test extensions again.
|
||||
"console": "integratedTerminal",
|
||||
|
||||
// allows you to place breakpoints right in vscode's gutter
|
||||
"disableOptimisticBPs": true,
|
||||
},
|
||||
|
||||
"disableOptimisticBPs": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
|
||||
[Galaxy uses Jest](https://jestjs.io/) for its client-side unit testing
|
||||
framework.
|
||||
|
||||
For testing Vue components, we use the [Vue testing
|
||||
utils](https://vue-test-utils.vuejs.org/) to mount individual components in a
|
||||
test bed and check them for rendered features. Please use jest-based mocking
|
||||
test bed and check them for rendered features. Please use jest-based mocking
|
||||
for isolating test functionality.
|
||||
|
||||
|
||||
### Specific test scenarios & examples
|
||||
|
||||
* [Mocking an imported
|
||||
dependency](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
|
||||
* [Testing async
|
||||
operations](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
|
||||
* [Testing a Vue component for expected rendering
|
||||
output](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
|
||||
* [Firing an event against a shallow mounted vue
|
||||
component](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
|
||||
- [Mocking an imported
|
||||
dependency](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
|
||||
- [Testing async
|
||||
operations](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/tagService.test.js)
|
||||
- [Testing a Vue component for expected rendering
|
||||
output](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
|
||||
- [Firing an event against a shallow mounted vue
|
||||
component](https://github.com/galaxyproject/galaxy/blob/dev/client/src/components/Tags/StatelessTags.test.js)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Part of making good code is making that code easy to test.
|
||||
Part of making good code is making that code easy to test.
|
||||
|
||||
### Implement logic in pure functions when possible
|
||||
|
||||
@@ -10,14 +10,13 @@ There is almost definitely no such thing as a well-written 1000 line function.
|
||||
Most of whatever happened in that thing was probably deterministic and can be
|
||||
broken up into easily testable chunks.
|
||||
|
||||
|
||||
### Wrap native browser resources in a function so they can be easily mocked
|
||||
|
||||
If your javascript needs to talk to the window object, or navigator, etc. wrap
|
||||
that in a function call so that it can be easily mocked during testing.
|
||||
|
||||
|
||||
#### Your Module
|
||||
|
||||
```js static
|
||||
// myModule.js
|
||||
|
||||
@@ -35,6 +34,7 @@ export function theThingYouReallyCareAbout() {
|
||||
```
|
||||
|
||||
#### Your test file
|
||||
|
||||
```js static
|
||||
// myModule.test.js
|
||||
import { theThingYouReallyCareAbout, redirectTo } from "./myModule";
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
Please remember that these tests are not _for_ you. They're for the people who
|
||||
come after you. It will be a lot easier to modify, repair and upgrade your code
|
||||
if they can figure out what you were originally hoping to accomplish. Try to
|
||||
if they can figure out what you were originally hoping to accomplish. Try to
|
||||
use as detailed 'expect' statements as possible -- overuse of 'toBeTruthy()'
|
||||
for example, can hide the intent of your test.
|
||||
|
||||
Add a couple of comments. Use variable names that mean something. Nobody's
|
||||
Add a couple of comments. Use variable names that mean something. Nobody's
|
||||
code is as self-documenting as they believe it to be.
|
||||
|
||||
|
||||
### Only test the public API that you define
|
||||
|
||||
Internal implementations come and go with library upgrades and new tech. But
|
||||
@@ -20,17 +19,16 @@ Separate your concerns and identify the developer-facing methods and functions
|
||||
you expect them to use. Test THOSE. Everything else should probably be
|
||||
considered an implementation detail.
|
||||
|
||||
The other side of the same coin is to test *only* the unit in question. If your
|
||||
The other side of the same coin is to test _only_ the unit in question. If your
|
||||
component has a model that uses a service that touches Vuex, which then uses
|
||||
Axios to fetch some data -- don't test all that at once. Break things apart and
|
||||
mock functionality to isolate testing to units. End to end testing is a
|
||||
Axios to fetch some data -- don't test all that at once. Break things apart and
|
||||
mock functionality to isolate testing to units. End to end testing is a
|
||||
separate thing that shouldn't be attempted using spec tests in Jest.
|
||||
|
||||
Assume nobody cares _how_ your code works, we just need to know that the public
|
||||
API you designed _does_ work. If performance problems or new tech necessitate a
|
||||
re-write, these tests become a guide for the next implementation.
|
||||
|
||||
|
||||
### Writing a test file
|
||||
|
||||
Jest will try to test any file ending in "\*.test.js". Please place your test
|
||||
@@ -65,9 +63,7 @@ describe("some module you wrote", () => {
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Check out the Jest helper functions
|
||||
|
||||
We have created some [common helpers for common testing
|
||||
scenarios](https://github.com/galaxyproject/galaxy/blob/dev/client/tests/jest/helpers.js).
|
||||
|
||||
scenarios](https://github.com/galaxyproject/galaxy/blob/dev/client/tests/jest/helpers.js).
|
||||
|
||||
+13
-11
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "galaxy-client",
|
||||
"name": "@galaxyproject/galaxy-client",
|
||||
"version": "0.1.0",
|
||||
"description": "Galaxy client application build system",
|
||||
"keywords": [
|
||||
@@ -9,7 +9,10 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/galaxyproject/galaxy.git"
|
||||
},
|
||||
"license": "AFL-3.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"browserslist": [
|
||||
"defaults",
|
||||
"not ie <= 11",
|
||||
@@ -110,20 +113,18 @@
|
||||
"xml-beautifier": "^0.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "gulp && yarn run save-build-hash && yarn run webpack-watch",
|
||||
"serve": "NODE_OPTIONS=--max-old-space-size=4096 && NODE_ENV=development gulp && webpack serve",
|
||||
"build": "NODE_ENV=development gulp && webpack && yarn run save-build-hash",
|
||||
"build-production": "NODE_ENV=production gulp && yarn run webpack-production && yarn run save-build-hash",
|
||||
"build-production-maps": "NODE_ENV=production gulp && yarn run webpack-production-maps && yarn run save-build-hash",
|
||||
"develop": "NODE_OPTIONS=--max-old-space-size=4096 NODE_ENV=development gulp && webpack-dev-server",
|
||||
"build": "NODE_ENV=development gulp && webpack && yarn run stage-build",
|
||||
"build-production": "NODE_ENV=production gulp && yarn run webpack-production && yarn run stage-build",
|
||||
"build-production-maps": "NODE_ENV=production gulp && yarn run webpack-production-maps && yarn run stage-build",
|
||||
"build-stats": "NODE_ENV=production webpack --profile --json=webpack-stats.json",
|
||||
"view-stats": "webpack-bundle-analyzer webpack-stats.json ../static/dist/",
|
||||
"webpack-watch": "webpack --watch",
|
||||
"webpack-production": "NODE_ENV=production webpack",
|
||||
"webpack-production-maps": "GXY_BUILD_SOURCEMAPS=1 NODE_ENV=production webpack",
|
||||
"gulp": "gulp",
|
||||
"save-build-hash": "(git rev-parse HEAD 2>/dev/null || echo '') >../static/client_build_hash.txt",
|
||||
"format": "prettier --write 'src/style/scss/**/*.scss' 'src/**/{*.ts,*.js,*.vue}' 'tests/jest/standalone/{*.ts,*.js}' '!src/libs/**'",
|
||||
"format-check": "prettier --check 'src/style/scss/**/*.scss' 'src/**/{*.js,*.vue}' 'tests/jest/standalone/{*.ts,*.js}' '!src/libs/**'",
|
||||
"stage-build": "cpy 'dist/*' '../static/dist' && (git rev-parse HEAD 2>/dev/null || echo '') >../static/client_build_hash.txt",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"prettier": "yarn run format",
|
||||
"test": "yarn run qunit && yarn run jest",
|
||||
"jest": "jest --config tests/jest/jest.config.js",
|
||||
@@ -151,6 +152,7 @@
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"babel-jest": "^29.3.1",
|
||||
"buffer": "^6.0.3",
|
||||
"cpy-cli": "^4.2.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||
"del": "^6.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
tabWidth: 4,
|
||||
printWidth: 120,
|
||||
bracketSameLine: true
|
||||
bracketSameLine: true,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
```js
|
||||
<div v-for="variant in [ 'success', 'info', 'warning', 'error' ]">
|
||||
<Alert v-bind:variant="variant">A {{variant}} message</Alert>
|
||||
<Alert v-bind:variant="variant">A {{ variant }} message</Alert>
|
||||
</div>
|
||||
```
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
<div class="dropdown-menu" aria-labelledby="dataset-dropdown">
|
||||
<a class="dropdown-item" href="#" @click.prevent="showDataset">
|
||||
<span class="fa fa-eye fa-fw mr-1" />
|
||||
<span>Show in History</span>
|
||||
<span>Switch to History containing dataset</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="copyDataset">
|
||||
<span class="fa fa-copy fa-fw mr-1" />
|
||||
<span>Copy to History</span>
|
||||
<span>Copy to current History</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,12 +20,16 @@
|
||||
@click="submit('attribute', 'attributes')">
|
||||
<font-awesome-icon icon="save" class="mr-1" />{{ "Save" | l }}
|
||||
</b-button>
|
||||
<b-button @click="submit('attribute', 'autodetect')">
|
||||
<b-button v-if="!result['metadata_disable']" @click="submit('attribute', 'autodetect')">
|
||||
<font-awesome-icon icon="redo" class="mr-1" />{{ "Auto-detect" | l }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab v-if="!result['conversion_disable'] || !result['datatype_disable']">
|
||||
<b-tab
|
||||
v-if="
|
||||
(!result['conversion_disable'] || !result['datatype_disable']) &&
|
||||
!result['metadata_disable']
|
||||
">
|
||||
<template v-slot:title>
|
||||
<span v-if="!result['conversion_disable']">
|
||||
<font-awesome-icon icon="cog" class="mr-1" />{{ "Convert" | l }}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<li class="dataset-source">
|
||||
<a v-if="browserCompatUri" :href="sourceUri" target="_blank">
|
||||
<a v-if="browserCompatUri" v-b-tooltip.hover title="Dataset Source URL" :href="sourceUri" target="_blank">
|
||||
{{ source.source_uri }}
|
||||
<font-awesome-icon v-b-tooltip.hover title="Dataset Source URL" icon="external-link-alt" />
|
||||
<font-awesome-icon icon="external-link-alt" />
|
||||
</a>
|
||||
<span v-else>
|
||||
{{ source.source_uri }}
|
||||
</span>
|
||||
<font-awesome-icon v-b-tooltip.hover title="Copy URI" icon="copy" style="cursor: pointer" @click="copyLink" />
|
||||
<span v-b-tooltip.hover title="Copy URI"
|
||||
><font-awesome-icon icon="copy" style="cursor: pointer" @click="copyLink"
|
||||
/></span>
|
||||
<br />
|
||||
<DatasetSourceTransform :transform="source.transform" />
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormUpload from "./FormUpload";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("FormUpload", () => {
|
||||
const mountFormUpload = (props) =>
|
||||
mount(FormUpload, {
|
||||
propsData: props,
|
||||
localVue,
|
||||
});
|
||||
|
||||
it("should display selected file in disabled textarea", async () => {
|
||||
const v = "H1, H2, H3\nv1, v2, v3";
|
||||
const wrapper = mountFormUpload({ value: v });
|
||||
const el = wrapper.find("textarea");
|
||||
expect(el.element.value).toEqual(v);
|
||||
expect(el.element.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should not display text box if file has not been selected", async () => {
|
||||
const wrapper = mountFormUpload({ value: null });
|
||||
const el = wrapper.find("textarea");
|
||||
expect(el.isVisible()).toBe(false);
|
||||
expect(el.text()).toBe("");
|
||||
const noInput = wrapper.find("label");
|
||||
expect(noInput.text()).toBe("No file chosen");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
const props = defineProps({
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const file = ref(null);
|
||||
const waiting = ref(false);
|
||||
|
||||
const emit = defineEmits(["input"]);
|
||||
|
||||
const currentValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(newValue) {
|
||||
emit("input", newValue);
|
||||
},
|
||||
});
|
||||
|
||||
function readFile() {
|
||||
var reader = new FileReader();
|
||||
if (file.value) {
|
||||
waiting.value = true;
|
||||
reader.onload = () => {
|
||||
currentValue.value = reader.result;
|
||||
waiting.value = false;
|
||||
};
|
||||
reader.readAsText(file.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-form-file v-model="file" class="mb-1" @input="readFile" />
|
||||
<div v-if="waiting">
|
||||
<font-awesome-icon icon="spinner" spin />
|
||||
Uploading File...
|
||||
</div>
|
||||
<textarea v-show="currentValue" v-model="currentValue" class="ui-textarea" disabled />
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,6 +10,7 @@ import FormNumber from "./Elements/FormNumber.vue";
|
||||
import FormText from "./Elements/FormText.vue";
|
||||
import FormOptionalText from "./Elements/FormOptionalText.vue";
|
||||
import FormRulesEdit from "./Elements/FormRulesEdit.vue";
|
||||
import FormUpload from "./Elements/FormUpload.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { ref, computed, useAttrs } from "vue";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
@@ -265,6 +266,7 @@ const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !
|
||||
:multiple="attrs.multiple" />
|
||||
<FormColor v-else-if="props.type === 'color'" :id="props.id" v-model="currentValue" />
|
||||
<FormDirectory v-else-if="props.type === 'directory_uri'" v-model="currentValue" />
|
||||
<FormUpload v-else-if="props.type === 'upload'" v-model="currentValue" />
|
||||
<FormRulesEdit v-else-if="props.type == 'rules'" v-model="currentValue" :target="attrs.target" />
|
||||
<FormParameter
|
||||
v-else-if="backbonejs"
|
||||
|
||||
@@ -5,16 +5,8 @@
|
||||
<script>
|
||||
import HistoryList from "./history-list";
|
||||
export default {
|
||||
props: {
|
||||
actionId: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
new HistoryList.View({
|
||||
action_id: this.actionId,
|
||||
}).$el.appendTo(this.$refs.target);
|
||||
new HistoryList.View().$el.appendTo(this.$refs.target);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -62,15 +62,10 @@ var View = Backbone.View.extend({
|
||||
initialize: function (options) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
LoadingIndicator.markViewAsLoading(this);
|
||||
|
||||
if (options.action_id == "list_published") {
|
||||
this.active_tab = "shared";
|
||||
} else if (options.action_id == "list") {
|
||||
this.active_tab = "user";
|
||||
}
|
||||
this.active_tab = "user";
|
||||
this.model = new Backbone.Model();
|
||||
Utils.get({
|
||||
url: `${getAppRoot()}history/${options.action_id}?${$.param(Galaxy.params)}`,
|
||||
url: `${getAppRoot()}history/list?${$.param(Galaxy.params)}`,
|
||||
success: (response) => {
|
||||
this.model.set(response);
|
||||
this.render();
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="showVisualizations"
|
||||
class="px-1"
|
||||
class="visualize-btn px-1"
|
||||
title="Visualize"
|
||||
size="sm"
|
||||
variant="link"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import GenericElement from "./GenericElement";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("GenericElement", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(GenericElement, {
|
||||
propsData: {
|
||||
dsc: {
|
||||
elements: [
|
||||
{
|
||||
element_index: 1,
|
||||
element_identifier: "element-1",
|
||||
element_type: "hda",
|
||||
object: {
|
||||
id: "item-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
element_index: 2,
|
||||
element_identifier: "element-2",
|
||||
element_type: "hdca",
|
||||
object: {
|
||||
id: "item-2",
|
||||
collection_type: "list",
|
||||
element_count: 2,
|
||||
elements_datatypes: ["txt"],
|
||||
elements: [
|
||||
{
|
||||
element_index: 3,
|
||||
element_identifier: "element-3",
|
||||
element_type: "hda",
|
||||
object: {
|
||||
id: "item_3",
|
||||
},
|
||||
},
|
||||
{
|
||||
element_index: 4,
|
||||
element_identifier: "element-4",
|
||||
element_type: "hda",
|
||||
object: {
|
||||
id: "item_4",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
it("check basics", async () => {
|
||||
const contentItems = wrapper.findAll(".content-item");
|
||||
expect(contentItems.length).toBe(2);
|
||||
expect(contentItems.at(0).attributes("data-hid")).toBe("1");
|
||||
expect(contentItems.at(1).attributes("data-hid")).toBe("2");
|
||||
await contentItems.at(1).find(".cursor-pointer").trigger("click");
|
||||
const contentExpanded = wrapper.findAll(".content-item");
|
||||
expect(contentExpanded.length).toBe(4);
|
||||
expect(contentExpanded.at(2).attributes("data-hid")).toBe("3");
|
||||
expect(contentExpanded.at(3).attributes("data-hid")).toBe("4");
|
||||
});
|
||||
});
|
||||
@@ -1,36 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import Vue from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import type { components } from "@/schema";
|
||||
import { ref } from "vue";
|
||||
import ContentItem from "./ContentItem.vue";
|
||||
|
||||
defineProps({
|
||||
dsc: {
|
||||
type: Object as PropType<components["schemas"]["DCObject"]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const expandCollections = ref({});
|
||||
const expandDatasets = ref({});
|
||||
|
||||
function toggle(expansionMap: Record<string, boolean>, itemId: string) {
|
||||
Vue.set(expansionMap, itemId, !expansionMap[itemId]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ContentItem
|
||||
:id="item.element_index"
|
||||
:item="item.object"
|
||||
:name="item.element_identifier"
|
||||
:is-dataset="item.element_type == 'hda'"
|
||||
:expand-dataset="expandDataset"
|
||||
@update:expand-dataset="expandDataset = $event"
|
||||
@view-collection="viewCollection = !viewCollection" />
|
||||
<GenericItem v-if="viewCollection" :item-id="item.object.id" :item-src="item.object.history_content_type" />
|
||||
<div v-for="(item, index) in dsc.elements" :key="index">
|
||||
<ContentItem
|
||||
:id="item.element_index"
|
||||
:item="item.object"
|
||||
:name="item.element_identifier"
|
||||
:is-dataset="item.element_type == 'hda'"
|
||||
:expand-dataset="!!expandDatasets[item.id]"
|
||||
class="mx-3"
|
||||
@update:expand-dataset="toggle(expandDatasets, item.id)"
|
||||
@view-collection="toggle(expandCollections, item.id)" />
|
||||
<div v-if="!!expandCollections[item.id]" class="mx-3">
|
||||
<GenericElement :dsc="item.object" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentItem from "./ContentItem";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContentItem,
|
||||
GenericItem: () => import("components/History/Content/GenericItem"),
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
viewCollection: false,
|
||||
expandDataset: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
@undelete="onUndelete(item)"
|
||||
@unhide="onUnhide(item)" />
|
||||
<div v-if="viewCollection">
|
||||
<div v-for="(collectionItem, collectionIndex) in item.elements" :key="collectionIndex">
|
||||
<GenericElement :item="collectionItem" />
|
||||
</div>
|
||||
<GenericElement :dsc="item" />
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
|
||||
@@ -33,6 +33,7 @@ export const STATES = {
|
||||
/** metadata discovery/setting failed or errored (but otherwise ok) */
|
||||
failed_metadata: {
|
||||
status: "danger",
|
||||
text: "Metadata generation failed. Please retry.",
|
||||
icon: "exclamation-triangle",
|
||||
},
|
||||
/** was created without a tool */
|
||||
|
||||
@@ -56,6 +56,11 @@ describe("History Navigation", () => {
|
||||
provide: { store },
|
||||
});
|
||||
|
||||
const createButton = wrapper.find("*[data-description='create new history']");
|
||||
expect(createButton.attributes().disabled).toBeFalsy();
|
||||
const switchButton = wrapper.find("*[data-description='switch to another history']");
|
||||
expect(switchButton.attributes().disabled).toBeFalsy();
|
||||
|
||||
const dropDown = wrapper.find("*[data-description='history options']");
|
||||
const optionElements = dropDown.findAll("b-dropdown-item-stub");
|
||||
const optionTexts = optionElements.wrappers.map((el) => el.text());
|
||||
@@ -76,8 +81,12 @@ describe("History Navigation", () => {
|
||||
provide: { store },
|
||||
});
|
||||
|
||||
const dropDown = wrapper.find("*[data-description='history options']");
|
||||
const createButton = wrapper.find("*[data-description='create new history']");
|
||||
expect(createButton.attributes().disabled).toBeTruthy();
|
||||
const switchButton = wrapper.find("*[data-description='switch to another history']");
|
||||
expect(switchButton.attributes().disabled).toBeTruthy();
|
||||
|
||||
const dropDown = wrapper.find("*[data-description='history options']");
|
||||
const enabledOptionElements = dropDown.findAll("b-dropdown-item-stub:not([disabled])");
|
||||
const enabledOptionTexts = enabledOptionElements.wrappers.map((el) => el.text());
|
||||
expect(enabledOptionTexts).toStrictEqual(anonymousOptions);
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
data-description="create new history"
|
||||
size="sm"
|
||||
variant="link"
|
||||
title="Create new history"
|
||||
:disabled="currentUser.isAnonymous"
|
||||
:title="userTitle('Create new history')"
|
||||
@click="$emit('createNewHistory')">
|
||||
<Icon fixed-width icon="plus" />
|
||||
</b-button>
|
||||
@@ -23,7 +24,8 @@
|
||||
data-description="switch to another history"
|
||||
size="sm"
|
||||
variant="link"
|
||||
title="Switch to history">
|
||||
:disabled="currentUser.isAnonymous"
|
||||
:title="userTitle('Switch to history')">
|
||||
<Icon fixed-width icon="exchange-alt" />
|
||||
</b-button>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import SelectorModal from "./SelectorModal";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
const SELECTED_HISTORY_ID = "COOL_ID";
|
||||
const CURRENT_HISTORY_ID = "COOL_ID";
|
||||
const getFakeHistorySummaries = (num, selectedIndex = 0) => {
|
||||
const result = Array.from({ length: num }, (_, index) => ({
|
||||
id: `ID-${index}`,
|
||||
@@ -13,11 +13,11 @@ const getFakeHistorySummaries = (num, selectedIndex = 0) => {
|
||||
tags: [],
|
||||
update_time: new Date().toISOString(),
|
||||
}));
|
||||
result[selectedIndex].id = SELECTED_HISTORY_ID;
|
||||
result[selectedIndex].id = CURRENT_HISTORY_ID;
|
||||
return result;
|
||||
};
|
||||
const PROPS_WITH_10_HISTORIES = {
|
||||
currentHistoryId: SELECTED_HISTORY_ID,
|
||||
currentHistoryId: CURRENT_HISTORY_ID,
|
||||
histories: getFakeHistorySummaries(10),
|
||||
perPage: 3,
|
||||
static: true, // Force the modal visible for testing
|
||||
@@ -27,6 +27,8 @@ const PROPS_WITH_10_HISTORY_MULTIPLE_SELECT = {
|
||||
multiple: true,
|
||||
};
|
||||
|
||||
const CURRENT_HISTORY_INDICATION_TEXT = "(Current)";
|
||||
|
||||
describe("History SelectorModal.vue", () => {
|
||||
let wrapper;
|
||||
|
||||
@@ -38,12 +40,11 @@ describe("History SelectorModal.vue", () => {
|
||||
await flushPromises();
|
||||
}
|
||||
|
||||
it("should highlight the currently selected history", async () => {
|
||||
it("should indicate the currently selected history", async () => {
|
||||
await mountWith(PROPS_WITH_10_HISTORIES);
|
||||
|
||||
const selectedRows = wrapper.findAll(".table-info");
|
||||
expect(selectedRows.length).toBe(1);
|
||||
expect(selectedRows.at(0).attributes("data-pk")).toBe(SELECTED_HISTORY_ID);
|
||||
const currentHistoryRow = wrapper.find(`[data-pk="${CURRENT_HISTORY_ID}"]`);
|
||||
expect(currentHistoryRow.html()).toContain(CURRENT_HISTORY_INDICATION_TEXT);
|
||||
});
|
||||
|
||||
it("paginates the histories", async () => {
|
||||
@@ -67,25 +68,29 @@ describe("History SelectorModal.vue", () => {
|
||||
expect(wrapper.emitted()["selectHistory"][0][0].id).toBe(targetHistoryId);
|
||||
});
|
||||
|
||||
it("select multiple histories", async () => {
|
||||
await mountWith(PROPS_WITH_10_HISTORY_MULTIPLE_SELECT);
|
||||
describe("Multi-selection Mode", () => {
|
||||
it("should select multiple histories", async () => {
|
||||
await mountWith(PROPS_WITH_10_HISTORY_MULTIPLE_SELECT);
|
||||
|
||||
expect(wrapper.emitted()["selectHistories"]).toBeUndefined();
|
||||
expect(wrapper.emitted()["selectHistories"]).toBeUndefined();
|
||||
|
||||
const targetHistoryId1 = "ID-1";
|
||||
const targetRow1 = wrapper.find(`[data-pk="${targetHistoryId1}"]`);
|
||||
await targetRow1.trigger("click");
|
||||
const targetHistoryId1 = "ID-1";
|
||||
const targetRow1 = wrapper.find(`[data-pk="${targetHistoryId1}"]`);
|
||||
await targetRow1.trigger("click");
|
||||
|
||||
const targetHistoryId2 = "ID-2";
|
||||
const targetRow2 = wrapper.find(`[data-pk="${targetHistoryId2}"]`);
|
||||
await targetRow2.trigger("click");
|
||||
const targetHistoryId2 = "ID-2";
|
||||
const targetRow2 = wrapper.find(`[data-pk="${targetHistoryId2}"]`);
|
||||
await targetRow2.trigger("click");
|
||||
|
||||
expect(wrapper.vm.selectedHistories.length).toBe(2);
|
||||
expect(wrapper.vm.selectedHistories.length).toBe(2);
|
||||
|
||||
const button = wrapper.find(".btn-primary");
|
||||
const button = wrapper.find(".btn-primary");
|
||||
|
||||
await button.trigger("click");
|
||||
await button.trigger("click");
|
||||
|
||||
expect(wrapper.emitted()["selectHistories"][0][0][0].id).toBe(targetHistoryId1);
|
||||
expect(wrapper.emitted()["selectHistories"][0][0][0].id).toBe(targetHistoryId1);
|
||||
|
||||
console.debug(wrapper.html());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
primary-key="id"
|
||||
:fields="fields"
|
||||
:filter="filter"
|
||||
:items="formattedItems"
|
||||
:items="histories"
|
||||
:per-page="perPage"
|
||||
:current-page="currentPage"
|
||||
:selectable="true"
|
||||
@@ -22,6 +22,9 @@
|
||||
selected-variant="success"
|
||||
@row-selected="rowSelected"
|
||||
@filtered="onFiltered">
|
||||
<template v-slot:cell(name)="row">
|
||||
{{ row.item.name }} <i v-if="row.item.id === currentHistoryId"><b>(Current)</b></i>
|
||||
</template>
|
||||
<template v-slot:cell(tags)="row">
|
||||
<stateless-tags :value="row.item.tags" :disabled="true" />
|
||||
</template>
|
||||
@@ -31,7 +34,9 @@
|
||||
</b-table>
|
||||
<template v-slot:modal-footer>
|
||||
<b-pagination v-model="currentPage" :total-rows="totalRows" :per-page="perPage" />
|
||||
<b-button v-if="multiple" variant="primary" @click="addSelected">Add Selected</b-button>
|
||||
<b-button v-if="multiple" :disabled="isEmptySelection" variant="primary" @click="addSelected">
|
||||
Add Selected
|
||||
</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
</template>
|
||||
@@ -69,13 +74,8 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
formattedItems() {
|
||||
return this.histories.map((item) => {
|
||||
if (item.id === this.currentHistoryId) {
|
||||
item._rowVariant = "info";
|
||||
}
|
||||
return item;
|
||||
});
|
||||
isEmptySelection() {
|
||||
return this.selectedHistories.length === 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
### History Panel Component Tree
|
||||
|
||||
This is not intended to be a complete listing, but a general idea of how the components are intended
|
||||
to interact with each other.
|
||||
|
||||
```html static
|
||||
<Index :history="history">
|
||||
|
||||
<!-- if main history selected -->
|
||||
<CurrentHistory :history="history">
|
||||
|
||||
<!-- Data providers do the heavy-lifting of mixing params, history, and
|
||||
scroll position to deliver the content for the scroller -->
|
||||
<StoreProvider>
|
||||
@@ -18,7 +17,7 @@ to interact with each other.
|
||||
|
||||
<!-- Uses a virtual scroller plugin to render all ContentItems and throttles the scrolling pace by limiting
|
||||
the frequency and magnitude of offset changes. -->
|
||||
<Listing>
|
||||
<listing>
|
||||
|
||||
<!-- The ContentItem renders a row in the list, showing the title, some attributes,
|
||||
and basic operation buttons such as display and edit for either a dataset or a collection.
|
||||
@@ -40,11 +39,11 @@ to interact with each other.
|
||||
<CollectionOperations />
|
||||
|
||||
<!-- As above, the same virtual scroller and ContentItem component is being used to render the elements. -->
|
||||
<Listing>
|
||||
<listing>
|
||||
(<ContentItem />)
|
||||
<DatasetDetails />
|
||||
</Listing>
|
||||
</StoreProvider>
|
||||
</CurrentCollection>
|
||||
</Index>
|
||||
```
|
||||
```
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
<b
|
||||
><a class="generated-export-link" :href="link">{{ link }}</a></b
|
||||
>
|
||||
<font-awesome-icon
|
||||
v-b-tooltip.hover
|
||||
title="Copy export URL to your clipboard"
|
||||
class="copy-export-link"
|
||||
icon="link"
|
||||
style="cursor: pointer"
|
||||
@click="copyUrl" />
|
||||
<span v-b-tooltip.hover title="Copy export URL to your clipboard">
|
||||
<font-awesome-icon class="copy-export-link" icon="link" style="cursor: pointer" @click="copyUrl" />
|
||||
</span>
|
||||
<i
|
||||
title="Information about when the history export was generated is included in the job details. Additionally, if there are issues with export, the job details may help figure out the underlying problem or communicate issues to your Galaxy administrator.">
|
||||
(<b-link class="show-job-link" href="#" @click="showDetails">view job details</b-link>)
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
<template>
|
||||
<span>
|
||||
<font-awesome-icon
|
||||
v-if="object.published"
|
||||
v-b-tooltip.hover
|
||||
:title="'Published' | localize"
|
||||
icon="globe"
|
||||
class="sharing-indicator-published"
|
||||
@click="$emit('filter', 'is:published')" />
|
||||
<font-awesome-icon
|
||||
v-if="object.shared"
|
||||
v-b-tooltip.hover
|
||||
:title="'Shared' | localize"
|
||||
icon="share-alt"
|
||||
class="sharing-indicator-shared"
|
||||
@click="$emit('filter', 'is:shared_with_me')" />
|
||||
<span v-if="object.published" v-b-tooltip.hover :title="'Published' | localize">
|
||||
<font-awesome-icon
|
||||
icon="globe"
|
||||
class="sharing-indicator-published"
|
||||
@click="$emit('filter', 'is:published')" />
|
||||
</span>
|
||||
<span v-if="object.shared" v-b-tooltip.hover :title="'Shared' | localize">
|
||||
<font-awesome-icon
|
||||
icon="share-alt"
|
||||
class="sharing-indicator-shared"
|
||||
@click="$emit('filter', 'is:shared_with_me')" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ th:focus {
|
||||
}
|
||||
|
||||
.pagination-input-field {
|
||||
max-width: 60px
|
||||
max-width: 60px;
|
||||
}
|
||||
|
||||
.pagination-total-pages-text {
|
||||
margin-left: .25rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.more-text-btn {
|
||||
margin-left: .25rem;
|
||||
margin-left: 0.25rem;
|
||||
font-size: 60%;
|
||||
color: grey;
|
||||
}
|
||||
@@ -20,7 +20,6 @@ th:focus {
|
||||
width: 40rem;
|
||||
}
|
||||
|
||||
|
||||
.empty-folder-message {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -34,5 +33,5 @@ th:focus {
|
||||
}
|
||||
|
||||
.lib-btn {
|
||||
margin-bottom: 2%
|
||||
margin-bottom: 2%;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
<b-form-select v-else v-model="license" :options="licenseOptions"></b-form-select>
|
||||
<License v-if="currentLicenseInfo" :license-id="license" :input-license-info="currentLicenseInfo">
|
||||
<template v-slot:buttons>
|
||||
<font-awesome-icon v-b-tooltip.hover title="Save License" icon="save" @click="onSave" />
|
||||
<font-awesome-icon v-b-tooltip.hover title="Cancel Edit" icon="times" @click="disableEdit" />
|
||||
<span v-b-tooltip.hover title="Save License"><font-awesome-icon icon="save" @click="onSave" /></span>
|
||||
<span v-b-tooltip.hover title="Cancel Edit"
|
||||
><font-awesome-icon icon="times" @click="disableEdit"
|
||||
/></span>
|
||||
</template>
|
||||
</License>
|
||||
<div v-else>
|
||||
@@ -16,7 +18,9 @@
|
||||
<div v-else-if="license">
|
||||
<License :license-id="license">
|
||||
<template v-slot:buttons>
|
||||
<font-awesome-icon v-b-tooltip.hover title="Edit License" icon="edit" @click="editLicense = true" />
|
||||
<span v-b-tooltip.hover title="Edit License"
|
||||
><font-awesome-icon icon="edit" @click="editLicense = true"
|
||||
/></span>
|
||||
</template>
|
||||
</License>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<span>
|
||||
<span :class="spinnerClasses"></span>
|
||||
<span class="loading-message">{{ message }}.<span class="blinking">..</span></span>
|
||||
<span :class="spinnerClasses" title="loading"></span>
|
||||
<span v-if="!spinnerOnly" class="loading-message">{{ message }}.<span class="blinking">..</span></span>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
@@ -16,6 +16,11 @@ export default {
|
||||
type: String,
|
||||
default: "Loading",
|
||||
},
|
||||
spinnerOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
spinnerClasses() {
|
||||
|
||||
@@ -59,9 +59,9 @@ export default {
|
||||
computed: {
|
||||
targetClass() {
|
||||
if (this.toolKey) {
|
||||
return `tool-menu-item-${this.tool[this.toolKey]} title-link`;
|
||||
return `tool-menu-item-${this.tool[this.toolKey]} title-link cursor-pointer`;
|
||||
} else {
|
||||
return `title-link`;
|
||||
return `title-link cursor-pointer`;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
this.$emit("onResults", this.favoritesResults);
|
||||
} else {
|
||||
// keys with sorting order
|
||||
const keys = { exact: 2, name: 1, description: 0 };
|
||||
const keys = { exact: 3, name: 2, description: 1, combined: 0 };
|
||||
this.$emit("onResults", searchToolsByKeys(this.toolsList, keys, q));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -137,9 +137,19 @@ export default {
|
||||
this.config.toolbox_auto_sort === true &&
|
||||
!this.category.elems.some((el) => el.text !== undefined && el.text !== "")
|
||||
) {
|
||||
return Object.entries(
|
||||
[...this.category.elems].sort((a, b) => a.name.toLowerCase() > b.name.toLowerCase())
|
||||
);
|
||||
const elements = [...this.category.elems];
|
||||
const sorted = elements.sort((a, b) => {
|
||||
const aNameLower = a.name.toLowerCase();
|
||||
const bNameLower = b.name.toLowerCase();
|
||||
if (aNameLower > bNameLower) {
|
||||
return 1;
|
||||
} else if (aNameLower < bNameLower) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
return Object.entries(sorted);
|
||||
} else {
|
||||
return Object.entries(this.category.elems);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<tool-search
|
||||
:current-panel-view="currentPanelView"
|
||||
placeholder="search tools"
|
||||
:toolbox="toolbox"
|
||||
:toolbox="workflowTools"
|
||||
:query="query"
|
||||
@onQuery="onQuery"
|
||||
@onResults="onResults" />
|
||||
@@ -74,7 +74,7 @@
|
||||
import _l from "utils/localization";
|
||||
import ToolSection from "./Common/ToolSection";
|
||||
import ToolSearch from "./Common/ToolSearch";
|
||||
import { filterToolSections } from "./utilities";
|
||||
import { filterToolSections, removeDisabledTools } from "./utilities";
|
||||
import PanelViewButton from "./Buttons/PanelViewButton";
|
||||
|
||||
export default {
|
||||
@@ -140,12 +140,13 @@ export default {
|
||||
};
|
||||
},
|
||||
sections() {
|
||||
return filterToolSections(this.toolsLayout, this.results);
|
||||
return filterToolSections(this.workflowTools, this.results);
|
||||
},
|
||||
toolsLayout() {
|
||||
return this.toolbox.map((section) => {
|
||||
return {
|
||||
...section,
|
||||
disabled: !section.elems && !section.is_workflow_compatible,
|
||||
elems:
|
||||
section.elems &&
|
||||
section.elems.map((el) => {
|
||||
@@ -155,6 +156,9 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
workflowTools() {
|
||||
return removeDisabledTools(this.toolsLayout);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onQuery(query) {
|
||||
|
||||
@@ -89,7 +89,12 @@ export function searchToolsByKeys(tools, keys, query) {
|
||||
const returnedTools = [];
|
||||
for (const tool of tools) {
|
||||
for (const key of Object.keys(keys)) {
|
||||
const actualValue = tool[key] ? tool[key].toLowerCase() : "";
|
||||
let actualValue = "";
|
||||
if (key === "combined") {
|
||||
actualValue = tool.name.toLowerCase() + " " + tool.description.toLowerCase();
|
||||
} else {
|
||||
actualValue = tool[key] ? tool[key].toLowerCase() : "";
|
||||
}
|
||||
const queryLowerCase = query.toLowerCase();
|
||||
if (actualValue.match(queryLowerCase)) {
|
||||
// do we care for exact matches && is it an exact match ?
|
||||
@@ -109,6 +114,22 @@ export function normalizeTools(tools) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
export function removeDisabledTools(tools) {
|
||||
return tools.filter((section) => {
|
||||
if (section.model_class === "ToolSectionLabel") {
|
||||
return true;
|
||||
} else if (!section.elems && section.disabled) {
|
||||
return false;
|
||||
} else if (section.elems) {
|
||||
section.elems = section.elems.filter((el) => !el.disabled);
|
||||
if (!section.elems.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function flattenToolsSection(section) {
|
||||
const flattenTools = [];
|
||||
if (section.elems) {
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("test helpers in tool searching utilities", () => {
|
||||
});
|
||||
|
||||
it("test tool search helper that searches for tools given keys", async () => {
|
||||
const q = "collection";
|
||||
let q = "collection";
|
||||
let expectedResults = [
|
||||
"__FILTER_FAILED_DATASETS__",
|
||||
"__FILTER_EMPTY_DATASETS__",
|
||||
@@ -37,6 +37,12 @@ describe("test helpers in tool searching utilities", () => {
|
||||
keys = { description: 0, name: 1 };
|
||||
results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
|
||||
expect(results).toEqual(expectedResults);
|
||||
|
||||
q = "filter empty datasets";
|
||||
expectedResults = ["__FILTER_EMPTY_DATASETS__"];
|
||||
keys = { description: 1, name: 2, combined: 0 };
|
||||
results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
|
||||
expect(results).toEqual(expectedResults);
|
||||
});
|
||||
|
||||
it("test tool filtering helpers on toolsList given list of ids", async () => {
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
<div v-for="(creator, index) in creatorsCurrent" :key="index">
|
||||
<CreatorViewer :creator="creator">
|
||||
<template v-slot:buttons>
|
||||
<font-awesome-icon v-b-tooltip.hover title="Edit Creator" icon="edit" @click="onEdit(index)" />
|
||||
<font-awesome-icon
|
||||
v-b-tooltip.hover
|
||||
title="Remove Creator"
|
||||
icon="times"
|
||||
@click="onRemove(index)" />
|
||||
<span v-b-tooltip.hover title="Edit Creator"
|
||||
><font-awesome-icon icon="edit" @click="onEdit(index)"
|
||||
/></span>
|
||||
<span v-b-tooltip.hover title="Remove Creator">
|
||||
<font-awesome-icon icon="times" @click="onRemove(index)" />
|
||||
</span>
|
||||
</template>
|
||||
</CreatorViewer>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
<b-form @submit="onSave" @reset="onReset">
|
||||
<div v-for="attribute in displayedAttributes" :key="attribute.key" role="group" class="form-group">
|
||||
<label :for="attribute.key">{{ attribute.label }}</label>
|
||||
<font-awesome-icon
|
||||
v-b-tooltip.hover
|
||||
title="Hide Attribute"
|
||||
icon="eye-slash"
|
||||
@click="onHide(attribute.key)" />
|
||||
<span v-b-tooltip.hover title="Hide Attribute"
|
||||
><font-awesome-icon icon="eye-slash" @click="onHide(attribute.key)"
|
||||
/></span>
|
||||
<b-form-input
|
||||
:id="attribute.key"
|
||||
v-model="currentValues[attribute.key]"
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
<span v-else-if="email" itemprop="email" :content="organization.email">
|
||||
{{ email }}
|
||||
</span>
|
||||
<a v-if="url" :href="url" target="_blank">
|
||||
<a v-if="url" v-b-tooltip.hover title="Organization URL" :href="url" target="_blank">
|
||||
<link itemprop="url" :href="url" />
|
||||
<font-awesome-icon v-b-tooltip.hover title="Organization URL" icon="external-link-alt" />
|
||||
<font-awesome-icon icon="external-link-alt" />
|
||||
</a>
|
||||
<meta
|
||||
v-for="attribute in explicitMetaAttributes"
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
<b-form @submit="onSave" @reset="onReset">
|
||||
<div v-for="attribute in displayedAttributes" :key="attribute.key" role="group" class="form-group">
|
||||
<label :for="attribute.key">{{ attribute.label }}</label>
|
||||
<font-awesome-icon
|
||||
v-b-tooltip.hover
|
||||
title="Hide Attribute"
|
||||
icon="eye-slash"
|
||||
@click="onHide(attribute.key)" />
|
||||
<span v-b-tooltip.hover title="Hide Attribute"
|
||||
><font-awesome-icon icon="eye-slash" @click="onHide(attribute.key)"
|
||||
/></span>
|
||||
<b-form-input
|
||||
:id="attribute.key"
|
||||
v-model="currentValues[attribute.key]"
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
<span v-else itemprop="email" :content="person.email">
|
||||
{{ email }}
|
||||
</span>
|
||||
<a v-if="orcidLink" :href="orcidLink" target="_blank">
|
||||
<a v-if="orcidLink" v-b-tooltip.hover title="View orcid.org profile" :href="orcidLink" target="_blank">
|
||||
<link itemprop="identifier" :href="orcidLink" />
|
||||
<font-awesome-icon v-b-tooltip.hover title="View orcid.org profile" :icon="['fab', 'orcid']" />
|
||||
<font-awesome-icon :icon="['fab', 'orcid']" />
|
||||
</a>
|
||||
<a v-if="url" :href="url" target="_blank">
|
||||
<a v-if="url" v-b-tooltip.hover title="URL" :href="url" target="_blank">
|
||||
<link itemprop="url" :href="url" />
|
||||
<font-awesome-icon v-b-tooltip.hover title="URL" icon="external-link-alt" />
|
||||
<font-awesome-icon icon="external-link-alt" />
|
||||
</a>
|
||||
<meta
|
||||
v-for="attribute in explicitMetaAttributes"
|
||||
|
||||
@@ -163,6 +163,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
disabled: false,
|
||||
initialized: false,
|
||||
showLoading: true,
|
||||
showForm: false,
|
||||
showEntryPoints: false,
|
||||
@@ -225,7 +226,7 @@ export default {
|
||||
watch: {
|
||||
getLatestCreateTime() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy && Galaxy.currHistoryPanel) {
|
||||
if (this.initialized && Galaxy && Galaxy.currHistoryPanel) {
|
||||
console.debug("History change watcher detected a change.");
|
||||
this.onHistoryChange();
|
||||
}
|
||||
@@ -233,6 +234,7 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.requestTool().then(() => {
|
||||
this.initialized = true;
|
||||
console.debug(`ToolForm::created - Started listening to history changes. [${this.id}]`);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="ui-thumbnails">
|
||||
<div>
|
||||
<h1 class="h-lg">Galaxy Tours</h1>
|
||||
<p>
|
||||
This page presents a list of interactive tours available on this Galaxy server. Select any tour to get
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getAppRoot } from "onload";
|
||||
import { useCurrentUser } from "composables/user";
|
||||
import { useUserHistories } from "composables/userHistories";
|
||||
import { useConfig } from "composables/config";
|
||||
import { wait } from "@/utils/wait";
|
||||
|
||||
const { currentUser } = useCurrentUser();
|
||||
const { currentHistoryId } = useUserHistories(currentUser);
|
||||
@@ -51,12 +52,6 @@ function dismiss(result) {
|
||||
showModal.value = false;
|
||||
}
|
||||
|
||||
function wait(milliseconds) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), milliseconds);
|
||||
});
|
||||
}
|
||||
|
||||
async function open(overrideOptions) {
|
||||
const newOptions = overrideOptions ?? {};
|
||||
|
||||
|
||||
@@ -218,7 +218,6 @@ export default {
|
||||
},
|
||||
immediateUpload: function (files) {
|
||||
this.$refs.regular?.addFiles(files);
|
||||
this.$refs.regular?._eventStart();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<b-row class="ml-3 mb-1">
|
||||
<i class="pref-icon pt-1 fa fa-lg fa-broadcast-tower" />
|
||||
<div class="pref-content pr-1">
|
||||
<a id="beacon-settings" href="javascript:void(0)"><b v-b-modal.modal-beacon v-localize>Manage Beacon</b></a>
|
||||
<div v-localize class="form-text text-muted">Contribute variants to Beacon</div>
|
||||
<b-modal
|
||||
id="modal-beacon"
|
||||
ref="modal"
|
||||
size="xl"
|
||||
ok-only
|
||||
title="Manage Beacon"
|
||||
title-tag="h1"
|
||||
@show="onOpenModal">
|
||||
<!-- Explanation text-->
|
||||
<p>
|
||||
The
|
||||
<a href="https://beacon-project.io">Global Alliance for Genomics & Health Beacon Project</a> enables
|
||||
safe sharing of human genetic variants.<br />
|
||||
<br />
|
||||
Galaxy lets you use the Beacon protocol to share genetic variants directly from your analysis with
|
||||
the scientific community in the following anonymous way:<br />
|
||||
<br />
|
||||
For participating users, we will merge variant lists to be shared into a single Beacon dataset per
|
||||
reference genome and make that dataset accessible through a Beacon server.<br />
|
||||
If someone queries the server for a specific variant that is in our Beacon dataset, the server will
|
||||
reply with
|
||||
<span class="cursive">“Yes, we have seen such a variant”</span>.<br />
|
||||
<br />
|
||||
The user that issued the query then has the possibility to contact a Galaxy server admin who can
|
||||
link the variant call in question to particular Galaxy users. If you are among the users that shared
|
||||
the variant, the admin will, in turn, contact you and ask if you want to contact the user that
|
||||
initiated the query to negotiate further information exchange or data access.
|
||||
</p>
|
||||
|
||||
<b-alert v-if="enabled" show>
|
||||
<div class="flex-row space-between">
|
||||
<div class="no-shrink">
|
||||
Beacon sharing is <span class="bold">enabled</span> for your profile
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<div class="no-shrink">
|
||||
<b-button variant="danger" @click="optOut">Disable</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</b-alert>
|
||||
|
||||
<!-- Setting to show when beacon is disabled -->
|
||||
<b-alert v-if="!enabled" show>
|
||||
<div class="flex-row space-between">
|
||||
<div class="no-shrink">
|
||||
Beacon sharing is currently <span class="bold">disabled</span> - no data will be shared
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<div>
|
||||
<b-button variant="success" @click="optIn">Enable</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</b-alert>
|
||||
|
||||
<div v-if="enabled">
|
||||
<p>
|
||||
You can share data by copying VCF or VCF.bgzip files to a history called
|
||||
<span class="cursive gray-background">{{ beaconHistoryName }}</span
|
||||
>. <br />
|
||||
<br />
|
||||
The Beacon database is rebuilt periodically. Therefore, changes do not go into effect
|
||||
immediately. If you disable beacon sharing or remove a dataset from the beacon history, the
|
||||
corresponding variants will disappear from the beacon dataset during the next rebuild.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Detailed information about the beacon history -->
|
||||
<div v-if="enabled" class="gray-box">
|
||||
<!-- Case: History does not exist-->
|
||||
<div v-if="beaconHistories.length < 1" class="flex-row history-entry">
|
||||
<div class="no-shrink">No beacon history found</div>
|
||||
<div class="fill"></div>
|
||||
<div class="no-shrink">
|
||||
<b-button @click="createBeaconHistory">Create Beacon History</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Case: History exists -->
|
||||
<div
|
||||
v-for="beaconHistory in beaconHistories"
|
||||
:key="beaconHistory.id"
|
||||
class="flex-row history-entry"
|
||||
:class="{
|
||||
'gray-border-bottom': beaconHistory.id !== beaconHistories[beaconHistories.length - 1].id,
|
||||
}">
|
||||
<div v-if="beaconHistory.contents" class="no-shrink">
|
||||
History with {{ beaconHistory.contents.length }} datasets
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<div class="no-shrink">
|
||||
<b-button @click="switchHistory(beaconHistory.id)">Switch to History</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="enabled">
|
||||
<p>Datasets must fulfill the following conditions in order to be processed</p>
|
||||
<ul>
|
||||
<li>must be VCF or VCF.bgzip format</li>
|
||||
<li>must have a human reference assigned to it (e.g. hg19)</li>
|
||||
<li>must define at least one sample in a dedicated genotype column</li>
|
||||
<li>
|
||||
must contain the info field <span class="cursive">AC</span>, with the total number of
|
||||
alternate alleles in called genotypes
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from "store";
|
||||
import axios from "axios";
|
||||
|
||||
import { withPrefix } from "utils/redirect";
|
||||
import { BAlert, BButton, BModal, BRow } from "bootstrap-vue";
|
||||
|
||||
export default {
|
||||
components: { BButton, BModal, BRow, BAlert },
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
enabled: false,
|
||||
beaconHistoryName: "Beacon Export 📡",
|
||||
beaconHistories: [{}],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
switchHistory: async function (historyId) {
|
||||
await store.dispatch("history/setCurrentHistory", historyId);
|
||||
},
|
||||
getBeaconHistories: function () {
|
||||
axios
|
||||
.get(withPrefix("api/histories?&keys=id,contents&q=name&qv=" + encodeURI(this.beaconHistoryName)))
|
||||
.then((response) => {
|
||||
this.beaconHistories = this.removeDeletedContents(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error.response);
|
||||
});
|
||||
},
|
||||
removeDeletedContents: function (beaconHistories) {
|
||||
beaconHistories.forEach((beaconHistory) => {
|
||||
beaconHistory.contents.splice(
|
||||
0,
|
||||
beaconHistory.contents.length,
|
||||
...beaconHistory.contents.filter((dataset) => {
|
||||
return !dataset.deleted;
|
||||
})
|
||||
);
|
||||
});
|
||||
return beaconHistories;
|
||||
},
|
||||
createBeaconHistory: function () {
|
||||
const annotation =
|
||||
"Variants will be collected from VCF datasets in this history if beacon sharing is activated";
|
||||
axios
|
||||
.post(withPrefix("api/histories"), { name: this.beaconHistoryName })
|
||||
.then((response) => {
|
||||
axios.put(withPrefix(`api/histories/${response.data.id}`), { annotation: annotation }).then(() => {
|
||||
this.getBeaconHistories();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.errorMessages.push(error.response.data.err_msg), console.log(error.response);
|
||||
});
|
||||
},
|
||||
optIn() {
|
||||
try {
|
||||
axios.post(withPrefix(`api/users/${this.userId}/beacon`), { enabled: true }).then((response) => {
|
||||
// TODO check response
|
||||
this.loadSettings();
|
||||
if (this.beaconHistories.length < 1) {
|
||||
this.createBeaconHistory();
|
||||
this.getBeaconHistories();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
optOut() {
|
||||
try {
|
||||
axios.post(withPrefix(`api/users/${this.userId}/beacon`), { enabled: false }).then((response) => {
|
||||
// TODO check response
|
||||
this.loadSettings();
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
onOpenModal() {
|
||||
this.loadSettings();
|
||||
this.getBeaconHistories();
|
||||
},
|
||||
async loadSettings() {
|
||||
try {
|
||||
await axios.get(withPrefix(`api/users/${this.userId}/beacon`)).then((response) => {
|
||||
this.enabled = response.data.enabled;
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
span.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pref-icon {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.gray-box {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
border: 1px solid #bdc6d0;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
.history-entry {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.gray-border-bottom {
|
||||
border-bottom: 1px solid #bdc6d0;
|
||||
}
|
||||
|
||||
.space-between {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cursive {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.fill {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-shrink {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -82,6 +82,11 @@
|
||||
description="Click here to make all data private."
|
||||
@click="makeDataPrivate" />
|
||||
</ConfigProvider>
|
||||
<ConfigProvider v-slot="{ config }">
|
||||
<UserBeaconSettings v-if="config && config.enable_beacon_integration" :user-id="userId"
|
||||
>>
|
||||
</UserBeaconSettings>
|
||||
</ConfigProvider>
|
||||
<ConfigProvider v-slot="{ config }">
|
||||
<UserDeletion
|
||||
v-if="config && !config.single_user && config.enable_account_interface"
|
||||
@@ -115,6 +120,7 @@ import UserDeletion from "./UserDeletion";
|
||||
import UserPreferencesElement from "./UserPreferencesElement";
|
||||
|
||||
import "@fortawesome/fontawesome-svg-core";
|
||||
import UserBeaconSettings from "./UserBeaconSettings";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
@@ -124,6 +130,7 @@ export default {
|
||||
UserDeletion,
|
||||
UserPreferencesElement,
|
||||
ThemeSelector,
|
||||
UserBeaconSettings,
|
||||
},
|
||||
props: {
|
||||
userId: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="ui-thumbnails">
|
||||
<div class="plugin-list">
|
||||
<div v-if="error" class="alert alert-danger">{{ error }}</div>
|
||||
<div v-else>
|
||||
<DelayedInput
|
||||
@@ -10,18 +10,14 @@
|
||||
@change="onSearch" />
|
||||
<div v-for="plugin in plugins" :key="plugin.name">
|
||||
<table v-if="match(plugin)">
|
||||
<tr class="ui-thumbnails-item" @click="select(plugin)">
|
||||
<tr class="plugin-list-item" :data-plugin-name="plugin.name" @click="select(plugin)">
|
||||
<td>
|
||||
<img
|
||||
v-if="plugin.logo"
|
||||
alt="ui thumbnails"
|
||||
class="ui-thumbnails-image"
|
||||
:src="plugin.logo" />
|
||||
<div v-else class="ui-thumbnails-icon fa fa-eye" />
|
||||
<img v-if="plugin.logo" alt="ui thumbnails" class="plugin-list-image" :src="plugin.logo" />
|
||||
<div v-else class="plugin-list-icon fa fa-eye" />
|
||||
</td>
|
||||
<td>
|
||||
<div class="ui-thumbnails-title font-weight-bold">{{ plugin.html }}</div>
|
||||
<div class="ui-thumbnails-text">{{ plugin.description }}</div>
|
||||
<div class="plugin-list-title font-weight-bold">{{ plugin.html }}</div>
|
||||
<div class="plugin-list-text">{{ plugin.description }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!fixed">
|
||||
@@ -155,3 +151,38 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "theme/blue.scss";
|
||||
@import "~scss/mixins.scss";
|
||||
|
||||
.plugin-list {
|
||||
.plugin-list-item {
|
||||
cursor: pointer;
|
||||
.plugin-list-image {
|
||||
padding: 1rem;
|
||||
width: 5rem;
|
||||
height: 4.3rem;
|
||||
}
|
||||
.plugin-list-icon {
|
||||
padding: 1rem;
|
||||
width: 5rem;
|
||||
height: 4.3rem;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
color: $text-color;
|
||||
}
|
||||
.plugin-list-text {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
.plugin-list-item:hover {
|
||||
.plugin-list-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.plugin-list-text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,7 +24,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import { computed, onMounted, ref, watch, type ComputedRef } from "vue";
|
||||
import { type OutputTerminals, type InputTerminals, terminalFactory } from "./modules/terminals";
|
||||
import {
|
||||
type OutputTerminals,
|
||||
type InputTerminalsAndInvalid,
|
||||
terminalFactory,
|
||||
type InputTerminals,
|
||||
} from "./modules/terminals";
|
||||
import { useFocusWithin } from "@/composables/useActiveElement";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
|
||||
@@ -79,7 +84,7 @@ function decrement() {
|
||||
}
|
||||
}
|
||||
|
||||
function terminalToInputObject(terminal: InputTerminals, connected: boolean): InputObject {
|
||||
function terminalToInputObject(terminal: InputTerminalsAndInvalid, connected: boolean): InputObject {
|
||||
const step = stepStore.getStep(terminal.stepId);
|
||||
assertDefined(step);
|
||||
const inputLabel = `${terminal.name} in step ${step.id + 1}: ${step.label}`;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<g :id="id" class="ribbon">
|
||||
<g :id="id" :class="ribbonClasses">
|
||||
<g v-for="svgLine in paths" :key="svgLine">
|
||||
<path class="ribbon-outer" :d="svgLine" :stroke-width="stroke.outerStroke" fill="none"></path>
|
||||
<path class="ribbon-inner" :d="svgLine" :stroke-width="stroke.innerStroke" fill="none"></path>
|
||||
<path :class="innerClass" :d="svgLine" :stroke-width="stroke.innerStroke" fill="none"></path>
|
||||
</g>
|
||||
</g>
|
||||
</template>
|
||||
@@ -17,11 +17,15 @@ const props = withDefaults(
|
||||
position: TerminalPosition;
|
||||
inputIsMappedOver: boolean;
|
||||
outputIsMappedOver: boolean;
|
||||
connectionIsValid?: boolean;
|
||||
nullable?: boolean;
|
||||
}>(),
|
||||
{
|
||||
id: undefined,
|
||||
inputIsMappedOver: false,
|
||||
outputIsMappedOver: false,
|
||||
connectionIsValid: true,
|
||||
nullable: false,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -40,6 +44,9 @@ const stroke = computed(() => {
|
||||
return { innerStroke, outerStroke };
|
||||
});
|
||||
|
||||
const ribbonClasses = computed(() => (props.nullable ? "ribbon dashed" : "ribbon"));
|
||||
const innerClass = computed(() => (props.connectionIsValid ? "ribbon-inner" : "ribbon-inner ribbon-inner-invalid"));
|
||||
|
||||
const offsets = computed(() => {
|
||||
const _offsets = [-2 * ribbonMargin, -ribbonMargin, 0, ribbonMargin, 2 * ribbonMargin];
|
||||
let startOffsets = [0];
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import FormElement from "@/components/Form/FormElement.vue";
|
||||
import { computed } from "vue";
|
||||
import type { Step } from "@/stores/workflowStepStore";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onUpdateStep", value: Step): void;
|
||||
}>();
|
||||
const props = defineProps<{
|
||||
step: Step;
|
||||
}>();
|
||||
|
||||
const conditionalDefined = computed(() => {
|
||||
return Boolean(props.step.when);
|
||||
});
|
||||
|
||||
function onSkipBoolean(value: boolean) {
|
||||
if (props.step.when && value === false) {
|
||||
emit("onUpdateStep", { ...props.step, when: undefined });
|
||||
} else if (value === true && !props.step.when) {
|
||||
const when = "${inputs.when}";
|
||||
const newStep = {
|
||||
...props.step,
|
||||
when,
|
||||
input_connections: { ...props.step.input_connections, when: undefined },
|
||||
};
|
||||
emit("onUpdateStep", newStep);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormElement
|
||||
id="__conditional"
|
||||
:value="conditionalDefined"
|
||||
title="Conditionally skip step?"
|
||||
help="Set to true and connect a boolean parameter that determines whether step will be skipped"
|
||||
type="boolean"
|
||||
@input="onSkipBoolean"></FormElement>
|
||||
<!-- We don't seem to have a disabled text field
|
||||
<FormElement
|
||||
v-if="setConditional"
|
||||
id="__when"
|
||||
:value="step.when"
|
||||
type="text"
|
||||
help="Step will be executed if javascript expression below evaluates to true."
|
||||
>
|
||||
</FormElement>
|
||||
-->
|
||||
</template>
|
||||
@@ -40,7 +40,7 @@ describe("FormDefault", () => {
|
||||
const title = wrapper.find(".portlet-title-text").text();
|
||||
expect(title).toBe("label");
|
||||
const inputCount = wrapper.findAll("input").length;
|
||||
expect(inputCount).toBe(3);
|
||||
expect(inputCount).toBe(4);
|
||||
const outputLabelCount = wrapper.findAll("#__label__output-name").length;
|
||||
expect(outputLabelCount).toBe(1);
|
||||
const otherLabelCount = wrapper.findAll("#__label__other-name").length;
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
:area="true"
|
||||
help="Add an annotation or notes to this step. Annotations are available when a workflow is viewed."
|
||||
@input="onAnnotation" />
|
||||
<FormConditional v-if="isSubworkflow" :step="step" v-on="$listeners" />
|
||||
<FormDisplay
|
||||
v-if="configForm?.inputs"
|
||||
:id="formDisplayId"
|
||||
@@ -61,6 +62,7 @@ import FormDisplay from "@/components/Form/FormDisplay.vue";
|
||||
import FormCard from "@/components/Form/FormCard.vue";
|
||||
import FormElement from "@/components/Form/FormElement.vue";
|
||||
import FormOutputLabel from "@/components/Workflow/Editor/Forms/FormOutputLabel.vue";
|
||||
import FormConditional from "./FormConditional.vue";
|
||||
import WorkflowIcons from "@/components/Workflow/icons";
|
||||
import { useWorkflowStepStore, type Step } from "@/stores/workflowStepStore";
|
||||
import { useUniqueLabelError } from "../composables/useUniqueLabelError";
|
||||
@@ -77,7 +79,16 @@ const stepRef = toRef(props, "step");
|
||||
const { stepId, contentId, annotation, label, name, type, configForm } = useStepProps(stepRef);
|
||||
const stepStore = useWorkflowStepStore();
|
||||
const uniqueErrorLabel = useUniqueLabelError(stepStore, label?.value);
|
||||
const stepTitle = computed(() => (label?.value || contentId?.value || name.value)!);
|
||||
const stepTitle = computed(() => {
|
||||
if (label?.value) {
|
||||
return label.value;
|
||||
}
|
||||
if (isSubworkflow.value) {
|
||||
return name.value;
|
||||
} else {
|
||||
return contentId?.value || name.value;
|
||||
}
|
||||
});
|
||||
const nodeIcon = computed(() => WorkflowIcons[type.value]);
|
||||
const formDisplayId = computed(() => stepId.value.toString());
|
||||
const isSubworkflow = computed(() => type.value === "subworkflow");
|
||||
|
||||
@@ -11,9 +11,7 @@ import { createTestingPinia } from "@pinia/testing";
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("FormTool", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
function mountTarget() {
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
user: mockModule(userStore),
|
||||
@@ -21,7 +19,7 @@ describe("FormTool", () => {
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = mount(FormTool, {
|
||||
return mount(FormTool, {
|
||||
propsData: {
|
||||
id: "input",
|
||||
datatypes: [],
|
||||
@@ -32,7 +30,7 @@ describe("FormTool", () => {
|
||||
name: "tool_name",
|
||||
version: "1.0",
|
||||
description: "description",
|
||||
inputs: [],
|
||||
inputs: [{ name: "input", label: "input", type: "text", value: "value" }],
|
||||
help: "help_text",
|
||||
versions: ["1.0", "2.0", "3.0"],
|
||||
citations: false,
|
||||
@@ -46,15 +44,16 @@ describe("FormTool", () => {
|
||||
stubs: {
|
||||
CurrentUser: MockCurrentUser({ id: "fakeuser" }),
|
||||
ConfigProvider: MockConfigProvider({ id: "fakeconfig" }),
|
||||
FormElement: { template: "<div>form-element</div>" },
|
||||
ToolFooter: { template: "<div>tool-footer</div>" },
|
||||
},
|
||||
pinia: createTestingPinia(),
|
||||
provide: { store },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("changes between different versions", async () => {
|
||||
const wrapper = mountTarget();
|
||||
|
||||
const dropdowns = wrapper.findAll(".tool-versions .dropdown-item");
|
||||
let version = dropdowns.at(1);
|
||||
expect(version.text()).toBe("Switch to 2.0");
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
:area="true"
|
||||
help="Add an annotation or notes to this step. Annotations are available when a workflow is viewed."
|
||||
@input="onAnnotation" />
|
||||
<FormConditional :step="step" v-on="$listeners" />
|
||||
<div class="mt-2 mb-4">
|
||||
<Heading h2 separator bold size="sm"> Tool Parameters </Heading>
|
||||
<FormDisplay
|
||||
@@ -55,13 +56,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CurrentUser from "components/providers/CurrentUser";
|
||||
import FormDisplay from "components/Form/FormDisplay";
|
||||
import ToolCard from "components/Tool/ToolCard";
|
||||
import FormSection from "./FormSection";
|
||||
import FormElement from "components/Form/FormElement";
|
||||
import CurrentUser from "@/components/providers/CurrentUser";
|
||||
import FormDisplay from "@/components/Form/FormDisplay.vue";
|
||||
import ToolCard from "@/components/Tool/ToolCard.vue";
|
||||
import FormSection from "./FormSection.vue";
|
||||
import FormElement from "@/components/Form/FormElement.vue";
|
||||
import FormConditional from "./FormConditional.vue";
|
||||
import Utils from "utils/utils";
|
||||
import Heading from "components/Common/Heading";
|
||||
import Heading from "@/components/Common/Heading.vue";
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import { useUniqueLabelError } from "../composables/useUniqueLabelError";
|
||||
import { useStepProps } from "../composables/useStepProps";
|
||||
@@ -73,6 +75,7 @@ export default {
|
||||
FormDisplay,
|
||||
ToolCard,
|
||||
FormElement,
|
||||
FormConditional,
|
||||
FormSection,
|
||||
Heading,
|
||||
},
|
||||
@@ -87,13 +90,24 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
emits: ["onSetData", "onUpdateStep", "onChangePostJobActions", "onAnnotation", "onLabel"],
|
||||
setup(props, { emit }) {
|
||||
const { stepId, annotation, label, stepInputs, stepOutputs, configForm, postJobActions } = useStepProps(
|
||||
toRef(props, "step")
|
||||
);
|
||||
const stepStore = useWorkflowStepStore();
|
||||
const uniqueErrorLabel = useUniqueLabelError(stepStore, label);
|
||||
return { stepId, annotation, label, stepInputs, stepOutputs, configForm, postJobActions, uniqueErrorLabel };
|
||||
|
||||
return {
|
||||
stepId,
|
||||
annotation,
|
||||
label,
|
||||
stepInputs,
|
||||
stepOutputs,
|
||||
configForm,
|
||||
postJobActions,
|
||||
uniqueErrorLabel,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -150,9 +164,16 @@ export default {
|
||||
onLabel(newLabel) {
|
||||
this.$emit("onLabel", this.stepId, newLabel);
|
||||
},
|
||||
/**
|
||||
* Change event is triggered on component creation and input changes.
|
||||
* @param { Object } values contains flat key-value pairs `prefixed-name=value`
|
||||
*/
|
||||
onChange(values) {
|
||||
const initialRequest = Object.keys(this.mainValues).length === 0;
|
||||
this.mainValues = values;
|
||||
this.postChanges();
|
||||
if (!initialRequest) {
|
||||
this.postChanges();
|
||||
}
|
||||
},
|
||||
onChangePostJobActions(postJobActions) {
|
||||
this.$emit("onChangePostJobActions", this.stepId, postJobActions);
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
<div class="unified-panel-header-inner">
|
||||
<WorkflowOptions
|
||||
:has-changes="hasChanges"
|
||||
:has-invalid-connections="hasInvalidConnections"
|
||||
@onSave="onSave"
|
||||
@onSaveAs="onSaveAs"
|
||||
@onRun="onRun"
|
||||
@@ -107,11 +108,13 @@
|
||||
<div>
|
||||
<FormTool
|
||||
v-if="hasActiveNodeTool"
|
||||
:key="activeStep.id"
|
||||
:step="activeStep"
|
||||
:datatypes="datatypes"
|
||||
@onChangePostJobActions="onChangePostJobActions"
|
||||
@onAnnotation="onAnnotation"
|
||||
@onLabel="onLabel"
|
||||
@onUpdateStep="onUpdateStep"
|
||||
@onSetData="onSetData" />
|
||||
<FormDefault
|
||||
v-else-if="hasActiveNodeDefault"
|
||||
@@ -121,6 +124,7 @@
|
||||
@onLabel="onLabel"
|
||||
@onEditSubworkflow="onEditSubworkflow"
|
||||
@onAttemptRefactor="onAttemptRefactor"
|
||||
@onUpdateStep="onUpdateStep"
|
||||
@onSetData="onSetData" />
|
||||
<WorkflowAttributes
|
||||
v-else-if="showAttributes"
|
||||
@@ -185,7 +189,7 @@ import WorkflowGraph from "./WorkflowGraph.vue";
|
||||
import { defaultPosition } from "./composables/useDefaultStepPosition";
|
||||
import { useConnectionStore } from "@/stores/workflowConnectionStore";
|
||||
|
||||
import Vue, { onUnmounted, computed } from "vue";
|
||||
import Vue, { onUnmounted, computed, ref } from "vue";
|
||||
import { ConfirmDialog } from "@/composables/confirmDialog";
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
|
||||
@@ -233,7 +237,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
setup(props, { emit }) {
|
||||
const { datatypes, datatypesMapper, datatypesMapperLoading } = useDatatypesMapper();
|
||||
const connectionsStore = useConnectionStore();
|
||||
const stepStore = useWorkflowStepStore();
|
||||
@@ -247,6 +251,13 @@ export default {
|
||||
return null;
|
||||
});
|
||||
|
||||
const hasChanges = ref(false);
|
||||
const hasInvalidConnections = computed(() => Object.keys(connectionsStore.invalidConnections).length > 0);
|
||||
|
||||
stepStore.$subscribe((mutation, state) => {
|
||||
hasChanges.value = true;
|
||||
});
|
||||
|
||||
function resetStores() {
|
||||
connectionsStore.$reset();
|
||||
stepStore.$reset();
|
||||
@@ -254,9 +265,12 @@ export default {
|
||||
}
|
||||
onUnmounted(() => {
|
||||
resetStores();
|
||||
emit("update:confirmation", false);
|
||||
});
|
||||
return {
|
||||
connectionsStore,
|
||||
hasChanges,
|
||||
hasInvalidConnections,
|
||||
stepStore,
|
||||
steps,
|
||||
nodeIndex: getStepIndex,
|
||||
@@ -276,7 +290,6 @@ export default {
|
||||
markdownText: null,
|
||||
versions: [],
|
||||
parameters: null,
|
||||
hasChanges: false,
|
||||
report: {},
|
||||
labels: {},
|
||||
license: null,
|
||||
@@ -333,9 +346,6 @@ export default {
|
||||
this.hasChanges = true;
|
||||
}
|
||||
},
|
||||
steps(newSteps, oldSteps) {
|
||||
this.hasChanges = true;
|
||||
},
|
||||
hasChanges() {
|
||||
this.$emit("update:confirmation", this.hasChanges);
|
||||
},
|
||||
@@ -348,7 +358,6 @@ export default {
|
||||
methods: {
|
||||
onUpdateStep(step) {
|
||||
this.stepStore.updateStep(step);
|
||||
this.hasChanges = true;
|
||||
},
|
||||
onUpdateStepPosition(stepId, position) {
|
||||
const step = { ...this.steps[stepId], position };
|
||||
@@ -400,6 +409,7 @@ export default {
|
||||
hide_modal(); // hide other modals created in utilities also...
|
||||
},
|
||||
async onRefactor(response) {
|
||||
this.resetStores();
|
||||
await fromSimple(response.workflow);
|
||||
this._loadEditorData(response.workflow);
|
||||
},
|
||||
@@ -637,6 +647,7 @@ export default {
|
||||
name: name,
|
||||
content_id: contentId,
|
||||
type: type,
|
||||
outputs: [],
|
||||
position: defaultPosition(this.graphOffset, this.transform),
|
||||
post_job_actions: {},
|
||||
};
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
@move="onMoveTo"
|
||||
@pan-by="onPanBy">
|
||||
<div class="node-header unselectable clearfix" @click="makeActive" @keyup.enter="makeActive">
|
||||
<loading-span v-if="isLoading" message="Loading details" />
|
||||
<b-button-group class="float-right">
|
||||
<loading-span v-if="isLoading" spinner-only />
|
||||
<b-button
|
||||
v-if="canClone"
|
||||
v-b-tooltip.hover
|
||||
@@ -57,6 +57,9 @@
|
||||
</b-popover>
|
||||
</b-button-group>
|
||||
<i :class="iconClass" />
|
||||
<span v-if="step.when" v-b-tooltip.hover title="This step is conditionally executed.">
|
||||
<font-awesome-icon icon="fa-code-branch" />
|
||||
</span>
|
||||
<span
|
||||
v-b-tooltip.hover
|
||||
title="Index of the step in the workflow run form. Steps are ordered by distance to the upper-left corner of the window; inputs are listed first."
|
||||
@@ -69,8 +72,8 @@
|
||||
</b-alert>
|
||||
<div v-else class="node-body" @click="makeActive" @keyup.enter="makeActive">
|
||||
<node-input
|
||||
v-for="input in inputs"
|
||||
:key="input.name"
|
||||
v-for="(input, index) in inputs"
|
||||
:key="`${index}-${input.name}`"
|
||||
:input="input"
|
||||
:step-id="id"
|
||||
:datatypes-mapper="datatypesMapper"
|
||||
@@ -82,8 +85,8 @@
|
||||
@onChange="onChange" />
|
||||
<div v-if="showRule" class="rule" />
|
||||
<node-output
|
||||
v-for="output in outputs"
|
||||
:key="output.name"
|
||||
v-for="(output, index) in outputs"
|
||||
:key="`${index + inputs.length}-${output.name}`"
|
||||
:output="output"
|
||||
:workflow-outputs="workflowOutputs"
|
||||
:post-job-actions="postJobActions"
|
||||
@@ -118,9 +121,17 @@ import { useWorkflowStateStore, type XYPosition } from "@/stores/workflowEditorS
|
||||
import type { Step } from "@/stores/workflowStepStore";
|
||||
import { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import type { UseElementBoundingReturn, UseScrollReturn } from "@vueuse/core";
|
||||
import { useConnectionStore } from "@/stores/workflowConnectionStore";
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import { faCodeBranch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
// @ts-ignore
|
||||
library.add(faCodeBranch);
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: Number, required: true },
|
||||
contentId: { type: String as PropType<string | null>, default: null },
|
||||
@@ -161,7 +172,9 @@ function remove() {
|
||||
const el: Ref<HTMLElement | null> = ref(null);
|
||||
const postJobActions = computed(() => props.step.post_job_actions || {});
|
||||
const workflowOutputs = computed(() => props.step.workflow_outputs || []);
|
||||
const connectionStore = useConnectionStore();
|
||||
const stateStore = useWorkflowStateStore();
|
||||
const stepStore = useWorkflowStepStore();
|
||||
const isLoading = computed(() => Boolean(stateStore.getStepLoadingState(props.id)?.loading));
|
||||
useNodePosition(el, props.id, stateStore);
|
||||
const title = computed(() => props.step.label || props.step.name);
|
||||
@@ -184,8 +197,43 @@ const style = computed(() => {
|
||||
return { top: props.step.position!.top + "px", left: props.step.position!.left + "px" };
|
||||
});
|
||||
const errors = computed(() => props.step.errors || stateStore.getStepLoadingState(props.id)?.error);
|
||||
const inputs = computed(() => props.step.inputs);
|
||||
const outputs = computed(() => props.step.outputs);
|
||||
const inputs = computed(() => {
|
||||
const connections = connectionStore.getConnectionsForStep(props.id);
|
||||
const extraStepInputs = stepStore.getStepExtraInputs(props.id);
|
||||
const stepInputs = [...extraStepInputs, ...(props.step.inputs || [])];
|
||||
const unknownInputs: string[] = [];
|
||||
connections.forEach((connection) => {
|
||||
if (connection.input.stepId == props.id && !stepInputs.find((input) => input.name === connection.input.name)) {
|
||||
unknownInputs.push(connection.input.name);
|
||||
}
|
||||
});
|
||||
const invalidInputNames = [...new Set(unknownInputs)];
|
||||
const invalidInputTerminalSource = invalidInputNames.map((name) => {
|
||||
return { name, optional: false, extensions: [], valid: false, input_type: "dataset" };
|
||||
});
|
||||
return [...stepInputs, ...invalidInputTerminalSource];
|
||||
});
|
||||
const invalidOutputs = computed(() => {
|
||||
const connections = connectionStore.getConnectionsForStep(props.id);
|
||||
const invalidConnections = connections.filter(
|
||||
(connection) =>
|
||||
connection.output.stepId == props.id &&
|
||||
!props.step.outputs.find((output) => output.name === connection.output.name)
|
||||
);
|
||||
const invalidOutputNames = [...new Set(invalidConnections.map((connection) => connection.output.name))];
|
||||
return invalidOutputNames.map((name) => {
|
||||
return { name, optional: false, datatypes: [], valid: false };
|
||||
});
|
||||
});
|
||||
const outputs = computed(() => {
|
||||
let stepOutputs = props.step.outputs;
|
||||
if (props.step.when) {
|
||||
stepOutputs = stepOutputs.map((output) => {
|
||||
return { ...output, optional: true };
|
||||
});
|
||||
}
|
||||
return [...stepOutputs, ...invalidOutputs.value];
|
||||
});
|
||||
|
||||
function onMoveTo(position: XYPosition) {
|
||||
emit("onUpdateStepPosition", props.id, {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
class="form-row dataRow input-data-row"
|
||||
@mouseleave="leave"
|
||||
@mouseenter="enter"
|
||||
@focusin="enter"
|
||||
@focusout="leave">
|
||||
<div :class="rowClass" @mouseleave="leave" @mouseenter="enter" @focusin="enter" @focusout="leave">
|
||||
<div
|
||||
:id="id"
|
||||
:input-name="input.name"
|
||||
@@ -14,7 +9,13 @@
|
||||
@dragleave.prevent="dragLeave">
|
||||
<div :id="iconId" ref="el" v-b-tooltip.manual class="icon" :title="reason" />
|
||||
</div>
|
||||
<div v-if="showRemove" class="delete-terminal" @click="onRemove" @keyup.delete="onRemove" />
|
||||
<div
|
||||
v-if="showRemove"
|
||||
v-b-tooltip.hover
|
||||
:title="reason"
|
||||
class="delete-terminal"
|
||||
@click="onRemove"
|
||||
@keyup.delete="onRemove" />
|
||||
{{ label }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,7 +30,7 @@ import { storeToRefs } from "pinia";
|
||||
import { useTerminal } from "./composables/useTerminal";
|
||||
import { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
|
||||
import { terminalFactory } from "@/components/Workflow/Editor/modules/terminals";
|
||||
import { terminalFactory, ConnectionAcceptable } from "@/components/Workflow/Editor/modules/terminals";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -75,25 +76,60 @@ export default {
|
||||
watchEffect(() => {
|
||||
hasTerminals.value = connectionStore.getOutputTerminalsForInputTerminal(id.value).length > 0;
|
||||
});
|
||||
|
||||
const rowClass = computed(() => {
|
||||
const classes = ["form-row", "dataRow", "input-data-row"];
|
||||
if (props.input?.valid === false) {
|
||||
classes.push("form-row-error");
|
||||
}
|
||||
return classes;
|
||||
});
|
||||
|
||||
const stateStore = useWorkflowStateStore();
|
||||
const { draggingTerminal } = storeToRefs(stateStore);
|
||||
|
||||
const terminalIsHovered = ref(false);
|
||||
const connections = computed(() => {
|
||||
return connectionStore.getConnectionsForTerminal(id.value);
|
||||
});
|
||||
const invalidConnectionReasons = computed(() =>
|
||||
connections.value
|
||||
.map((connection) => connectionStore.invalidConnections[connection.id])
|
||||
.filter((reason) => reason)
|
||||
);
|
||||
|
||||
const canAccept = computed(() => {
|
||||
if (draggingTerminal.value) {
|
||||
return terminal.value.canAccept(draggingTerminal.value);
|
||||
} else if (invalidConnectionReasons.value.length) {
|
||||
return new ConnectionAcceptable(false, invalidConnectionReasons.value[0]);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const showRemove = computed(() => {
|
||||
if (invalidConnectionReasons.value.length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
return terminalIsHovered.value && connections.value.length > 0;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
el,
|
||||
position,
|
||||
isDragging,
|
||||
draggingTerminal,
|
||||
connectionStore,
|
||||
id,
|
||||
iconId,
|
||||
rowClass,
|
||||
canAccept,
|
||||
hasTerminals,
|
||||
terminal,
|
||||
stateStore,
|
||||
isMultiple,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showRemove: false,
|
||||
showRemove,
|
||||
terminalIsHovered,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -112,12 +148,6 @@ export default {
|
||||
label() {
|
||||
return this.input.label || this.input.name;
|
||||
},
|
||||
canAccept() {
|
||||
if (this.draggingTerminal) {
|
||||
return this.terminal.canAccept(this.draggingTerminal);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
reason() {
|
||||
const reason = this.canAccept?.reason;
|
||||
return reason;
|
||||
@@ -165,13 +195,13 @@ export default {
|
||||
onRemove() {
|
||||
const connections = this.connectionStore.getConnectionsForTerminal(this.id);
|
||||
connections.forEach((connection) => this.terminal.disconnect(connection));
|
||||
this.showRemove = false;
|
||||
this.terminalIsHovered = false;
|
||||
},
|
||||
enter() {
|
||||
this.showRemove = this.hasTerminals;
|
||||
this.terminalIsHovered = true;
|
||||
},
|
||||
leave() {
|
||||
this.showRemove = false;
|
||||
this.terminalIsHovered = false;
|
||||
},
|
||||
onDrop(e) {
|
||||
const stepOut = JSON.parse(e.dataTransfer.getData("text/plain"));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="form-row dataRow output-data-row">
|
||||
<div :class="rowClass">
|
||||
<div
|
||||
v-if="showCalloutActiveOutput"
|
||||
v-b-tooltip
|
||||
@@ -154,6 +154,13 @@ export default {
|
||||
const activeLabel = workflowOutput.value?.label || props.output.name;
|
||||
return `${activeLabel} (${extensions.value.join(", ")})`;
|
||||
});
|
||||
const rowClass = computed(() => {
|
||||
const classes = ["form-row", "dataRow", "output-data-row"];
|
||||
if (props.output?.valid === false) {
|
||||
classes.push("form-row-error");
|
||||
}
|
||||
return classes;
|
||||
});
|
||||
|
||||
const menu = ref(null);
|
||||
const icon = ref(null);
|
||||
@@ -176,7 +183,6 @@ export default {
|
||||
function onToggleActive() {
|
||||
const step = stepStore.getStep(stepId.value);
|
||||
if (workflowOutput.value) {
|
||||
console.log("wfo", workflowOutput.value);
|
||||
step.workflow_outputs = step.workflow_outputs.filter(
|
||||
(workflowOutput) => workflowOutput.output_name !== output.value.name
|
||||
);
|
||||
@@ -212,6 +218,7 @@ export default {
|
||||
activeClass,
|
||||
visibleClass,
|
||||
visibleHint,
|
||||
rowClass,
|
||||
isVisible,
|
||||
terminal,
|
||||
isMultiple,
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
@click="$emit('onAttributes')">
|
||||
<span class="fa fa-pencil-alt" />
|
||||
</b-button>
|
||||
<b-button-group v-b-tooltip class="editor-button-save-group" title="Save Workflow">
|
||||
<b-button-group v-b-tooltip class="editor-button-save-group" :title="saveHover">
|
||||
<b-button
|
||||
id="workflow-save-button"
|
||||
role="button"
|
||||
variant="link"
|
||||
aria-label="Save Workflow"
|
||||
class="editor-button-save"
|
||||
:disabled="!hasChanges"
|
||||
:disabled="!hasChanges || hasInvalidConnections"
|
||||
@click="$emit('onSave')">
|
||||
<span class="fa fa-floppy-o" />
|
||||
</b-button>
|
||||
@@ -77,22 +77,23 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { BDropdown, BDropdownItem, BButton } from "bootstrap-vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BDropdown,
|
||||
BDropdownItem,
|
||||
BButton,
|
||||
},
|
||||
props: {
|
||||
hasChanges: {
|
||||
type: Boolean,
|
||||
},
|
||||
requiredReindex: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = defineProps<{
|
||||
hasChanges?: boolean;
|
||||
hasInvalidConnections?: boolean;
|
||||
requiredReindex?: boolean;
|
||||
}>();
|
||||
|
||||
const saveHover = computed(() => {
|
||||
if (!props.hasChanges) {
|
||||
return "Workflow has no changes";
|
||||
} else if (props.hasInvalidConnections) {
|
||||
return "Workflow has invalid connections, review and remove invalid connections";
|
||||
} else {
|
||||
return "Save Workflow";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
:id="connectionId"
|
||||
:position="position"
|
||||
:output-is-mapped-over="outputIsMappedOver"
|
||||
:input-is-mapped-over="inputIsMappedOver"></raw-connector>
|
||||
:input-is-mapped-over="inputIsMappedOver"
|
||||
:connection-is-valid="connectionIsValid"
|
||||
:nullable="outputIsOptional"></raw-connector>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RawConnector from "@/components/Workflow/Editor/Connector.vue";
|
||||
import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import type { Connection } from "@/stores/workflowConnectionStore";
|
||||
import { useConnectionStore, type Connection } from "@/stores/workflowConnectionStore";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -19,8 +21,27 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const stepStore = useWorkflowStepStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
const outputIsMappedOver = computed(() => stepStore.stepMapOver[props.connection.output.stepId]?.isCollection);
|
||||
const inputIsMappedOver = computed(() => stepStore.stepMapOver[props.connection.input.stepId]?.isCollection);
|
||||
const inputIsMappedOver = computed(
|
||||
() =>
|
||||
stepStore.stepInputMapOver[props.connection.input.stepId] &&
|
||||
stepStore.stepInputMapOver[props.connection.input.stepId][props.connection.input.name]?.isCollection
|
||||
);
|
||||
const outputIsOptional = computed(() => {
|
||||
return Boolean(
|
||||
stepStore.getStep(props.connection.output.stepId)?.when ||
|
||||
stepStore
|
||||
.getStep(props.connection.output.stepId)
|
||||
?.outputs.find((output) => output.name === props.connection.output.name && output.optional)
|
||||
);
|
||||
});
|
||||
|
||||
// move into connection store ?
|
||||
// move all of terminal logic into connection store ?
|
||||
const connectionIsValid = computed(() => {
|
||||
return !connectionStore.invalidConnections[props.connection.id];
|
||||
});
|
||||
|
||||
const stateStore = useWorkflowStateStore();
|
||||
const connectionId = computed(() => {
|
||||
|
||||
@@ -26,6 +26,10 @@ const dragStyle = computed(() => {
|
||||
}
|
||||
return dragStyle;
|
||||
});
|
||||
|
||||
const dragIsOptional = computed(() => {
|
||||
return props.draggingTerminal?.optional;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
@@ -35,7 +39,8 @@ const dragStyle = computed(() => {
|
||||
v-if="draggingTerminal && draggingConnection"
|
||||
:position="draggingConnection"
|
||||
:input-is-mapped-over="false"
|
||||
:output-is-mapped-over="draggingTerminal.mapOver.isCollection"></raw-connector>
|
||||
:output-is-mapped-over="draggingTerminal.mapOver.isCollection"
|
||||
:nullable="dragIsOptional"></raw-connector>
|
||||
<terminal-connector
|
||||
v-for="connection in connections"
|
||||
:key="connection.id"
|
||||
|
||||
@@ -48,10 +48,11 @@ import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
|
||||
import type { TerminalPosition } from "@/stores/workflowEditorStateStore";
|
||||
import { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import { useWorkflowStepStore, type Step } from "@/stores/workflowStepStore";
|
||||
import { useZoom } from "./composables/useZoom";
|
||||
import { useD3Zoom } from "./composables/d3Zoom";
|
||||
import type { XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import type { OutputTerminals } from "./modules/terminals";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
import { minZoom, maxZoom } from "./modules/zoomLevels";
|
||||
|
||||
const emit = defineEmits(["transform", "graph-offset", "onRemove", "scrollTo"]);
|
||||
const props = defineProps({
|
||||
@@ -68,7 +69,7 @@ const canvas: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
const elementBounding = useElementBounding(canvas, { windowResize: false, windowScroll: false });
|
||||
const scroll = useScroll(canvas);
|
||||
const { transform, panBy, setZoom, moveTo } = useZoom(1, 0.2, 5, canvas, scroll);
|
||||
const { transform, panBy, setZoom, moveTo } = useD3Zoom(1, minZoom, maxZoom, canvas, scroll);
|
||||
|
||||
const isDragging = ref(false);
|
||||
provide("isDragging", isDragging);
|
||||
|
||||
@@ -1,46 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import Vue, { computed } from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getZoomInLevel, getZoomOutLevel, isMinZoom, isMaxZoom } from "./modules/zoomLevels";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
zoomLevel: number;
|
||||
}>(),
|
||||
{
|
||||
zoomLevel: 1,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits(["onZoom"]);
|
||||
const props = defineProps({
|
||||
zoomLevel: { type: Number, default: 1 },
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onZoom", zoom: number): void;
|
||||
}>();
|
||||
|
||||
const zoomDefault = 1;
|
||||
const zoomLevels = [0.1, 0.2, 0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.33, 1.5, 2, 2.5, 3, 4, 5];
|
||||
const isMin = computed(() => Math.round(props.zoomLevel * 100) == Math.round(zoomLevels[0]! * 100));
|
||||
const isMax = computed(() => Math.round(props.zoomLevel * 100) == Math.round(zoomLevels.at(-1)! * 100));
|
||||
const zoomPercentage = computed(() => Math.round(props.zoomLevel * 100));
|
||||
const index = computed(() => {
|
||||
let index = zoomLevels.indexOf(props.zoomLevel);
|
||||
if (index < 0) {
|
||||
const closest = zoomLevels.reduce((prev, curr) => {
|
||||
return Math.abs(curr - props.zoomLevel) < Math.abs(prev - props.zoomLevel) ? curr : prev;
|
||||
});
|
||||
index = zoomLevels.indexOf(closest);
|
||||
}
|
||||
return index;
|
||||
});
|
||||
|
||||
function onZoomIn() {
|
||||
const zoomLevel = zoomLevels[index.value + 1];
|
||||
if (zoomLevel) {
|
||||
emit("onZoom", zoomLevel);
|
||||
}
|
||||
emit("onZoom", getZoomInLevel(props.zoomLevel));
|
||||
}
|
||||
|
||||
function onZoomOut() {
|
||||
const zoomLevel = zoomLevels[index.value - 1];
|
||||
if (zoomLevel) {
|
||||
emit("onZoom", zoomLevel);
|
||||
}
|
||||
emit("onZoom", getZoomOutLevel(props.zoomLevel));
|
||||
}
|
||||
|
||||
function onZoomReset() {
|
||||
emit("onZoom", zoomDefault);
|
||||
}
|
||||
@@ -49,8 +32,7 @@ function onZoomReset() {
|
||||
<template>
|
||||
<span class="zoom-control float-right btn-group-horizontal">
|
||||
<b-button
|
||||
v-b-tooltip.hover
|
||||
:disabled="isMin"
|
||||
:disabled="isMinZoom(props.zoomLevel)"
|
||||
role="button"
|
||||
class="fa fa-minus"
|
||||
title="Zoom Out"
|
||||
@@ -69,8 +51,7 @@ function onZoomReset() {
|
||||
{{ zoomPercentage }}%
|
||||
</b-button>
|
||||
<b-button
|
||||
v-b-tooltip.hover
|
||||
:disabled="isMax"
|
||||
:disabled="isMaxZoom(props.zoomLevel)"
|
||||
role="button"
|
||||
class="fa fa-plus"
|
||||
title="Zoom In"
|
||||
|
||||
+2
-16
@@ -1,6 +1,6 @@
|
||||
import { zoom, zoomIdentity, type D3ZoomEvent } from "d3-zoom";
|
||||
import { ref, watch } from "vue";
|
||||
import { pointer, select } from "d3-selection";
|
||||
import { select } from "d3-selection";
|
||||
import type { Ref } from "vue";
|
||||
import type { XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import type { UseScrollReturn } from "@vueuse/core";
|
||||
@@ -12,7 +12,7 @@ const filter = (event: any) => {
|
||||
return !preventZoom;
|
||||
};
|
||||
|
||||
export function useZoom(
|
||||
export function useD3Zoom(
|
||||
k: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
@@ -29,20 +29,6 @@ export function useZoom(
|
||||
.translate(transform.value.x, transform.value.y)
|
||||
.scale(transform.value.k);
|
||||
d3Zoom.transform(d3Selection, updatedTransform);
|
||||
|
||||
d3Selection
|
||||
.on("wheel", (event) => {
|
||||
const currentZoom = d3Selection.property("__zoom").k || 1;
|
||||
|
||||
const pinchDelta =
|
||||
-event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10;
|
||||
const point = pointer(event);
|
||||
point[0] += scroll.x.value;
|
||||
point[1] += scroll.y.value;
|
||||
const zoom = currentZoom * 2 ** pinchDelta;
|
||||
d3Zoom.scaleTo(d3Selection, zoom, point);
|
||||
})
|
||||
.on("wheel.zoom", null);
|
||||
}
|
||||
|
||||
d3Zoom.on("zoom", (event: D3ZoomEvent<HTMLElement, unknown>) => {
|
||||
@@ -13,7 +13,7 @@ export function useTerminal(
|
||||
watchEffect(() => {
|
||||
// rebuild terminal if any of the tracked dependencies change
|
||||
const newTerminal = terminalFactory(stepId.value, terminalSource.value, datatypesMapper.value);
|
||||
newTerminal.destroyInvalidConnections();
|
||||
newTerminal.getInvalidConnectedTerminals();
|
||||
terminal.value = newTerminal;
|
||||
isMappedOver.value = newTerminal.isMappedOver();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from "vue";
|
||||
|
||||
export function useUniqueLabelError(
|
||||
workflowStateStore: ReturnType<typeof useWorkflowStepStore>,
|
||||
label: string | undefined
|
||||
label: string | null | undefined
|
||||
) {
|
||||
const error = ref("");
|
||||
if (label && workflowStateStore.workflowOutputs[label]) {
|
||||
|
||||
@@ -27,15 +27,19 @@ export async function fromSimple(data: Workflow, appendData = false, defaultPosi
|
||||
step.position.left += defaultPosition.left;
|
||||
step.position.top += defaultPosition.top;
|
||||
Object.values(step.input_connections).forEach((link) => {
|
||||
let linkArray: ConnectionOutputLink[];
|
||||
if (!Array.isArray(link)) {
|
||||
linkArray = [link];
|
||||
if (link === undefined) {
|
||||
console.error("input connections invalid", step.input_connections);
|
||||
} else {
|
||||
linkArray = link;
|
||||
let linkArray: ConnectionOutputLink[];
|
||||
if (!Array.isArray(link)) {
|
||||
linkArray = [link];
|
||||
} else {
|
||||
linkArray = link;
|
||||
}
|
||||
linkArray.forEach((link) => {
|
||||
link.id += stepIdOffset;
|
||||
});
|
||||
}
|
||||
linkArray.forEach((link) => {
|
||||
link.id += stepIdOffset;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { setActivePinia, createPinia } from "pinia";
|
||||
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import { useWorkflowStepStore, type TerminalSource } from "@/stores/workflowStepStore";
|
||||
import {
|
||||
terminalFactory,
|
||||
InputCollectionTerminal,
|
||||
@@ -10,11 +10,16 @@ import {
|
||||
OutputTerminal,
|
||||
OutputParameterTerminal,
|
||||
producesAcceptableDatatype,
|
||||
InvalidOutputTerminal,
|
||||
} from "./terminals";
|
||||
import { testDatatypesMapper } from "@/components/Datatypes/test_fixtures";
|
||||
import { useConnectionStore } from "@/stores/workflowConnectionStore";
|
||||
import type { DataOutput, Steps, Step } from "@/stores/workflowStepStore";
|
||||
import { NULL_COLLECTION_TYPE_DESCRIPTION } from "./collectionTypeDescription";
|
||||
import {
|
||||
ANY_COLLECTION_TYPE_DESCRIPTION,
|
||||
CollectionTypeDescription,
|
||||
NULL_COLLECTION_TYPE_DESCRIPTION,
|
||||
} from "./collectionTypeDescription";
|
||||
import { simpleSteps, advancedSteps } from "../test_fixtures";
|
||||
|
||||
function setupAdvanced() {
|
||||
@@ -34,6 +39,17 @@ function setupAdvanced() {
|
||||
return terminals;
|
||||
}
|
||||
|
||||
function rebuildTerminal<T extends ReturnType<typeof terminalFactory>>(terminal: T): T {
|
||||
let terminalSource: TerminalSource;
|
||||
const step = terminal.stepStore.getStep(terminal.stepId);
|
||||
if (terminal.terminalType === "input") {
|
||||
terminalSource = step.inputs.find((input) => input.name == terminal.name)!;
|
||||
} else {
|
||||
terminalSource = step.outputs.find((output) => output.name == terminal.name)!;
|
||||
}
|
||||
return terminalFactory(terminal.stepId, terminalSource, testDatatypesMapper) as T;
|
||||
}
|
||||
|
||||
describe("terminalFactory", () => {
|
||||
let terminals: { [index: string]: { [index: string]: ReturnType<typeof terminalFactory> } } = {};
|
||||
beforeEach(() => {
|
||||
@@ -63,6 +79,8 @@ describe("terminalFactory", () => {
|
||||
expect(terminals["any collection"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["multi data"]?.["advanced|advanced_threshold"]).toBeInstanceOf(InputParameterTerminal);
|
||||
expect(terminals["list collection input"]?.["input1"]).toBeInstanceOf(InputCollectionTerminal);
|
||||
expect(terminals["filter_failed"]?.["input"]).toBeInstanceOf(InputCollectionTerminal);
|
||||
expect(terminals["filter_failed"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
});
|
||||
it("throws error on invalid terminalSource", () => {
|
||||
const invalidFactory = () => terminalFactory(1, {} as any, testDatatypesMapper);
|
||||
@@ -101,6 +119,9 @@ describe("canAccept", () => {
|
||||
dataIn.connect(collectionOut);
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
expect(dataIn.canAccept(collectionOut).canAccept).toBe(false);
|
||||
expect(dataIn.canAccept(collectionOut).reason).toBe(
|
||||
"Input already filled with another connection, delete it before connecting another output."
|
||||
);
|
||||
dataIn.disconnect(collectionOut);
|
||||
expect(dataIn.canAccept(collectionOut).canAccept).toBe(true);
|
||||
expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
@@ -142,7 +163,7 @@ describe("canAccept", () => {
|
||||
const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
expect(dataIn.canAccept(dataOut).canAccept).toBe(false);
|
||||
expect(dataIn.canAccept(dataOut).reason).toBe("Cannot connection output to input of same step.");
|
||||
expect(dataIn.canAccept(dataOut).reason).toBe("Cannot connect output to input of same step.");
|
||||
});
|
||||
it("rejects paired input on multi-data input", () => {
|
||||
const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal;
|
||||
@@ -204,6 +225,8 @@ describe("canAccept", () => {
|
||||
const integerInputParam = terminals["multi data"]!["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
const integerOutputParam = terminals["integer parameter input"]!["output"] as OutputParameterTerminal;
|
||||
expect(integerInputParam.canAccept(integerOutputParam).canAccept).toBe(true);
|
||||
// regression test for https://github.com/galaxyproject/galaxy/issues/15417
|
||||
expect(integerInputParam.canAccept(integerOutputParam).reason).toBe(null);
|
||||
});
|
||||
it("rejects text to integer parameter connection", () => {
|
||||
const integerInputParam = terminals["multi data"]!["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
@@ -251,7 +274,7 @@ describe("canAccept", () => {
|
||||
"Can't map over this input with output collection type - this step has outputs defined constraining the mapping of this tool. Disconnect outputs and retry."
|
||||
);
|
||||
});
|
||||
it("rejects attaching non-collection outputs to mapper over inputs", () => {
|
||||
it("rejects attaching non-collection outputs to mapped-over inputs", () => {
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const simpleDataOut = terminals["data input"]!["output"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
@@ -260,22 +283,31 @@ describe("canAccept", () => {
|
||||
dataIn.connect(collectionOut);
|
||||
dataInTwo.connect(dataOut);
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
expect(dataIn.localMapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
//
|
||||
dataIn.disconnect(collectionOut);
|
||||
// this is weird and not particularly robust, if you save and reload this will most likely not be constrained
|
||||
// TODO: avoid this if possible ...
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
expect(dataIn.localMapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
expect(dataIn.localMapOver.isCollection).toBe(true);
|
||||
expect(dataIn.canAccept(simpleDataOut).canAccept).toBe(false);
|
||||
expect(dataIn.canAccept(simpleDataOut).reason).toBe(
|
||||
"Cannot attach non-collection outputs to mapped over inputs, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
);
|
||||
dataInTwo.disconnect(dataOut);
|
||||
// terminal isn't a reactive class in any way (worth a thought doing, but difficult!), the following happens when a new terminal is built
|
||||
// in useTerminal.ts
|
||||
const rebuiltDataIn = rebuildTerminal(dataIn);
|
||||
expect(rebuiltDataIn.localMapOver.isCollection).toBe(false);
|
||||
expect(rebuiltDataIn.canAccept(simpleDataOut).canAccept).toBe(true);
|
||||
});
|
||||
// TODO: test mapOver reset when constraint removed
|
||||
it("resets mapOver when constraint is lifted", () => {
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal;
|
||||
const dataInTwo = terminals["simple data 2"]!["input"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]["out_file1"] as OutputTerminal;
|
||||
const dataInTwo = terminals["simple data 2"]["input"] as InputTerminal;
|
||||
dataIn.connect(collectionOut);
|
||||
dataInTwo.connect(dataOut);
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
@@ -342,7 +374,9 @@ describe("canAccept", () => {
|
||||
const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const collectionIn = terminals["list collection input"]!["input1"] as InputCollectionTerminal;
|
||||
expect(collectionIn.canAccept(collectionOut).canAccept).toBe(true);
|
||||
expect(collectionIn.isMappedOver()).toBe(false);
|
||||
collectionIn.connect(collectionOut);
|
||||
expect(collectionIn.isMappedOver()).toBe(true);
|
||||
expect(collectionIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
const intermediateOut = terminals["list collection input"]!["out_file1"] as OutputCollectionTerminal;
|
||||
const otherListIn = terminals["list collection input 2"]!["input1"] as InputCollectionTerminal;
|
||||
@@ -412,6 +446,20 @@ describe("canAccept", () => {
|
||||
"Effective output data type(s) [tabular] do not appear to match input type(s) [ab1]."
|
||||
);
|
||||
});
|
||||
it("resolves collection type source", () => {
|
||||
const filterFailedInput = terminals["filter_failed"]["input"] as InputCollectionTerminal;
|
||||
const filterFailedOutput = terminals["filter_failed"]["output"] as OutputCollectionTerminal;
|
||||
expect(filterFailedOutput.collectionType).toBe(ANY_COLLECTION_TYPE_DESCRIPTION);
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
filterFailedInput.connect(collectionOut);
|
||||
expect(rebuildTerminal(filterFailedOutput).collectionType).toStrictEqual(new CollectionTypeDescription("list"));
|
||||
filterFailedInput.disconnect(collectionOut);
|
||||
const listPairedOutput = terminals["list:paired input"]["output"] as OutputCollectionTerminal;
|
||||
filterFailedInput.connect(listPairedOutput);
|
||||
expect(rebuildTerminal(filterFailedOutput).collectionType).toStrictEqual(
|
||||
new CollectionTypeDescription("list:paired")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Input terminal", () => {
|
||||
@@ -473,6 +521,14 @@ describe("Input terminal", () => {
|
||||
connectionStore.addConnection(connection);
|
||||
expect(firstInputTerminal.canAccept(dataInputOutputTerminal).canAccept).toBe(false);
|
||||
});
|
||||
it("will maintain invalid connections", () => {
|
||||
const connection = connectionStore.connections[0];
|
||||
connection.output.name = "I don't exist";
|
||||
const firstInputTerminal = terminals[1]["input"] as InputTerminal;
|
||||
const invalidTerminals = firstInputTerminal.getConnectedTerminals();
|
||||
expect(invalidTerminals.length).toBe(1);
|
||||
expect(invalidTerminals[0]).toBeInstanceOf(InvalidOutputTerminal);
|
||||
});
|
||||
});
|
||||
|
||||
describe("producesAcceptableDatatype", () => {
|
||||
|
||||
@@ -18,9 +18,10 @@ import type {
|
||||
TerminalSource,
|
||||
} from "@/stores/workflowStepStore";
|
||||
import type { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import Vue from "vue";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
|
||||
class ConnectionAcceptable {
|
||||
export class ConnectionAcceptable {
|
||||
reason: string | null;
|
||||
canAccept: boolean;
|
||||
constructor(canAccept: boolean, reason: string | null) {
|
||||
@@ -43,6 +44,7 @@ interface InputTerminalInputs {
|
||||
|
||||
interface InputTerminalArgs extends BaseTerminalArgs {
|
||||
input: InputTerminalInputs;
|
||||
input_type: "dataset" | "dataset_collection" | "parameter";
|
||||
}
|
||||
|
||||
class Terminal extends EventEmitter {
|
||||
@@ -53,6 +55,7 @@ class Terminal extends EventEmitter {
|
||||
stepId: number;
|
||||
terminalType: "input" | "output";
|
||||
datatypesMapper: DatatypesMapperModel;
|
||||
localMapOver: CollectionTypeDescriptor;
|
||||
|
||||
constructor(attr: BaseTerminalArgs) {
|
||||
super();
|
||||
@@ -63,6 +66,7 @@ class Terminal extends EventEmitter {
|
||||
this.multiple = false;
|
||||
this.terminalType = "input";
|
||||
this.datatypesMapper = attr.datatypesMapper;
|
||||
this.localMapOver = NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
public get id() {
|
||||
return `node-${this.stepId}-${this.terminalType}-${this.name}`;
|
||||
@@ -94,6 +98,7 @@ class Terminal extends EventEmitter {
|
||||
this.resetMappingIfNeeded(connection);
|
||||
}
|
||||
setMapOver(val: CollectionTypeDescriptor) {
|
||||
// we use this method to determine the map over status based on all terminals connected to this step
|
||||
let outputVal = val;
|
||||
if (this.multiple) {
|
||||
// emulate list input
|
||||
@@ -105,10 +110,16 @@ class Terminal extends EventEmitter {
|
||||
outputVal = val.effectiveMapOver(description);
|
||||
}
|
||||
const effectiveMapOver = this._effectiveMapOver(outputVal);
|
||||
if (!this.mapOver.equal(effectiveMapOver)) {
|
||||
if (!this.localMapOver.equal(effectiveMapOver)) {
|
||||
this.stepStore.changeStepInputMapOver(this.stepId, this.name, effectiveMapOver);
|
||||
this.localMapOver = effectiveMapOver;
|
||||
}
|
||||
if (
|
||||
!this.mapOver.equal(effectiveMapOver) &&
|
||||
(effectiveMapOver.isCollection ||
|
||||
!Object.values(this.stepStore.stepInputMapOver[this.stepId]).find((mapOver) => mapOver.isCollection))
|
||||
) {
|
||||
this.stepStore.changeStepMapOver(this.stepId, effectiveMapOver);
|
||||
// TODO: do we even need to listen for this?
|
||||
this.emit("changeMapOver", outputVal);
|
||||
}
|
||||
}
|
||||
_effectiveMapOver(otherCollectionType: CollectionTypeDescriptor) {
|
||||
@@ -119,8 +130,7 @@ class Terminal extends EventEmitter {
|
||||
}
|
||||
resetMapping(connection?: Connection) {
|
||||
this.stepStore.changeStepMapOver(this.stepId, NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
// necessary ?
|
||||
this.emit("changeMapOver", this.mapOver);
|
||||
this.stepStore.resetStepInputMapOver(this.stepId);
|
||||
}
|
||||
hasConnectedMappedInputTerminals() {
|
||||
// check if step has connected and mapped input terminals ... should maybe be on step/node ?
|
||||
@@ -150,7 +160,7 @@ class Terminal extends EventEmitter {
|
||||
// No output terminals are counting on this being mapped
|
||||
// over if connected inputs are still mapped over or if none
|
||||
// of the outputs are connected...
|
||||
const reset = this.hasConnectedMappedInputTerminals() || !this.hasConnectedOutputTerminals();
|
||||
const reset = !this.hasConnectedOutputTerminals();
|
||||
if (reset) {
|
||||
this.resetMapping(connection);
|
||||
}
|
||||
@@ -160,12 +170,18 @@ class Terminal extends EventEmitter {
|
||||
class BaseInputTerminal extends Terminal {
|
||||
datatypes: InputTerminalInputs["datatypes"];
|
||||
optional: InputTerminalInputs["optional"];
|
||||
localMapOver: CollectionTypeDescriptor;
|
||||
|
||||
constructor(attr: InputTerminalArgs) {
|
||||
super(attr);
|
||||
this.datatypes = attr.input.datatypes;
|
||||
this.multiple = attr.input.multiple;
|
||||
this.optional = attr.input.optional;
|
||||
if (this.stepStore.stepInputMapOver[this.stepId] && this.stepStore.stepInputMapOver[this.stepId][this.name]) {
|
||||
this.localMapOver = this.stepStore.stepInputMapOver[this.stepId][this.name];
|
||||
} else {
|
||||
this.localMapOver = NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
}
|
||||
connect(other: BaseOutputTerminal): void {
|
||||
super.connect(other);
|
||||
@@ -174,12 +190,18 @@ class BaseInputTerminal extends Terminal {
|
||||
setDefaultMapOver(other: BaseOutputTerminal) {
|
||||
const otherCollectionType = this._otherCollectionType(other);
|
||||
if (otherCollectionType.isCollection) {
|
||||
this.setMapOver(otherCollectionType);
|
||||
return this.setMapOver(otherCollectionType);
|
||||
}
|
||||
}
|
||||
getStepMapOver() {
|
||||
this.getConnectedTerminals().forEach((other) => {
|
||||
this.setDefaultMapOver(other);
|
||||
});
|
||||
}
|
||||
|
||||
canAccept(outputTerminal: BaseOutputTerminal) {
|
||||
if (this.stepId == outputTerminal.stepId) {
|
||||
return new ConnectionAcceptable(false, "Cannot connection output to input of same step.");
|
||||
return new ConnectionAcceptable(false, "Cannot connect output to input of same step.");
|
||||
}
|
||||
if (this._inputFilled()) {
|
||||
return new ConnectionAcceptable(
|
||||
@@ -201,6 +223,7 @@ class BaseInputTerminal extends Terminal {
|
||||
}
|
||||
resetMapping(connection?: Connection) {
|
||||
super.resetMapping(connection);
|
||||
this.stepStore.changeStepInputMapOver(this.stepId, this.name, NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
const outputStepIds = this._getOutputTerminals().map((outputTerminal) => outputTerminal.stepId);
|
||||
if (connection) {
|
||||
outputStepIds.push(connection.output.stepId);
|
||||
@@ -229,6 +252,11 @@ class BaseInputTerminal extends Terminal {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
isMappedOver(): boolean {
|
||||
return Boolean(this.localMapOver.isCollection);
|
||||
}
|
||||
|
||||
connected() {
|
||||
return Boolean(this._getFirstOutputTerminal());
|
||||
}
|
||||
@@ -305,22 +333,31 @@ class BaseInputTerminal extends Terminal {
|
||||
getConnectedTerminals() {
|
||||
return this.connections.map((connection) => {
|
||||
const outputStep = this.stepStore.getStep(connection.output.stepId);
|
||||
assertDefined(outputStep, `No such step with id ${connection.output.stepId}`);
|
||||
|
||||
if (!outputStep) {
|
||||
return new InvalidOutputTerminal({
|
||||
stepId: -1,
|
||||
optional: false,
|
||||
datatypes: [],
|
||||
name: connection.output.name,
|
||||
valid: false,
|
||||
datatypesMapper: this.datatypesMapper,
|
||||
});
|
||||
}
|
||||
let terminalSource = outputStep.outputs.find((output) => output.name === connection.output.name);
|
||||
|
||||
/*
|
||||
This can't happen, I think, because we'd drop the connection.
|
||||
We (probably) want to eventually display invalid connections,
|
||||
so maybe generate a NullTerminal when there is no terminalSource ?
|
||||
*/
|
||||
assertDefined(
|
||||
terminalSource,
|
||||
`Could not find output ${connection.output.name} on step ${connection.output.stepId}`
|
||||
);
|
||||
|
||||
if (!terminalSource) {
|
||||
return new InvalidOutputTerminal({
|
||||
stepId: -1,
|
||||
optional: false,
|
||||
datatypes: [],
|
||||
name: connection.output.name,
|
||||
valid: false,
|
||||
datatypesMapper: this.datatypesMapper,
|
||||
});
|
||||
}
|
||||
const postJobActionKey = `ChangeDatatypeAction${connection.output.name}`;
|
||||
|
||||
if (outputStep.when) {
|
||||
terminalSource = { ...terminalSource, optional: true };
|
||||
}
|
||||
if (
|
||||
"extensions" in terminalSource &&
|
||||
outputStep.post_job_actions &&
|
||||
@@ -337,13 +374,43 @@ class BaseInputTerminal extends Terminal {
|
||||
return terminalFactory(outputStep.id, terminalSource, this.datatypesMapper);
|
||||
});
|
||||
}
|
||||
destroyInvalidConnections() {
|
||||
this.getConnectedTerminals().forEach((terminal) => {
|
||||
if (!this.attachable(terminal).canAccept) {
|
||||
this.disconnect(terminal);
|
||||
|
||||
getInvalidConnectedTerminals() {
|
||||
return this.getConnectedTerminals().filter((terminal) => {
|
||||
const canAccept = this.attachable(terminal);
|
||||
const connectionId = `${this.stepId}-${this.name}-${terminal.stepId}-${terminal.name}`;
|
||||
if (!canAccept.canAccept) {
|
||||
this.connectionStore.markInvalidConnection(connectionId, canAccept.reason ?? "Unknown");
|
||||
return true;
|
||||
} else if (this.connectionStore.invalidConnections[connectionId]) {
|
||||
this.connectionStore.dropFromInvalidConnections(connectionId);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
destroyInvalidConnections() {
|
||||
this.getInvalidConnectedTerminals().forEach((terminal) => this.disconnect(terminal));
|
||||
}
|
||||
}
|
||||
|
||||
interface InvalidInputTerminalArgs extends InputTerminalArgs {
|
||||
valid: false;
|
||||
}
|
||||
|
||||
export class InvalidInputTerminal extends BaseInputTerminal {
|
||||
valid: false;
|
||||
localMapOver: CollectionTypeDescriptor;
|
||||
|
||||
constructor(attr: InvalidInputTerminalArgs) {
|
||||
super(attr);
|
||||
this.valid = false;
|
||||
this.localMapOver = NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
|
||||
attachable(terminal: BaseOutputTerminal) {
|
||||
return new ConnectionAcceptable(false, "Cannot attach to invalid input. Disconnect this input.");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: turn into InputTerminalAttachableMixin ?
|
||||
@@ -353,7 +420,7 @@ export class InputTerminal extends BaseInputTerminal {
|
||||
constructor(attr: InputTerminalArgs) {
|
||||
super(attr);
|
||||
this.collection = false;
|
||||
this.getConnectedTerminals().map((other) => this.setDefaultMapOver(other));
|
||||
this.getStepMapOver();
|
||||
}
|
||||
|
||||
attachable(other: BaseOutputTerminal) {
|
||||
@@ -410,11 +477,13 @@ export class InputTerminal extends BaseInputTerminal {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (mapOver.isCollection) {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Cannot attach non-collection outputs to mapped over inputs, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
);
|
||||
} else {
|
||||
if (this.localMapOver.isCollection) {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Cannot attach non-collection output to mapped over input, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
);
|
||||
}
|
||||
}
|
||||
return this._producesAcceptableDatatypeAndOptionalness(other);
|
||||
}
|
||||
@@ -430,7 +499,7 @@ export class InputParameterTerminal extends BaseInputTerminal {
|
||||
constructor(attr: InputParameterTerminalArgs) {
|
||||
super(attr);
|
||||
this.type = attr.type;
|
||||
this.getConnectedTerminals().map((other) => this.setDefaultMapOver(other));
|
||||
this.getStepMapOver();
|
||||
}
|
||||
|
||||
effectiveType(parameterType: string) {
|
||||
@@ -440,9 +509,10 @@ export class InputParameterTerminal extends BaseInputTerminal {
|
||||
const effectiveThisType = this.effectiveType(this.type);
|
||||
const otherType = ("type" in other && other.type) || "data";
|
||||
const effectiveOtherType = this.effectiveType(otherType);
|
||||
const canAccept = effectiveThisType === effectiveOtherType;
|
||||
return new ConnectionAcceptable(
|
||||
effectiveThisType == effectiveOtherType,
|
||||
`Cannot attach a ${effectiveOtherType} parameter to a ${effectiveThisType} input`
|
||||
canAccept,
|
||||
canAccept ? null : `Cannot attach a ${effectiveOtherType} parameter to a ${effectiveThisType} input`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -465,7 +535,7 @@ export class InputCollectionTerminal extends BaseInputTerminal {
|
||||
if (!this.collectionTypes.length) {
|
||||
this.collectionTypes.push(ANY_COLLECTION_TYPE_DESCRIPTION);
|
||||
}
|
||||
this.getConnectedTerminals().map((other) => this.setDefaultMapOver(other));
|
||||
this.getStepMapOver();
|
||||
}
|
||||
_effectiveMapOver(otherCollectionType: CollectionTypeDescriptor) {
|
||||
const collectionTypes = this.collectionTypes;
|
||||
@@ -540,6 +610,8 @@ interface BaseOutputTerminalArgs extends BaseTerminalArgs {
|
||||
optional: boolean;
|
||||
}
|
||||
|
||||
export type InputTerminalsAndInvalid = InputTerminals | InvalidInputTerminal;
|
||||
|
||||
class BaseOutputTerminal extends Terminal {
|
||||
datatypes: BaseOutputTerminalArgs["datatypes"];
|
||||
optional: BaseOutputTerminalArgs["optional"];
|
||||
@@ -553,19 +625,45 @@ class BaseOutputTerminal extends Terminal {
|
||||
this.optional = attr.optional;
|
||||
this.terminalType = "output";
|
||||
}
|
||||
getConnectedTerminals() {
|
||||
getConnectedTerminals(): InputTerminalsAndInvalid[] {
|
||||
return this.connections.map((connection) => {
|
||||
const inputStep = this.stepStore.getStep(connection.input.stepId);
|
||||
assertDefined(inputStep, `No such step with id ${connection.output.stepId}`);
|
||||
const terminalSource = inputStep.inputs.find((input) => input.name === connection.input.name);
|
||||
assertDefined(
|
||||
terminalSource,
|
||||
`Could not find input ${connection.input.name} on step ${connection.input.stepId}`
|
||||
const extraStepInput = this.stepStore.getStepExtraInputs(inputStep.id);
|
||||
const terminalSource = [...extraStepInput, ...inputStep.inputs].find(
|
||||
(input) => input.name === connection.input.name
|
||||
);
|
||||
|
||||
if (!terminalSource) {
|
||||
return new InvalidInputTerminal({
|
||||
valid: false,
|
||||
name: connection.input.name,
|
||||
stepId: connection.input.stepId,
|
||||
datatypesMapper: this.datatypesMapper,
|
||||
input_type: "dataset",
|
||||
input: {
|
||||
datatypes: [],
|
||||
optional: false,
|
||||
multiple: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
return terminalFactory(inputStep.id, terminalSource, this.datatypesMapper);
|
||||
});
|
||||
}
|
||||
|
||||
getInvalidConnectedTerminals() {
|
||||
return this.getConnectedTerminals().filter((terminal: any) => {
|
||||
const canAccept = terminal.attachable(this);
|
||||
const connectionId = `${terminal.stepId}-${terminal.name}-${this.stepId}-${this.name}`;
|
||||
if (!canAccept.canAccept) {
|
||||
this.connectionStore.markInvalidConnection(connectionId, canAccept.reason ?? "Unknown");
|
||||
return true;
|
||||
} else if (this.connectionStore.invalidConnections[connectionId]) {
|
||||
this.connectionStore.dropFromInvalidConnections(connectionId);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
destroyInvalidConnections() {
|
||||
this.getConnectedTerminals().forEach((terminal) => {
|
||||
if (!terminal.attachable(this).canAccept) {
|
||||
@@ -595,19 +693,56 @@ interface OutputCollectionTerminalArgs extends BaseOutputTerminalArgs {
|
||||
}
|
||||
|
||||
export class OutputCollectionTerminal extends BaseOutputTerminal {
|
||||
collectionTypeSource: string | null;
|
||||
|
||||
constructor(attr: OutputCollectionTerminalArgs) {
|
||||
super(attr);
|
||||
this.collectionTypeSource = null;
|
||||
if (attr.collection_type) {
|
||||
this.collectionType = new CollectionTypeDescription(attr.collection_type);
|
||||
} else {
|
||||
const collectionTypeSource = attr.collection_type_source;
|
||||
if (!collectionTypeSource) {
|
||||
this.collectionTypeSource = attr.collection_type_source;
|
||||
if (!this.collectionTypeSource) {
|
||||
console.log("Warning: No collection type or collection type source defined.");
|
||||
}
|
||||
this.collectionType = ANY_COLLECTION_TYPE_DESCRIPTION;
|
||||
this.collectionType = this.getCollectionTypeFromInput() || ANY_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
this.isCollection = true;
|
||||
}
|
||||
|
||||
getCollectionTypeFromInput() {
|
||||
const connection = this.connectionStore.connections.find(
|
||||
(connection) =>
|
||||
connection.input.name === this.collectionTypeSource && connection.input.stepId === this.stepId
|
||||
);
|
||||
if (connection) {
|
||||
const outputStep = this.stepStore.getStep(connection.output.stepId);
|
||||
const inputStep = this.stepStore.getStep(this.stepId);
|
||||
if (outputStep) {
|
||||
const stepOutput = outputStep.outputs.find((output) => output.name == connection.output.name);
|
||||
const stepInput = inputStep.inputs.find((input) => input.name === this.collectionTypeSource);
|
||||
if (stepInput && stepOutput) {
|
||||
const outputTerminal = terminalFactory(connection.output.stepId, stepOutput, this.datatypesMapper);
|
||||
const inputTerminal = terminalFactory(connection.output.stepId, stepInput, this.datatypesMapper);
|
||||
// otherCollectionType is the mapped over output collection as it would appear at the input terminal
|
||||
const otherCollectionType = inputTerminal._otherCollectionType(outputTerminal);
|
||||
// we need to find which of the possible input collection types is connected
|
||||
if ("collectionTypes" in inputTerminal) {
|
||||
// collection_type_source must point at input collection terminal
|
||||
const connectedCollectionType = inputTerminal.collectionTypes.find(
|
||||
(collectionType) =>
|
||||
otherCollectionType.canMatch(collectionType) ||
|
||||
otherCollectionType.canMapOver(collectionType)
|
||||
);
|
||||
if (connectedCollectionType) {
|
||||
return connectedCollectionType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ANY_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
}
|
||||
|
||||
interface OutputParameterTerminalArgs extends Omit<BaseOutputTerminalArgs, "datatypes"> {
|
||||
@@ -621,6 +756,27 @@ export class OutputParameterTerminal extends BaseOutputTerminal {
|
||||
}
|
||||
}
|
||||
|
||||
interface InvalidOutputTerminalArgs extends BaseOutputTerminalArgs {
|
||||
valid: false;
|
||||
}
|
||||
|
||||
export class InvalidOutputTerminal extends BaseOutputTerminal {
|
||||
valid: false;
|
||||
|
||||
constructor(attr: InvalidOutputTerminalArgs) {
|
||||
super(attr);
|
||||
this.valid = false;
|
||||
}
|
||||
|
||||
validInputTerminals() {
|
||||
return [];
|
||||
}
|
||||
|
||||
canAccept() {
|
||||
return new ConnectionAcceptable(false, "Can't connect to invalid terminal.");
|
||||
}
|
||||
}
|
||||
|
||||
export type OutputTerminals = OutputTerminal | OutputCollectionTerminal | OutputParameterTerminal;
|
||||
export type InputTerminals = InputTerminal | InputCollectionTerminal | InputParameterTerminal;
|
||||
|
||||
@@ -666,7 +822,27 @@ export function producesAcceptableDatatype(
|
||||
);
|
||||
}
|
||||
|
||||
type TerminalOf<T extends TerminalSource> = T extends DataStepInput
|
||||
type TerminalSourceAndInvalid = TerminalSource | InvalidOutputTerminalArgs | InvalidInputTerminalArgs;
|
||||
|
||||
function isInvalidOutputArg(arg: TerminalSourceAndInvalid): arg is InvalidOutputTerminalArgs {
|
||||
return "name" in arg && "valid" in arg && arg.valid === false;
|
||||
}
|
||||
|
||||
function isOutputParameterArg(arg: TerminalSourceAndInvalid): arg is ParameterOutput {
|
||||
return "name" in arg && "parameter" in arg && arg.parameter === true;
|
||||
}
|
||||
|
||||
function isOutputCollectionArg(arg: TerminalSourceAndInvalid): arg is CollectionOutput {
|
||||
return "name" in arg && "collection" in arg && arg.collection;
|
||||
}
|
||||
|
||||
function isOutputArg(arg: TerminalSourceAndInvalid): arg is DataOutput {
|
||||
return "name" in arg && "extensions" in arg;
|
||||
}
|
||||
|
||||
type TerminalOf<T extends TerminalSourceAndInvalid> = T extends InvalidInputTerminalArgs
|
||||
? InvalidInputTerminal
|
||||
: T extends DataStepInput
|
||||
? InputTerminal
|
||||
: T extends DataCollectionStepInput
|
||||
? InputCollectionTerminal
|
||||
@@ -678,9 +854,11 @@ type TerminalOf<T extends TerminalSource> = T extends DataStepInput
|
||||
? OutputCollectionTerminal
|
||||
: T extends ParameterOutput
|
||||
? OutputParameterTerminal
|
||||
: T extends BaseOutputTerminalArgs
|
||||
? InvalidOutputTerminal
|
||||
: never;
|
||||
|
||||
export function terminalFactory<T extends TerminalSource>(
|
||||
export function terminalFactory<T extends TerminalSourceAndInvalid>(
|
||||
stepId: number,
|
||||
terminalSource: T,
|
||||
datatypesMapper: DatatypesMapperModel
|
||||
@@ -688,36 +866,49 @@ export function terminalFactory<T extends TerminalSource>(
|
||||
if ("input_type" in terminalSource) {
|
||||
const terminalArgs = {
|
||||
datatypesMapper: datatypesMapper,
|
||||
input_type: terminalSource.input_type,
|
||||
name: terminalSource.name,
|
||||
stepId: stepId,
|
||||
};
|
||||
const inputArgs = {
|
||||
datatypes: terminalSource.extensions,
|
||||
multiple: terminalSource.multiple,
|
||||
optional: terminalSource.optional,
|
||||
};
|
||||
if (terminalSource.input_type == "dataset") {
|
||||
// type cast appears to be necessary: https://github.com/Microsoft/TypeScript/issues/13995
|
||||
return new InputTerminal({
|
||||
if ("valid" in terminalSource) {
|
||||
return new InvalidInputTerminal({
|
||||
...terminalArgs,
|
||||
input: inputArgs,
|
||||
}) as TerminalOf<T>;
|
||||
} else if (terminalSource.input_type === "dataset_collection") {
|
||||
return new InputCollectionTerminal({
|
||||
...terminalArgs,
|
||||
collection_types: terminalSource.collection_types,
|
||||
input: {
|
||||
...inputArgs,
|
||||
},
|
||||
}) as TerminalOf<T>;
|
||||
} else if (terminalSource.input_type === "parameter") {
|
||||
return new InputParameterTerminal({
|
||||
...terminalArgs,
|
||||
type: terminalSource.type,
|
||||
input: {
|
||||
...inputArgs,
|
||||
datatypes: [],
|
||||
multiple: false,
|
||||
optional: false,
|
||||
},
|
||||
valid: terminalSource.valid,
|
||||
}) as TerminalOf<T>;
|
||||
} else {
|
||||
const inputArgs = {
|
||||
datatypes: terminalSource.extensions,
|
||||
multiple: terminalSource.multiple,
|
||||
optional: terminalSource.optional,
|
||||
};
|
||||
if (terminalSource.input_type == "dataset") {
|
||||
// type cast appears to be necessary: https://github.com/Microsoft/TypeScript/issues/13995
|
||||
return new InputTerminal({
|
||||
...terminalArgs,
|
||||
input: inputArgs,
|
||||
}) as TerminalOf<T>;
|
||||
} else if (terminalSource.input_type === "dataset_collection") {
|
||||
return new InputCollectionTerminal({
|
||||
...terminalArgs,
|
||||
collection_types: terminalSource.collection_types,
|
||||
input: {
|
||||
...inputArgs,
|
||||
},
|
||||
}) as TerminalOf<T>;
|
||||
} else if (terminalSource.input_type === "parameter") {
|
||||
return new InputParameterTerminal({
|
||||
...terminalArgs,
|
||||
type: terminalSource.type,
|
||||
input: {
|
||||
...inputArgs,
|
||||
},
|
||||
}) as TerminalOf<T>;
|
||||
}
|
||||
}
|
||||
} else if (terminalSource.name) {
|
||||
const outputArgs = {
|
||||
@@ -726,24 +917,27 @@ export function terminalFactory<T extends TerminalSource>(
|
||||
stepId: stepId,
|
||||
datatypesMapper: datatypesMapper,
|
||||
};
|
||||
if ("parameter" in terminalSource) {
|
||||
if (isOutputParameterArg(terminalSource)) {
|
||||
return new OutputParameterTerminal({
|
||||
...outputArgs,
|
||||
type: terminalSource.type,
|
||||
}) as TerminalOf<T>;
|
||||
} else if ("collection" in terminalSource && terminalSource.collection) {
|
||||
} else if (isOutputCollectionArg(terminalSource)) {
|
||||
return new OutputCollectionTerminal({
|
||||
...outputArgs,
|
||||
datatypes: terminalSource.extensions,
|
||||
collection_type: terminalSource.collection_type,
|
||||
collection_type_source: terminalSource.collection_type_source,
|
||||
}) as TerminalOf<T>;
|
||||
} else {
|
||||
} else if (isOutputArg(terminalSource)) {
|
||||
return new OutputTerminal({
|
||||
...outputArgs,
|
||||
datatypes: terminalSource.extensions,
|
||||
}) as TerminalOf<T>;
|
||||
}
|
||||
}
|
||||
if (isInvalidOutputArg(terminalSource)) {
|
||||
return new InvalidOutputTerminal(terminalSource) as TerminalOf<T>;
|
||||
}
|
||||
throw `Could not build terminal for ${terminalSource}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
export const zoomLevels = [
|
||||
0.1, 0.2, 0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.33, 1.5, 2, 2.5, 3, 4, 5,
|
||||
] as const;
|
||||
|
||||
export type ZoomLevel = (typeof zoomLevels)[number];
|
||||
|
||||
export const minZoom = zoomLevels[0];
|
||||
export const maxZoom = zoomLevels[zoomLevels.length - 1];
|
||||
|
||||
/**
|
||||
* Finds the closest snapped zoom level
|
||||
* @param zoom decimal number indicating current zoom multiplier
|
||||
* @returns snapped zoom level
|
||||
*/
|
||||
export function getSnappedZoom(zoom: number): ZoomLevel {
|
||||
return zoomLevels.reduce((a, b) => {
|
||||
return Math.abs(b - zoom) < Math.abs(a - zoom) ? b : a;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next largest zoom level
|
||||
* @param zoom decimal number indicating current zoom multiplier
|
||||
* @returns snapped zoom level
|
||||
*/
|
||||
export function getZoomInLevel(zoom: number): ZoomLevel {
|
||||
const snapped = getSnappedZoom(zoom);
|
||||
const index = zoomLevels.indexOf(snapped);
|
||||
|
||||
if (index === zoomLevels.length - 1) {
|
||||
return snapped;
|
||||
} else {
|
||||
return zoomLevels[index + 1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next smaller zoom level
|
||||
* @param zoom decimal number indicating current zoom multiplier
|
||||
* @returns snapped zoom level
|
||||
*/
|
||||
export function getZoomOutLevel(zoom: number): ZoomLevel {
|
||||
const snapped = getSnappedZoom(zoom);
|
||||
const index = zoomLevels.indexOf(snapped);
|
||||
|
||||
if (index === 0) {
|
||||
return snapped;
|
||||
} else {
|
||||
return zoomLevels[index - 1];
|
||||
}
|
||||
}
|
||||
|
||||
export function isMinZoom(zoom: number): boolean {
|
||||
return zoom === minZoom;
|
||||
}
|
||||
|
||||
export function isMaxZoom(zoom: number): boolean {
|
||||
return zoom === maxZoom;
|
||||
}
|
||||
@@ -812,5 +812,99 @@
|
||||
"left": 70.45245361328125,
|
||||
"top": 316.3525695800781
|
||||
}
|
||||
},
|
||||
"17": {
|
||||
"id": 17,
|
||||
"type": "tool",
|
||||
"label": "filter_failed",
|
||||
"content_id": "__FILTER_FAILED_DATASETS__",
|
||||
"name": "Filter failed datasets",
|
||||
"tool_state": {
|
||||
"input": "{\"__class__\": \"RuntimeValue\"}",
|
||||
"__page__": null,
|
||||
"__rerun_remap_job_id__": null
|
||||
},
|
||||
"errors": null,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"label": "Input Collection",
|
||||
"multiple": false,
|
||||
"input_type": "dataset_collection",
|
||||
"collection_types": [
|
||||
"list",
|
||||
"list:paired"
|
||||
],
|
||||
"optional": false,
|
||||
"extensions": [
|
||||
"data"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "output",
|
||||
"extensions": [
|
||||
"input"
|
||||
],
|
||||
"type": "collection",
|
||||
"optional": false,
|
||||
"collection": true,
|
||||
"collection_type": null,
|
||||
"collection_type_source": "input",
|
||||
"label": "input dataset(s) (filtered failed datasets)"
|
||||
}
|
||||
],
|
||||
"annotation": "",
|
||||
"post_job_actions": {},
|
||||
"uuid": "f5de18b9-b08e-4160-b71b-54768a4e5208",
|
||||
"when": null,
|
||||
"workflow_outputs": [
|
||||
{
|
||||
"output_name": "output",
|
||||
"uuid": "3774e554-b7d1-41ba-9acd-ee82ba9fbf54",
|
||||
"label": null
|
||||
}
|
||||
],
|
||||
"input_connections": {},
|
||||
"position": {
|
||||
"left": 0,
|
||||
"top": 0
|
||||
}
|
||||
},
|
||||
"18": {
|
||||
"id": 18,
|
||||
"type": "data_collection_input",
|
||||
"label": "list:paired input",
|
||||
"content_id": null,
|
||||
"name": "Input dataset collection",
|
||||
"tool_state": {
|
||||
"collection_type": "\"list:paired\"",
|
||||
"optional": "\"false\"",
|
||||
"tag": "\"\"",
|
||||
"__page__": null,
|
||||
"__rerun_remap_job_id__": null
|
||||
},
|
||||
"errors": null,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "output",
|
||||
"extensions": [
|
||||
"input"
|
||||
],
|
||||
"collection": true,
|
||||
"collection_type": "list:paired",
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"annotation": "",
|
||||
"post_job_actions": {},
|
||||
"workflow_outputs": [],
|
||||
"input_connections": {},
|
||||
"position": {
|
||||
"left": 12.8729248046875,
|
||||
"top": 501.7839660644531
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useWorkflowInstance } from "@/composables/useWorkflowInstance";
|
||||
import type { InvocationMessageResponseModel } from "./invocationMessageModel";
|
||||
import GenericHistoryItem from "@/components/History/Content/GenericItem.vue";
|
||||
import WorkflowInvocationStep from "@/components/WorkflowInvocationState/WorkflowInvocationStep.vue";
|
||||
import JobInformation from "@/components/JobInformation/JobInformation.vue";
|
||||
|
||||
type ReasonToLevel = {
|
||||
history_deleted: "cancel";
|
||||
user_request: "cancel";
|
||||
cancelled_on_review: "cancel";
|
||||
dataset_failed: "error";
|
||||
collection_failed: "error";
|
||||
job_failed: "error";
|
||||
output_not_found: "error";
|
||||
expression_evaluation_failed: "error";
|
||||
when_not_boolean: "error";
|
||||
unexpected_failure: "error";
|
||||
workflow_output_not_found: "warning";
|
||||
};
|
||||
|
||||
const level: ReasonToLevel = {
|
||||
history_deleted: "cancel",
|
||||
user_request: "cancel",
|
||||
cancelled_on_review: "cancel",
|
||||
dataset_failed: "error",
|
||||
collection_failed: "error",
|
||||
job_failed: "error",
|
||||
output_not_found: "error",
|
||||
expression_evaluation_failed: "error",
|
||||
when_not_boolean: "error",
|
||||
unexpected_failure: "error",
|
||||
workflow_output_not_found: "warning",
|
||||
};
|
||||
|
||||
const levelClasses = {
|
||||
warning: "warningmessage",
|
||||
cancel: "infomessage",
|
||||
error: "errormessage",
|
||||
};
|
||||
|
||||
interface Invocation {
|
||||
workflow_id: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
interface InvocationMessageProps {
|
||||
invocationMessage: InvocationMessageResponseModel;
|
||||
invocation: Invocation;
|
||||
}
|
||||
|
||||
const props = defineProps<InvocationMessageProps>();
|
||||
const levelClass = computed(() => levelClasses[level[props.invocationMessage.reason]]);
|
||||
|
||||
const workflow = computed(() => {
|
||||
if ("workflow_step_id" in props.invocationMessage) {
|
||||
const { workflow } = useWorkflowInstance(props.invocation.workflow_id);
|
||||
return workflow.value;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const workflowStep = computed(() => {
|
||||
if ("workflow_step_id" in props.invocationMessage && workflow.value) {
|
||||
return workflow.value.steps[props.invocationMessage.workflow_step_id];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const dependentWorkflowStep = computed(() => {
|
||||
if ("dependent_workflow_step_id" in props.invocationMessage && workflow.value) {
|
||||
const stepId = props.invocationMessage["dependent_workflow_step_id"];
|
||||
if (stepId !== undefined) {
|
||||
return workflow.value.steps[stepId];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const jobId = computed(() => "job_id" in props.invocationMessage && props.invocationMessage.job_id);
|
||||
const HdaId = computed(() => "hda_id" in props.invocationMessage && props.invocationMessage.hda_id);
|
||||
const HdcaId = computed(() => "hdca_id" in props.invocationMessage && props.invocationMessage.hdca_id);
|
||||
|
||||
const cancelFragment = "Invocation scheduling cancelled because";
|
||||
const failFragment = "Invocation scheduling failed because ";
|
||||
const stepDescription = computed(() => {
|
||||
const messageLevel = level[props.invocationMessage.reason];
|
||||
if (messageLevel === "warning") {
|
||||
return "This step caused a warning";
|
||||
} else if (messageLevel === "cancel") {
|
||||
return "This step canceled the invocation";
|
||||
} else if (messageLevel === "error") {
|
||||
return "This step failed the invocation";
|
||||
} else {
|
||||
throw "Unknown message level";
|
||||
}
|
||||
});
|
||||
|
||||
const infoString = computed(() => {
|
||||
const invocationMessage = props.invocationMessage;
|
||||
const reason = invocationMessage.reason;
|
||||
if (reason === "user_request") {
|
||||
return `${cancelFragment} user requested cancellation.`;
|
||||
} else if (reason === "history_deleted") {
|
||||
return `${cancelFragment} the history of the invocation was deleted.`;
|
||||
} else if (reason === "cancelled_on_review") {
|
||||
return `${cancelFragment} the invocation was paused at step ${
|
||||
invocationMessage.workflow_step_id + 1
|
||||
} and not approved.`;
|
||||
} else if (reason === "collection_failed") {
|
||||
return `${failFragment} step ${
|
||||
invocationMessage.workflow_step_id + 1
|
||||
} requires a dataset collection created by step ${
|
||||
invocationMessage.dependent_workflow_step_id + 1
|
||||
}, but dataset collection entered a failed state.`;
|
||||
} else if (reason === "dataset_failed") {
|
||||
if (
|
||||
invocationMessage.dependent_workflow_step_id !== null &&
|
||||
invocationMessage.dependent_workflow_step_id !== undefined
|
||||
) {
|
||||
return `${failFragment} step ${invocationMessage.workflow_step_id + 1} requires a dataset created by step ${
|
||||
invocationMessage.dependent_workflow_step_id + 1
|
||||
}, but dataset entered a failed state.`;
|
||||
} else {
|
||||
return `${failFragment} step ${
|
||||
invocationMessage.workflow_step_id + 1
|
||||
} requires a dataset, but dataset entered a failed state.`;
|
||||
}
|
||||
} else if (reason === "job_failed") {
|
||||
return `${failFragment} step ${invocationMessage.workflow_step_id + 1} depends on job(s) created in step ${
|
||||
invocationMessage.dependent_workflow_step_id + 1
|
||||
}, but a job for that step failed.`;
|
||||
} else if (reason === "output_not_found") {
|
||||
return `${failFragment} step ${invocationMessage.workflow_step_id + 1} depends on output '${
|
||||
invocationMessage.output_name
|
||||
}' of step ${
|
||||
invocationMessage.dependent_workflow_step_id + 1
|
||||
}, but this step did not produce an output of that name.`;
|
||||
} else if (reason === "expression_evaluation_failed") {
|
||||
return `${failFragment} step ${
|
||||
invocationMessage.workflow_step_id + 1
|
||||
} contains an expression that could not be evaluated.`;
|
||||
} else if (reason === "when_not_boolean") {
|
||||
return `${failFragment} step ${
|
||||
invocationMessage.workflow_step_id + 1
|
||||
} is a conditional step and the result of the when expression is not a boolean type.`;
|
||||
} else if (reason === "unexpected_failure") {
|
||||
return `${failFragment} an unexpected failure occurred.`;
|
||||
} else if (reason === "workflow_output_not_found") {
|
||||
return `Defined workflow output '${invocationMessage.output_name}' was not found in step ${
|
||||
invocationMessage.workflow_step_id + 1
|
||||
}.`;
|
||||
} else {
|
||||
return reason;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div :class="levelClass" style="text-align: center">
|
||||
{{ infoString }}
|
||||
</div>
|
||||
<div v-if="dependentWorkflowStep">
|
||||
Problem occurred at this step:
|
||||
<workflow-invocation-step
|
||||
:invocation="invocation"
|
||||
:workflow="workflow"
|
||||
:workflow-step="dependentWorkflowStep"></workflow-invocation-step>
|
||||
</div>
|
||||
<div v-if="workflowStep">
|
||||
{{ stepDescription }}
|
||||
<workflow-invocation-step
|
||||
:invocation="invocation"
|
||||
:workflow="workflow"
|
||||
:workflow-step="workflowStep"></workflow-invocation-step>
|
||||
</div>
|
||||
<div v-if="HdaId">
|
||||
This dataset failed:
|
||||
<generic-history-item :item-id="HdaId" item-src="hda" />
|
||||
</div>
|
||||
<div v-if="HdcaId">
|
||||
This dataset collection failed:
|
||||
<generic-history-item :item-id="HdcaId" item-src="hdca" />
|
||||
</div>
|
||||
<div v-if="jobId">
|
||||
This job failed:
|
||||
<job-information :job_id="jobId" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,8 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from "vue";
|
||||
|
||||
import { useWorkflowStore } from "stores/workflowStore";
|
||||
|
||||
import { useWorkflowInstance } from "@/composables/useWorkflowInstance";
|
||||
import ParameterStep from "./ParameterStep";
|
||||
import GenericHistoryItem from "components/History/Content/GenericItem";
|
||||
import WorkflowInvocationStep from "./WorkflowInvocationStep";
|
||||
@@ -14,11 +11,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const workflowStore = useWorkflowStore();
|
||||
|
||||
const workflow = computed(() => {
|
||||
return workflowStore.workflowsByInstanceId[props.invocation.workflow_id];
|
||||
});
|
||||
const { workflow } = useWorkflowInstance(props.invocation.workflow_id);
|
||||
|
||||
function dataInputStepLabel(key, input) {
|
||||
const invocationStep = props.invocation.steps[key];
|
||||
@@ -32,12 +25,6 @@ function dataInputStepLabel(key, input) {
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!workflowStore.workflowsByInstanceId[props.invocation.workflow_id]) {
|
||||
workflowStore.fetchWorkflowForInstanceId(props.invocation.workflow_id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="invocation">
|
||||
|
||||
@@ -12,18 +12,26 @@
|
||||
title="Download PDF" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-else-if="!invocationSchedulingTerminal">
|
||||
<b-alert variant="info" show>
|
||||
<LoadingSpan :message="`Waiting to complete invocation ${indexStr}`" />
|
||||
</b-alert>
|
||||
<span
|
||||
v-if="!invocationSchedulingTerminal"
|
||||
v-b-tooltip.hover
|
||||
class="fa fa-times cancel-workflow-scheduling"
|
||||
title="Cancel scheduling of workflow invocation"
|
||||
@click="onCancel"></span>
|
||||
</div>
|
||||
<progress-bar v-if="!stepCount" note="Loading step state summary..." :loading="true" class="steps-progress" />
|
||||
<template v-if="invocation.messages?.length">
|
||||
<invocation-message
|
||||
v-for="message in invocation.messages"
|
||||
:key="message.reason"
|
||||
class="steps-progress my-1"
|
||||
:invocation-message="message"
|
||||
:invocation="invocation">
|
||||
</invocation-message>
|
||||
</template>
|
||||
<progress-bar
|
||||
v-else-if="invocationState == 'cancelled'"
|
||||
note="Invocation scheduling cancelled - expected jobs and outputs may not be generated."
|
||||
@@ -57,6 +65,7 @@ import { getRootFromIndexLink } from "onload";
|
||||
import mixin from "components/JobStates/mixin";
|
||||
import ProgressBar from "components/ProgressBar";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import InvocationMessage from "@/components/WorkflowInvocationState/InvocationMessage.vue";
|
||||
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
@@ -64,6 +73,7 @@ const getUrl = (path) => getRootFromIndexLink() + path;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InvocationMessage,
|
||||
ProgressBar,
|
||||
LoadingSpan,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||
*/
|
||||
|
||||
export type InvocationMessageResponseModel =
|
||||
| GenericInvocationCancellationReviewFailedEncodedDatabaseIdField
|
||||
| GenericInvocationCancellationHistoryDeletedEncodedDatabaseIdField
|
||||
| GenericInvocationCancellationUserRequestEncodedDatabaseIdField
|
||||
| GenericInvocationFailureDatasetFailedEncodedDatabaseIdField
|
||||
| GenericInvocationFailureCollectionFailedEncodedDatabaseIdField
|
||||
| GenericInvocationFailureJobFailedEncodedDatabaseIdField
|
||||
| GenericInvocationFailureOutputNotFoundEncodedDatabaseIdField
|
||||
| GenericInvocationFailureExpressionEvaluationFailedEncodedDatabaseIdField
|
||||
| GenericInvocationFailureWhenNotBooleanEncodedDatabaseIdField
|
||||
| GenericInvocationUnexpectedFailureEncodedDatabaseIdField
|
||||
| GenericInvocationEvaluationWarningWorkflowOutputNotFoundEncodedDatabaseIdField;
|
||||
|
||||
export interface GenericInvocationCancellationHistoryDeletedEncodedDatabaseIdField {
|
||||
reason: "history_deleted";
|
||||
/**
|
||||
* History ID of history that was deleted.
|
||||
*/
|
||||
history_id: string;
|
||||
}
|
||||
export interface GenericInvocationCancellationHistoryDeletedInt {
|
||||
reason: "history_deleted";
|
||||
/**
|
||||
* History ID of history that was deleted.
|
||||
*/
|
||||
history_id: number;
|
||||
}
|
||||
export interface GenericInvocationCancellationReviewFailedEncodedDatabaseIdField {
|
||||
reason: "cancelled_on_review";
|
||||
/**
|
||||
* Workflow step id of paused step that did not pass review.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
}
|
||||
export interface GenericInvocationCancellationReviewFailedInt {
|
||||
reason: "cancelled_on_review";
|
||||
/**
|
||||
* Workflow step id of paused step that did not pass review.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
}
|
||||
export interface GenericInvocationCancellationUserRequestEncodedDatabaseIdField {
|
||||
reason: "user_request";
|
||||
}
|
||||
export interface GenericInvocationCancellationUserRequestInt {
|
||||
reason: "user_request";
|
||||
}
|
||||
export interface GenericInvocationEvaluationWarningWorkflowOutputNotFoundEncodedDatabaseIdField {
|
||||
reason: "workflow_output_not_found";
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* Output that was designated as workflow output but that has not been found
|
||||
*/
|
||||
output_name: string;
|
||||
}
|
||||
export interface GenericInvocationEvaluationWarningWorkflowOutputNotFoundInt {
|
||||
reason: "workflow_output_not_found";
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* Output that was designated as workflow output but that has not been found
|
||||
*/
|
||||
output_name: string;
|
||||
}
|
||||
export interface GenericInvocationFailureCollectionFailedEncodedDatabaseIdField {
|
||||
reason: "collection_failed";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* HistoryDatasetCollectionAssociation ID that relates to failure.
|
||||
*/
|
||||
hdca_id?: string;
|
||||
/**
|
||||
* Workflow step id of step that caused failure.
|
||||
*/
|
||||
dependent_workflow_step_id: number;
|
||||
}
|
||||
export interface GenericInvocationFailureCollectionFailedInt {
|
||||
reason: "collection_failed";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* HistoryDatasetCollectionAssociation ID that relates to failure.
|
||||
*/
|
||||
hdca_id?: number;
|
||||
/**
|
||||
* Workflow step id of step that caused failure.
|
||||
*/
|
||||
dependent_workflow_step_id: number;
|
||||
}
|
||||
export interface GenericInvocationFailureDatasetFailedEncodedDatabaseIdField {
|
||||
reason: "dataset_failed";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* HistoryDatasetAssociation ID that relates to failure.
|
||||
*/
|
||||
hda_id: string;
|
||||
/**
|
||||
* Workflow step id of step that caused failure.
|
||||
*/
|
||||
dependent_workflow_step_id?: number;
|
||||
}
|
||||
export interface GenericInvocationFailureDatasetFailedInt {
|
||||
reason: "dataset_failed";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* HistoryDatasetAssociation ID that relates to failure.
|
||||
*/
|
||||
hda_id: number;
|
||||
/**
|
||||
* Workflow step id of step that caused failure.
|
||||
*/
|
||||
dependent_workflow_step_id?: number;
|
||||
}
|
||||
export interface GenericInvocationFailureExpressionEvaluationFailedEncodedDatabaseIdField {
|
||||
reason: "expression_evaluation_failed";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* May contain details to help troubleshoot this problem.
|
||||
*/
|
||||
details?: string;
|
||||
}
|
||||
export interface GenericInvocationFailureExpressionEvaluationFailedInt {
|
||||
reason: "expression_evaluation_failed";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* May contain details to help troubleshoot this problem.
|
||||
*/
|
||||
details?: string;
|
||||
}
|
||||
export interface GenericInvocationFailureJobFailedEncodedDatabaseIdField {
|
||||
reason: "job_failed";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* Job ID that relates to failure.
|
||||
*/
|
||||
job_id?: string;
|
||||
/**
|
||||
* Workflow step id of step that caused failure.
|
||||
*/
|
||||
dependent_workflow_step_id: number;
|
||||
}
|
||||
export interface GenericInvocationFailureJobFailedInt {
|
||||
reason: "job_failed";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* Job ID that relates to failure.
|
||||
*/
|
||||
job_id?: number;
|
||||
/**
|
||||
* Workflow step id of step that caused failure.
|
||||
*/
|
||||
dependent_workflow_step_id: number;
|
||||
}
|
||||
export interface GenericInvocationFailureOutputNotFoundEncodedDatabaseIdField {
|
||||
reason: "output_not_found";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
output_name: string;
|
||||
/**
|
||||
* Workflow step id of step that caused failure.
|
||||
*/
|
||||
dependent_workflow_step_id: number;
|
||||
}
|
||||
export interface GenericInvocationFailureOutputNotFoundInt {
|
||||
reason: "output_not_found";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
output_name: string;
|
||||
/**
|
||||
* Workflow step id of step that caused failure.
|
||||
*/
|
||||
dependent_workflow_step_id: number;
|
||||
}
|
||||
export interface GenericInvocationFailureWhenNotBooleanEncodedDatabaseIdField {
|
||||
reason: "when_not_boolean";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* Contains details to help troubleshoot this problem.
|
||||
*/
|
||||
details: string;
|
||||
}
|
||||
export interface GenericInvocationFailureWhenNotBooleanInt {
|
||||
reason: "when_not_boolean";
|
||||
/**
|
||||
* Workflow step id of step that failed.
|
||||
*/
|
||||
workflow_step_id: number;
|
||||
/**
|
||||
* Contains details to help troubleshoot this problem.
|
||||
*/
|
||||
details: string;
|
||||
}
|
||||
export interface GenericInvocationUnexpectedFailureEncodedDatabaseIdField {
|
||||
reason: "unexpected_failure";
|
||||
/**
|
||||
* May contains details to help troubleshoot this problem.
|
||||
*/
|
||||
details?: string;
|
||||
}
|
||||
export interface GenericInvocationUnexpectedFailureInt {
|
||||
reason: "unexpected_failure";
|
||||
/**
|
||||
* May contains details to help troubleshoot this problem.
|
||||
*/
|
||||
details?: string;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function useDetailedDatatypes() {
|
||||
|
||||
async function getDatatypes() {
|
||||
try {
|
||||
const datatypesPromise = datatypesFetcher({ extension_only: true });
|
||||
const datatypesPromise = datatypesFetcher({ extension_only: false });
|
||||
const datatypeEDAMFormatsPromise = edamFormatsFetcher({});
|
||||
const datatypeEDAMDataPromise = edamDataFetcher({});
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ref, unref } from "vue";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import { ref, unref, type Ref } from "vue";
|
||||
import { useEventListener, type MaybeComputedRef } from "@vueuse/core";
|
||||
import { wait } from "@/utils/wait";
|
||||
|
||||
export type FileDropHandler = (event: DragEvent) => void;
|
||||
|
||||
/**
|
||||
* Custom File-Drop composable
|
||||
@@ -7,18 +10,26 @@ import { useEventListener } from "@vueuse/core";
|
||||
* @param onDrop callback function called when drop occurs
|
||||
* @param solo when true, only reacts if no modal is open
|
||||
*/
|
||||
export function useFileDrop(dropZone, onDrop, solo) {
|
||||
export function useFileDrop(
|
||||
dropZone: MaybeComputedRef<EventTarget | null | undefined>,
|
||||
onDrop: Ref<FileDropHandler> | FileDropHandler,
|
||||
solo: MaybeComputedRef<boolean>
|
||||
) {
|
||||
const isFileOverDocument = ref(false);
|
||||
const isFileOverDropZone = ref(false);
|
||||
|
||||
const dragBlocked = ref(false);
|
||||
// blocks drag events in this composable, to avoid drag events in unwanted situations
|
||||
let dragBlocked = false;
|
||||
|
||||
// keeps track if the drag has exited, to avoid premature drag canceling
|
||||
let hasExited = true;
|
||||
|
||||
// Don't react to page-internal drag events
|
||||
useEventListener(
|
||||
document.body,
|
||||
"dragstart",
|
||||
() => {
|
||||
dragBlocked.value = true;
|
||||
dragBlocked = true;
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -27,9 +38,10 @@ export function useFileDrop(dropZone, onDrop, solo) {
|
||||
document.body,
|
||||
"dragover",
|
||||
(event) => {
|
||||
if (!dragBlocked.value) {
|
||||
if (!dragBlocked) {
|
||||
// prevent the browser from opening the file
|
||||
event.preventDefault();
|
||||
hasExited = false;
|
||||
}
|
||||
},
|
||||
true
|
||||
@@ -39,40 +51,54 @@ export function useFileDrop(dropZone, onDrop, solo) {
|
||||
document.body,
|
||||
"drop",
|
||||
(event) => {
|
||||
if (!dragBlocked.value) {
|
||||
if (!dragBlocked) {
|
||||
// prevent the browser from opening the file
|
||||
event.preventDefault();
|
||||
|
||||
if (isFileOverDropZone.value && isFileOverDocument.value) {
|
||||
unref(onDrop)(event);
|
||||
const dropHandler = unref(onDrop);
|
||||
dropHandler(event as DragEvent);
|
||||
}
|
||||
}
|
||||
isFileOverDocument.value = false;
|
||||
dragBlocked.value = false;
|
||||
dragBlocked = false;
|
||||
hasExited = true;
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
useEventListener(
|
||||
document.body,
|
||||
"dragend",
|
||||
() => {
|
||||
// reset on drag end
|
||||
isFileOverDocument.value = false;
|
||||
isFileOverDropZone.value = false;
|
||||
dragBlocked.value = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
/** Reset all variables */
|
||||
const reset = () => {
|
||||
isFileOverDocument.value = false;
|
||||
isFileOverDropZone.value = false;
|
||||
dragBlocked = false;
|
||||
hasExited = true;
|
||||
};
|
||||
|
||||
useEventListener(document.body, "dragend", reset, true);
|
||||
|
||||
useEventListener(document.body, "dragleave", async () => {
|
||||
hasExited = true;
|
||||
|
||||
// This event may have been triggered by components
|
||||
// which have not been properly childed to the body yet.
|
||||
// Wait a bit, and check if hasExited is still true.
|
||||
await wait(100);
|
||||
|
||||
if (hasExited) {
|
||||
reset();
|
||||
}
|
||||
});
|
||||
|
||||
useEventListener(
|
||||
document.body,
|
||||
"dragenter",
|
||||
(event) => {
|
||||
// init values if drag is possible
|
||||
if (!dragBlocked.value && !(unref(solo) && isAnyModalOpen())) {
|
||||
if (!dragBlocked && !(unref(solo) && isAnyModalOpen())) {
|
||||
isFileOverDocument.value = true;
|
||||
isFileOverDropZone.value = false;
|
||||
hasExited = false;
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -90,6 +116,7 @@ export function useFileDrop(dropZone, onDrop, solo) {
|
||||
"dragenter",
|
||||
() => {
|
||||
isFileOverDropZone.value = true;
|
||||
hasExited = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -99,6 +126,7 @@ export function useFileDrop(dropZone, onDrop, solo) {
|
||||
"dragleave",
|
||||
() => {
|
||||
isFileOverDropZone.value = false;
|
||||
hasExited = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useWorkflowStore } from "@/stores/workflowStore";
|
||||
import { ref } from "vue";
|
||||
|
||||
export function useWorkflowInstance(workflowId: string) {
|
||||
const workflowStore = useWorkflowStore();
|
||||
const workflow = ref(workflowStore.getWorkflowByInstanceId(workflowId));
|
||||
const loading = ref(false);
|
||||
|
||||
async function getWorkflowInstance() {
|
||||
if (!workflow.value) {
|
||||
loading.value = true;
|
||||
try {
|
||||
await workflowStore.fetchWorkflowForInstanceId(workflowId);
|
||||
} catch (e) {
|
||||
loading.value = false;
|
||||
console.error("unable to fetch workflow \n", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
getWorkflowInstance();
|
||||
|
||||
return { workflow, loading };
|
||||
}
|
||||
@@ -235,6 +235,12 @@ export function getRouter(Galaxy) {
|
||||
component: HistoryPublishedList,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "histories/list",
|
||||
component: GridHistory,
|
||||
props: true,
|
||||
redirect: redirectAnon(),
|
||||
},
|
||||
{
|
||||
path: "histories/:historyId/export",
|
||||
get component() {
|
||||
@@ -242,11 +248,6 @@ export function getRouter(Galaxy) {
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "histories/:actionId",
|
||||
component: GridHistory,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "interactivetool_entry_points/list",
|
||||
component: InteractiveTools,
|
||||
|
||||
@@ -3,7 +3,7 @@ import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
//temporary
|
||||
import { appendVueComponent } from "utils/mountVueComponent";
|
||||
import { replaceChildrenWithComponent } from "utils/mountVueComponent";
|
||||
import TabularChunkedView from "components/Visualizations/Tabular/TabularChunkedView.vue";
|
||||
|
||||
/**
|
||||
@@ -72,5 +72,5 @@ export var DatasetCollection = Backbone.Collection.extend({
|
||||
|
||||
export const createTabularDatasetChunkedView = (options) => {
|
||||
// We'll always have a parent_elt in options, so create a div and insert it into that.
|
||||
return appendVueComponent(options.parent_elt, TabularChunkedView, { options });
|
||||
return replaceChildrenWithComponent(options.parent_elt, TabularChunkedView, { options });
|
||||
};
|
||||
|
||||
@@ -31,12 +31,7 @@ export default Backbone.View.extend({
|
||||
this.$buttons = this.$(".charts-buttons");
|
||||
this.chart = new Chart({}, options);
|
||||
this.chart.plugin = options.visualization_plugin;
|
||||
this.chart.plugin.specs = this.chart.plugin.specs || {};
|
||||
if (Object.keys(this.chart.plugin.specs).length === 0) {
|
||||
this.chart.requiresConfirmation = false;
|
||||
} else {
|
||||
this.chart.requiresConfirmation = asBoolean(this.chart.plugin.specs.confirm);
|
||||
}
|
||||
this.chart.requiresConfirmation = asBoolean(this.chart.plugin.specs?.confirm);
|
||||
this.chart_load = options.chart_load;
|
||||
this.message = new Ui.Message();
|
||||
this.deferred = new Deferred();
|
||||
|
||||
@@ -8,7 +8,7 @@ import Utils from "utils/utils";
|
||||
import Repeat from "./repeat";
|
||||
import { visitInputs } from "components/Form/utilities";
|
||||
import FormDisplay from "components/Form/FormDisplay";
|
||||
import { appendVueComponent } from "utils/mountVueComponent";
|
||||
import { replaceChildrenWithComponent } from "utils/mountVueComponent";
|
||||
|
||||
var GroupView = Backbone.View.extend({
|
||||
initialize: function (app, options) {
|
||||
@@ -75,7 +75,7 @@ var GroupView = Backbone.View.extend({
|
||||
params[name] = input.value;
|
||||
});
|
||||
self.redraw(params);
|
||||
const instance = appendVueComponent(self.$el, FormDisplay, {
|
||||
const instance = replaceChildrenWithComponent(self.el, FormDisplay, {
|
||||
inputs: inputs,
|
||||
});
|
||||
instance.$on("onChange", (data) => {
|
||||
|
||||
@@ -134,14 +134,14 @@ export default Backbone.View.extend({
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var visible = this.model.get("visible");
|
||||
const visible = this.model.get("visible");
|
||||
this.app.$el[visible ? "removeClass" : "addClass"]("charts-fullscreen");
|
||||
this.execute_button.model.set("visible", visible && !!this.app.chart.plugin.specs.confirm);
|
||||
this.execute_button.model.set("visible", visible && !!this.app.chart.plugin.specs?.confirm);
|
||||
this.save_button.model.set("visible", visible);
|
||||
this.export_button.model.set("visible", visible);
|
||||
this.right_button.model.set("visible", visible);
|
||||
this.left_button.model.set("visible", !visible);
|
||||
var exports = this.app.chart.plugin.specs.exports || [];
|
||||
const exports = this.app.chart.plugin.specs?.exports ?? [];
|
||||
this.export_button.collection.each((model) => {
|
||||
model.set("visible", exports.indexOf(model.get("key")) !== -1);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import Backbone from "backbone";
|
||||
import Utils from "utils/utils";
|
||||
import { visitInputs } from "components/Form/utilities";
|
||||
import FormDisplay from "components/Form/FormDisplay";
|
||||
import { appendVueComponent } from "utils/mountVueComponent";
|
||||
import { replaceChildrenWithComponent } from "utils/mountVueComponent";
|
||||
|
||||
export default Backbone.View.extend({
|
||||
initialize: function (app) {
|
||||
@@ -27,7 +27,7 @@ export default Backbone.View.extend({
|
||||
self.chart.settings.set(name, input.value);
|
||||
}
|
||||
});
|
||||
const instance = appendVueComponent(this.$el, FormDisplay, {
|
||||
const instance = replaceChildrenWithComponent(this.el, FormDisplay, {
|
||||
inputs: inputs,
|
||||
});
|
||||
instance.$on("onChange", (data) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user