mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge branch 'dev' into activate_toolshed
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# Python CircleCI 2.0 configuration file
|
||||
version: 2
|
||||
variables:
|
||||
restore_repo_cache: &restore_repo_cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v1-repo-{{ .Environment.CIRCLE_SHA1 }}
|
||||
restore_yarn_cache: &restore_yarn_cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v1-repo-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- yarn-packages-{{ checksum "client/yarn.lock" }}
|
||||
save_yarn_cache: &save_yarn_cache
|
||||
save_cache:
|
||||
key: yarn-packages-{{ checksum "client/yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
install_tox: &install_tox
|
||||
run: sudo pip install tox
|
||||
set_workdir: &set_workdir
|
||||
working_directory: ~/repo
|
||||
requires_get_code: &requires_get_code
|
||||
requires:
|
||||
- get_code
|
||||
jobs:
|
||||
get_code:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
# Replace standard code checkout with shallow clone to speed things up.
|
||||
- run:
|
||||
name: Checkout code
|
||||
command: |-
|
||||
# Add github.com to known hosts
|
||||
mkdir -p ~/.ssh
|
||||
echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
|
||||
' >> ~/.ssh/known_hosts
|
||||
|
||||
# Add the user ssh key and set correct perms
|
||||
(umask 077; touch ~/.ssh/id_rsa)
|
||||
chmod 0600 ~/.ssh/id_rsa
|
||||
echo "$CHECKOUT_KEY" > ~/.ssh/id_rsa
|
||||
|
||||
# Use git+ssh instead of https
|
||||
git config --global url."ssh://git@github.com".insteadOf "https://github.com" || true
|
||||
git config --global gc.auto 0 || true
|
||||
|
||||
# Shallow clone
|
||||
git clone --depth=1 "${CIRCLE_REPOSITORY_URL}" .
|
||||
|
||||
if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then
|
||||
# Update PR refs for testing.
|
||||
FETCH_REFS="${FETCH_REFS} +refs/pull/${CIRCLE_PR_NUMBER}/head:pr/${CIRCLE_PR_NUMBER}/head"
|
||||
FETCH_REFS="${FETCH_REFS} +refs/pull/${CIRCLE_PR_NUMBER}/merge:pr/${CIRCLE_PR_NUMBER}/merge"
|
||||
|
||||
# Retrieve the refs
|
||||
git fetch --force origin ${FETCH_REFS}
|
||||
|
||||
# Checkout PR merge ref.
|
||||
git checkout -f "pr/${CIRCLE_PR_NUMBER}/merge"
|
||||
|
||||
# Test for *some* merge conflicts.
|
||||
git branch --merged | grep "pr/${CIRCLE_PR_NUMBER}/head" > /dev/null
|
||||
else
|
||||
if [ -n "$CIRCLE_TAG" ]; then
|
||||
git fetch --depth=1 --force origin "refs/tags/${CIRCLE_TAG}"
|
||||
else
|
||||
git fetch --depth=1 --force origin "$CIRCLE_BRANCH:remotes/origin/$CIRCLE_BRANCH"
|
||||
fi
|
||||
if [ -n "$CIRCLE_TAG" ]; then
|
||||
git reset --hard "$CIRCLE_SHA1"
|
||||
git checkout "$CIRCLE_TAG"
|
||||
elif [ -n "$CIRCLE_BRANCH" ]; then
|
||||
git reset --hard "$CIRCLE_SHA1"
|
||||
git checkout -B "$CIRCLE_BRANCH"
|
||||
fi
|
||||
git reset --hard "${CIRCLE_SHA1}"
|
||||
fi
|
||||
- save_cache:
|
||||
key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
|
||||
paths:
|
||||
- ~/repo
|
||||
py27_lint:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py27-lint
|
||||
py27_unit:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py27-unit
|
||||
py27_docstring:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py27-lint_docstring_include_list
|
||||
py27_first_startup:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- run: sh scripts/common_startup.sh
|
||||
- run: wget -q https://github.com/jmchilton/galaxy-downloads/raw/master/db_gx_rev_0127.sqlite
|
||||
- run: mv db_gx_rev_0127.sqlite database/universe.sqlite
|
||||
- run: sh manage_db.sh -c ./config/galaxy.yml.sample upgrade
|
||||
- *install_tox
|
||||
- run: tox -e py27-first_startup
|
||||
py35_lint:
|
||||
docker:
|
||||
- image: circleci/python:3.5
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py35-lint
|
||||
py35_unit:
|
||||
docker:
|
||||
- image: circleci/python:3.5
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py35-unit
|
||||
py35_first_startup:
|
||||
docker:
|
||||
- image: circleci/python:3.5
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: sudo apt-get update
|
||||
# For uwsgi
|
||||
- run: sudo apt-get install -y libpython3.5-dev
|
||||
- run: tox -e py35-first_startup
|
||||
validate_test_tools:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- run: sudo apt-get update
|
||||
- run: sudo apt-get install -y libxml2-utils
|
||||
- *install_tox
|
||||
- run: tox -e validate_test_tools
|
||||
check_py3_compatibility:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- run: sudo apt-get update
|
||||
- run: sudo apt-get install -y ack-grep
|
||||
- *install_tox
|
||||
- run: tox -e check_py3_compatibility
|
||||
js_unit:
|
||||
docker:
|
||||
- image: circleci/node:10-browsers
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_yarn_cache
|
||||
- run: cd client && yarn install --frozen-lockfile
|
||||
- *save_yarn_cache
|
||||
- run: cd client && yarn run build
|
||||
- run: cd client && yarn run test
|
||||
js_lint:
|
||||
docker:
|
||||
- image: circleci/node:10-browsers
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_yarn_cache
|
||||
- run: cd client && yarn install --frozen-lockfile
|
||||
- *save_yarn_cache
|
||||
- run: cd client && yarn run eslint
|
||||
workflows:
|
||||
version: 2
|
||||
get_code_and_test:
|
||||
jobs:
|
||||
- get_code
|
||||
- py27_lint:
|
||||
<<: *requires_get_code
|
||||
- py27_unit:
|
||||
<<: *requires_get_code
|
||||
- py27_first_startup:
|
||||
<<: *requires_get_code
|
||||
- py27_docstring:
|
||||
<<: *requires_get_code
|
||||
- py35_lint:
|
||||
<<: *requires_get_code
|
||||
- py35_unit:
|
||||
<<: *requires_get_code
|
||||
- py35_first_startup:
|
||||
<<: *requires_get_code
|
||||
- validate_test_tools:
|
||||
<<: *requires_get_code
|
||||
- check_py3_compatibility:
|
||||
<<: *requires_get_code
|
||||
- js_unit:
|
||||
<<: *requires_get_code
|
||||
- js_lint:
|
||||
<<: *requires_get_code
|
||||
@@ -145,10 +145,16 @@ doc/source/api/api.rst
|
||||
doc/source/api/ts_api.rst
|
||||
doc/source/dev/schema.md
|
||||
doc/source/dev/schema.rst
|
||||
client/styleguide
|
||||
|
||||
# webpack stats
|
||||
client/webpack-stats.json
|
||||
|
||||
# packages
|
||||
packages/*/build
|
||||
packages/*/dist
|
||||
packages/*/*.egg-info
|
||||
|
||||
# Misc
|
||||
*.orig
|
||||
.DS_Store
|
||||
|
||||
+12
-64
@@ -1,69 +1,10 @@
|
||||
language: python
|
||||
python: 2.7
|
||||
os:
|
||||
- linux
|
||||
os: osx
|
||||
# No version of Python is available via virtualenv on OS X workers, see https://github.com/travis-ci/travis-ci/issues/2312
|
||||
language: generic
|
||||
|
||||
env:
|
||||
- TOX_ENV=py27-lint
|
||||
- TOX_ENV=py27-unit
|
||||
- TOX_ENV=py27-first_startup
|
||||
- TOX_ENV=py27-lint_docstring_include_list
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- env: TOX_ENV=py35-lint
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- python3.5
|
||||
- env: TOX_ENV=py35-first_startup
|
||||
addons:
|
||||
&py3_addons
|
||||
apt:
|
||||
packages:
|
||||
- python3.5
|
||||
# For psutil, pyyaml, uwsgi...
|
||||
- libpython3.5-dev
|
||||
- env: TOX_ENV=validate_test_tools
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libxml2-utils
|
||||
- env: TOX_ENV=check_py3_compatibility
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- ack-grep
|
||||
- env: TOX_ENV=py35-unit
|
||||
addons: *py3_addons
|
||||
- os: osx
|
||||
# No version of Python is available via virtualenv on OS X workers, see https://github.com/travis-ci/travis-ci/issues/2312
|
||||
language: generic
|
||||
env: TOX_ENV=py27-first_startup
|
||||
- name: js-unit
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
before_install:
|
||||
- cd client
|
||||
install:
|
||||
- yarn
|
||||
- yarn run build
|
||||
script:
|
||||
- yarn run test
|
||||
- name: js-lint
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
before_install:
|
||||
- cd client
|
||||
install:
|
||||
- yarn
|
||||
script:
|
||||
- yarn run eslint
|
||||
|
||||
before_install:
|
||||
# Workaround for https://github.com/travis-ci/travis-ci/issues/7940
|
||||
- sudo rm -f /etc/boto.cfg
|
||||
- TOX_ENV=py35-first_startup
|
||||
|
||||
install:
|
||||
- set -e
|
||||
@@ -74,6 +15,13 @@ install:
|
||||
wget -q https://github.com/jmchilton/galaxy-downloads/raw/master/db_gx_rev_0127.sqlite
|
||||
mv db_gx_rev_0127.sqlite database/universe.sqlite
|
||||
sh manage_db.sh -c ./config/galaxy.yml.sample upgrade
|
||||
elif [ "$TOX_ENV" == "py35-first_startup" ]; then
|
||||
MINICONDA_URL="https://repo.anaconda.com/miniconda"
|
||||
MINICONDA_FILE="Miniconda3-latest-MacOSX-x86_64.sh"
|
||||
curl -L -O "${MINICONDA_URL}/${MINICONDA_FILE}"
|
||||
bash $MINICONDA_FILE -b
|
||||
~/miniconda3/bin/conda create --yes --override-channels --channel conda-forge --channel defaults --name _galaxy_ 'python=3.5' 'pip>=9' 'virtualenv>=16'
|
||||
. ~/miniconda3/bin/activate _galaxy_
|
||||
fi
|
||||
|
||||
script: tox -e $TOX_ENV
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
If you use or extend Galaxy in your published work, please cite this
|
||||
publication:
|
||||
|
||||
- Afgan E, Baker D, van den Beek M, Blankenberg D, Bouvier D, Čech M, Chilton
|
||||
J, Clements D, Coraor N, Eberhard C, Grüning B, Guerler A, Hillman-Jackson
|
||||
J, Von Kuster G, Rasche E, Soranzo N, Turaga N, Taylor J, Nekrutenko A, Goecks
|
||||
J. "The Galaxy platform for accessible, reproducible and collaborative
|
||||
biomedical analyses: 2016 update"
|
||||
Nucleic Acids Research (2016) 44(W1):W3-W10 doi:10.1093/nar/gkw343
|
||||
- Afgan E, Baker D, Batut B, van den Beek M, Bouvier D, Čech M, Chilton J,
|
||||
Clements D, Coraor N, Grüning BA, Guerler A, Hillman-Jackson J, Hiltemann S,
|
||||
Jalili V, Rasche H, Soranzo N, Goecks J, Taylor J, Nekrutenko A, Blankenberg
|
||||
D. "The Galaxy platform for accessible, reproducible and collaborative
|
||||
biomedical analyses: 2018 update"
|
||||
Nucleic Acids Res. (2018) 46(W1):W537-W544 doi:10.1093/nar/gky379
|
||||
|
||||
BibTeX format:
|
||||
|
||||
@article{afgan2016galaxy,
|
||||
title={The {Galaxy} platform for accessible, reproducible and collaborative biomedical analyses: 2016 update},
|
||||
author={Afgan, Enis and Baker, Dannon and van den Beek, Marius and Blankenberg, Daniel and Bouvier, Dave and \v{C}ech, Martin and Chilton, John and Clements, Dave and Coraor, Nate and Eberhard, Carl and Gr\"uning, Bj\"orn and Guerler, Aysam and Hillman-Jackson, Jennifer and Von Kuster, Greg and Rasche, Eric and Soranzo, Nicola and Turaga, Nitesh and Taylor, James and Nekrutenko, Anton and Goecks, Jeremy},
|
||||
journal={Nucleic Acids Res.},
|
||||
volume={44},
|
||||
number={W1},
|
||||
pages={W3-W10},
|
||||
year={2016},
|
||||
publisher={Oxford University Press}
|
||||
@article{10.1093/nar/gky379,
|
||||
title = {The {Galaxy} platform for accessible, reproducible and collaborative biomedical analyses: 2018 update},
|
||||
author = {Afgan, Enis and Baker, Dannon and Batut, B\'{e}r\'{e}nice and van den Beek, Marius and Bouvier, Dave and \v{C}ech, Martin and Chilton, John and Clements, Dave and Coraor, Nate and Gr\"uning, Bj\"orn A. and Guerler, Aysam and Hillman-Jackson, Jennifer and Hiltemann, Saskia and Jalili, Vahid and Rasche, Helena and Soranzo, Nicola and Goecks, Jeremy and Taylor, James and Nekrutenko, Anton and Blankenberg, Daniel},
|
||||
journal = {Nucleic Acids Res.},
|
||||
doi = {10.1093/nar/gky379},
|
||||
volume = {46},
|
||||
number = {W1},
|
||||
pages = {W537-W544},
|
||||
year = {2018},
|
||||
url = {https://doi.org/10.1093/nar/gky379},
|
||||
publisher = {Oxford University Press}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ The following individuals have contributed code to Galaxy:
|
||||
* Gianmauro Cuccuru <gmauro@crs4.it>
|
||||
* Frederik Delaere <frederik.delaere@gmail.com>
|
||||
* Matthias Desmet <matthias.desmet@ugent.be>
|
||||
* Matthew Ryan Dillon <matthewrdillon@gmail.com>
|
||||
* Olivia Doppelt <olivia.doppelt@pasteur.fr>
|
||||
* Shane Dowling <shane@shanedowling.com>
|
||||
* John Duddy <jduddy@illumina.com>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Location of virtualenv used for development.
|
||||
VENV?=.venv
|
||||
# Source virtualenv to execute command (flake8, sphinx, twine, etc...)
|
||||
IN_VENV=if [ -f $(VENV)/bin/activate ]; then . $(VENV)/bin/activate; fi;
|
||||
RELEASE_CURR:=16.01
|
||||
RELEASE_CURR_MINOR_NEXT:=$(shell python scripts/bootstrap_history.py --print-next-minor-version)
|
||||
RELEASE_CURR_MINOR_NEXT:=$(shell $(IN_VENV) python scripts/bootstrap_history.py --print-next-minor-version)
|
||||
RELEASE_NEXT:=16.04
|
||||
# TODO: This needs to be updated with create_release_rc
|
||||
#RELEASE_NEXT_BRANCH:=release_$(RELEASE_NEXT)
|
||||
RELEASE_NEXT_BRANCH:=dev
|
||||
RELEASE_UPSTREAM:=upstream
|
||||
MY_UPSTREAM:=origin
|
||||
# Location of virtualenv used for development.
|
||||
VENV?=.venv
|
||||
# Source virtualenv to execute command (flake8, sphinx, twine, etc...)
|
||||
IN_VENV=if [ -f $(VENV)/bin/activate ]; then . $(VENV)/bin/activate; fi;
|
||||
CONFIG_MANAGE=$(IN_VENV) python lib/galaxy/webapps/config_manage.py
|
||||
PROJECT_URL?=https://github.com/galaxyproject/galaxy
|
||||
DOCS_DIR=doc
|
||||
@@ -129,18 +129,21 @@ release-check-blocking-prs: ## Check github for release blocking PRs
|
||||
release-bootstrap-history: ## bootstrap history for a new release
|
||||
$(IN_VENV) python scripts/bootstrap_history.py --release $(RELEASE_CURR)
|
||||
|
||||
update-dependencies: ## update linting + dev dependencies
|
||||
sh lib/galaxy/dependencies/pipfiles/update.sh
|
||||
build-dependencies-docker: ## Builds the docker container used for dependency updates
|
||||
$(MAKE) -C lib/galaxy/dependencies/pipfiles/docker
|
||||
|
||||
update-and-commit-dependencies: ## update and commit linting + dev dependencies
|
||||
sh lib/galaxy/dependencies/pipfiles/update.sh -c
|
||||
update-dependencies: build-dependencies-docker ## update linting + dev dependencies
|
||||
sh lib/galaxy/dependencies/pipfiles/update.sh -d
|
||||
|
||||
update-and-commit-dependencies: build-dependencies-docker ## update and commit linting + dev dependencies
|
||||
sh lib/galaxy/dependencies/pipfiles/update.sh -d -c
|
||||
|
||||
node-deps: ## Install NodeJS dependencies.
|
||||
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;
|
||||
else
|
||||
cd client && yarn install --network-timeout 120000 --check-files
|
||||
cd client && yarn install --network-timeout 300000 --check-files
|
||||
endif
|
||||
|
||||
|
||||
|
||||
+2
-4
@@ -1,11 +1,9 @@
|
||||
.. figure:: https://galaxyproject.org/images/galaxy-logos/galaxy_project_logo.jpg
|
||||
:alt: Galaxy Logo
|
||||
|
||||
The latest information about Galaxy is available via `https://galaxyproject.org/ <https://galaxyproject.org/>`__
|
||||
The latest information about Galaxy can be found on the `Galaxy Community Hub <https://galaxyproject.org/>`__.
|
||||
|
||||
.. image:: https://img.shields.io/badge/questions-galaxy%20biostar-blue.svg
|
||||
:target: https://biostar.usegalaxy.org
|
||||
:alt: Ask a question
|
||||
Community support is available at `Galaxy Help <https://help.galaxyproject.org/>`__.
|
||||
|
||||
.. image:: https://img.shields.io/badge/chat-irc.freenode.net%23galaxyproject-blue.svg
|
||||
:target: https://webchat.freenode.net/?channels=galaxyproject
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": ["transform-vue-template", "@babel/plugin-syntax-dynamic-import"],
|
||||
"ignore": ["i18n.js", "utils/localization.js", "nls/*"]
|
||||
}
|
||||
+11
-13
@@ -2,32 +2,30 @@ Client Build System
|
||||
===================
|
||||
|
||||
Installs, stages, and builds the client-side scripts necessary for running the
|
||||
Galaxy webapp. There's no need to use this system unless you are modifying or
|
||||
developing client-side scripts, or are running the development branch of
|
||||
Galaxy. When started through `run.sh` or any other method that utilizes
|
||||
`common_startup.sh`, Galaxy will also (since 18.09) *automatically* build
|
||||
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
|
||||
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
|
||||
`source .venv/bin/activate` from the Galaxy root directory.
|
||||
`. .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
|
||||
nodejs.org and yarnpkg.com.
|
||||
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/ and https://yarnpkg.com/ .
|
||||
|
||||
The Galaxy client build has necessarily grown more complex in the past several
|
||||
years, but we're still trying to keep things as simple as possible for
|
||||
developers (everyone, really). If you're having any trouble with building the
|
||||
client after following the instructions below please create an issue on GitHub
|
||||
years, but we are still trying to keep things as simple as possible for
|
||||
everyone. If you're having any trouble with building the
|
||||
client after following the instructions below, please create an issue on GitHub
|
||||
or reach out for help directly on Gitter at
|
||||
https://gitter.im/galaxyproject/Lobby.
|
||||
https://gitter.im/galaxyproject/Lobby .
|
||||
|
||||
|
||||
Complete Client Build
|
||||
================================================
|
||||
=====================
|
||||
|
||||
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
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"homepage": "usegalaxy.org",
|
||||
"dependencies": {
|
||||
"farbtastic": "~2.0.0-alpha.1",
|
||||
"toastr": "~2.1.0",
|
||||
"jquery-form": "~3.46.0",
|
||||
"jquery-autocomplete": "git://github.com/dyve/jquery-autocomplete",
|
||||
"select2": "~3.5.2",
|
||||
|
||||
@@ -30,7 +30,7 @@ var AdminToolshedRouter = Backbone.Router.extend({
|
||||
"status/r/:repositories": "status",
|
||||
status: "status",
|
||||
"categories/s/:tool_shed": "categories",
|
||||
"category/s/:tool_shed/c/:cateory_id": "repositories",
|
||||
"category/s/:tool_shed/c/:category_id/k/:sort_key/p/:page/t/:sort_order": "repositories",
|
||||
"repository/s/:tool_shed/r/:repository_id": "repository"
|
||||
},
|
||||
|
||||
@@ -71,12 +71,18 @@ var GalaxyAdminToolshedApp = Backbone.View.extend({
|
||||
tool_shed: tool_shed.replace(/\//g, "%2f")
|
||||
});
|
||||
});
|
||||
this.admin_toolshed_router.on("route:repositories", (tool_shed, category_id) => {
|
||||
Galaxy.admintoolshedapp.adminShedCategoryView = new mod_repositories_view.Category({
|
||||
tool_shed: tool_shed.replace(/\//g, "%2f"),
|
||||
category_id: category_id
|
||||
});
|
||||
});
|
||||
this.admin_toolshed_router.on(
|
||||
"route:repositories",
|
||||
(tool_shed, category_id, sort_key = "name", page = 1, sort_order = "asc") => {
|
||||
Galaxy.admintoolshedapp.adminShedCategoryView = new mod_repositories_view.Category({
|
||||
tool_shed: tool_shed.replace(/\//g, "%2f"),
|
||||
category_id: category_id,
|
||||
page: page,
|
||||
sort_order: sort_order,
|
||||
sort_key: sort_key
|
||||
});
|
||||
}
|
||||
);
|
||||
this.admin_toolshed_router.on("route:repository", (tool_shed, repository_id) => {
|
||||
Galaxy.admintoolshedapp.adminRepositoryView = new mod_repository_view.RepoDetails({
|
||||
tool_shed: tool_shed.replace(/\//g, "%2f"),
|
||||
|
||||
@@ -96,7 +96,7 @@ export let chartUtilities = {
|
||||
|
||||
export { initMasthead } from "components/Masthead/initMasthead";
|
||||
export { panelManagement } from "onload/globalInits/panelManagement";
|
||||
export { init_tag_click_function } from "ui/autocom_tagging";
|
||||
export { mountMakoTags } from "components/Tags";
|
||||
|
||||
// Used in common.mako
|
||||
export { default as store } from "store";
|
||||
export { default as store } from "storemodern";
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<b-modal class="data-dialog-modal" v-model="modalShow" :ok-only="true" ok-title="Close">
|
||||
<template slot="modal-header">
|
||||
<b-input-group v-if="optionsShow">
|
||||
<b-input v-model="filter" placeholder="Type to Search" />
|
||||
<b-input-group-append>
|
||||
<b-btn :disabled="!filter" @click="filter = ''">Clear</b-btn>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</template>
|
||||
<b-alert v-if="errorMessage" variant="danger" :show="errorShow"> {{ errorMessage }} </b-alert>
|
||||
<div v-else>
|
||||
<div v-if="optionsShow">
|
||||
<b-table
|
||||
small
|
||||
hover
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
:filter="filter"
|
||||
@row-clicked="clicked"
|
||||
@filtered="filtered"
|
||||
>
|
||||
<template slot="name" slot-scope="data">
|
||||
<i v-if="data.item.history_content_type == 'dataset'" class="fa fa-file-o" />
|
||||
<i v-else class="fa fa-copy" /> {{ data.item.hid }}: {{ data.value }}
|
||||
</template>
|
||||
<template slot="extension" slot-scope="data">
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="update_time" slot-scope="data">
|
||||
{{ data.value ? data.value.substring(0, 16).replace("T", " ") : "-" }}
|
||||
</template>
|
||||
</b-table>
|
||||
<div v-if="nItems == 0">No search results found for: {{ this.filter }}.</div>
|
||||
</div>
|
||||
<div v-else><span class="fa fa-spinner fa-spin" /> <span>Please wait...</span></div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
callback: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fields: {
|
||||
name: {
|
||||
sortable: true
|
||||
},
|
||||
extension: {
|
||||
sortable: true
|
||||
},
|
||||
update_time: {
|
||||
sortable: true
|
||||
}
|
||||
},
|
||||
filter: null,
|
||||
nItems: 0,
|
||||
currentPage: 0,
|
||||
perPage: 10,
|
||||
items: [],
|
||||
errorMessage: null,
|
||||
errorShow: true,
|
||||
historyId: null,
|
||||
modalShow: true,
|
||||
optionsShow: false
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
filtered: function(items) {
|
||||
this.nItems = items.length;
|
||||
},
|
||||
clicked: function(record) {
|
||||
let host = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
|
||||
this.callback(`${host}/${record.url}/display`);
|
||||
this.modalShow = false;
|
||||
},
|
||||
load: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
this.historyId = Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model.id;
|
||||
if (this.historyId) {
|
||||
axios
|
||||
.get(`${getAppRoot()}api/histories/${this.historyId}/contents`)
|
||||
.then(response => {
|
||||
this.items = response.data;
|
||||
this.optionsShow = true;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.response) {
|
||||
this.errorMessage =
|
||||
e.response.data.err_msg || `${e.response.statusText} (${e.response.status})`;
|
||||
} else {
|
||||
this.errorMessage = "Server unavailable.";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.errorMessage = "History not accessible.";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.data-dialog-modal .modal-body {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
import sinon from "sinon";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import DataDialog from "./DataDialog.vue";
|
||||
import { __RewireAPI__ as rewire } from "./DataDialog";
|
||||
import { Model } from "./model.js";
|
||||
import { UrlTracker } from "./utilities.js";
|
||||
import { Services } from "./services";
|
||||
import Vue from "vue";
|
||||
|
||||
const mockOptions = {
|
||||
callback: () => {},
|
||||
host: "host",
|
||||
root: "root",
|
||||
history: "history"
|
||||
};
|
||||
|
||||
describe("model.js", () => {
|
||||
let result = null;
|
||||
it("Model operations for single, no format", () => {
|
||||
let model = new Model();
|
||||
try {
|
||||
model.add({ idx: 1 });
|
||||
throw "Accepted invalid record.";
|
||||
} catch (error) {
|
||||
expect(error).to.equals("Invalid record with no <id>.");
|
||||
}
|
||||
model.add({ id: 1 });
|
||||
expect(model.count()).to.equals(1);
|
||||
expect(model.exists(1)).to.equals(true);
|
||||
model.add({ id: 2, tag: "tag" });
|
||||
expect(model.count()).to.equals(1);
|
||||
expect(model.exists(1)).to.equals(false);
|
||||
expect(model.exists(2)).to.equals(true);
|
||||
result = model.finalize();
|
||||
expect(result.id).to.equals(2);
|
||||
expect(result.tag).to.equals("tag");
|
||||
});
|
||||
it("Model operations for multiple, with format", () => {
|
||||
let model = new Model({ multiple: true, format: "tag" });
|
||||
model.add({ id: 1, tag: "tag_1" });
|
||||
expect(model.count()).to.equals(1);
|
||||
model.add({ id: 2, tag: "tag_2" });
|
||||
expect(model.count()).to.equals(2);
|
||||
result = model.finalize();
|
||||
expect(result.length).to.equals(2);
|
||||
expect(result[0]).to.equals("tag_1");
|
||||
expect(result[1]).to.equals("tag_2");
|
||||
model.add({ id: 1 });
|
||||
expect(model.count()).to.equals(1);
|
||||
result = model.finalize();
|
||||
expect(result[0]).to.equals("tag_2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("utilities.js/UrlTracker", () => {
|
||||
it("Test url tracker", () => {
|
||||
let urlTracker = new UrlTracker("url_initial");
|
||||
let url = urlTracker.getUrl();
|
||||
expect(url).to.equals("url_initial");
|
||||
expect(urlTracker.atRoot()).to.equals(true);
|
||||
url = urlTracker.getUrl("url_1");
|
||||
expect(url).to.equals("url_1");
|
||||
expect(urlTracker.atRoot()).to.equals(false);
|
||||
url = urlTracker.getUrl("url_2");
|
||||
expect(url).to.equals("url_2");
|
||||
expect(urlTracker.atRoot()).to.equals(false);
|
||||
url = urlTracker.getUrl();
|
||||
expect(url).to.equals("url_1");
|
||||
expect(urlTracker.atRoot()).to.equals(false);
|
||||
url = urlTracker.getUrl();
|
||||
expect(url).to.equals("url_initial");
|
||||
expect(urlTracker.atRoot()).to.equals(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("services/Services:isDataset", () => {
|
||||
it("Test dataset identifier", () => {
|
||||
let services = new Services(mockOptions);
|
||||
expect(services.isDataset({})).to.equals(false);
|
||||
expect(services.isDataset({ history_content_type: "dataset" })).to.equals(true);
|
||||
expect(services.isDataset({ history_content_type: "xyz" })).to.equals(false);
|
||||
expect(services.isDataset({ type: "file" })).to.equals(true);
|
||||
expect(services.getRecord({ hid: 1, history_content_type: "dataset" }).isDataset).to.equals(true);
|
||||
expect(services.getRecord({ hid: 2, history_content_type: "xyz" }).isDataset).to.equals(false);
|
||||
expect(services.getRecord({ type: "file" }).isDataset).to.equals(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("services.js/Services", () => {
|
||||
it("Test data population from raw data", () => {
|
||||
let rawData = {
|
||||
hid: 1,
|
||||
id: 1,
|
||||
history_id: 0,
|
||||
name: "name_1"
|
||||
};
|
||||
let services = new Services(mockOptions);
|
||||
let items = services.getItems(rawData);
|
||||
expect(items.length).to.equals(1);
|
||||
let first = items[0];
|
||||
expect(first.name).to.equals("1: name_1");
|
||||
expect(first.download).to.equals("host/api/histories/0/contents/1/display");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DataDialog.vue", () => {
|
||||
let stub;
|
||||
let wrapper;
|
||||
let emitted;
|
||||
|
||||
let rawData = [
|
||||
{
|
||||
id: 1,
|
||||
hid: 1,
|
||||
name: "dataset_1",
|
||||
history_content_type: "dataset"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "dataset_2",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
hid: 3,
|
||||
name: "collection_1",
|
||||
history_content_type: "dataset_collection"
|
||||
}
|
||||
];
|
||||
|
||||
let mockServices = class {
|
||||
get(url) {
|
||||
let services = new Services(mockOptions);
|
||||
let items = services.getItems(rawData);
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(items);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
rewire.__Rewire__("Services", mockServices);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (stub) stub.restore();
|
||||
});
|
||||
|
||||
it("loads correctly, shows alert", () => {
|
||||
wrapper = mount(DataDialog, {
|
||||
propsData: mockOptions
|
||||
});
|
||||
emitted = wrapper.emitted();
|
||||
expect(wrapper.classes()).contain("data-dialog-modal");
|
||||
expect(wrapper.find(".fa-spinner").text()).to.equals("");
|
||||
expect(wrapper.find(".btn-secondary").text()).to.equals("Clear");
|
||||
expect(wrapper.find(".btn-primary").text()).to.equals("Ok");
|
||||
expect(wrapper.contains(".fa-spinner")).to.equals(true);
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(wrapper.findAll(".fa-copy").length).to.equals(2);
|
||||
expect(wrapper.findAll(".fa-file-o").length).to.equals(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("loads correctly, shows datasets and folders", () => {
|
||||
wrapper = mount(DataDialog, {
|
||||
propsData: mockOptions
|
||||
});
|
||||
emitted = wrapper.emitted();
|
||||
expect(wrapper.classes()).contain("data-dialog-modal");
|
||||
expect(wrapper.find(".fa-spinner").text()).to.equals("");
|
||||
expect(wrapper.find(".btn-secondary").text()).to.equals("Clear");
|
||||
expect(wrapper.find(".btn-primary").text()).to.equals("Ok");
|
||||
expect(wrapper.contains(".fa-spinner")).to.equals(true);
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(wrapper.findAll(".fa-copy").length).to.equals(2);
|
||||
expect(wrapper.findAll(".fa-file-o").length).to.equals(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<b-modal class="data-dialog-modal" v-if="modalShow" visible ok-only ok-title="Close">
|
||||
<template slot="modal-header">
|
||||
<data-dialog-search v-model="filter" />
|
||||
</template>
|
||||
<b-alert v-if="errorMessage" variant="danger" show v-html="errorMessage" />
|
||||
<div v-else>
|
||||
<data-dialog-table
|
||||
v-if="optionsShow"
|
||||
:items="items"
|
||||
:multiple="multiple"
|
||||
:filter="filter"
|
||||
@clicked="clicked"
|
||||
@load="load"
|
||||
/>
|
||||
<div v-else><span class="fa fa-spinner fa-spin" /> <span>Please wait...</span></div>
|
||||
</div>
|
||||
<div v-if="!errorMessage" slot="modal-footer" class="w-100">
|
||||
<b-btn size="sm" class="float-left" v-if="undoShow" @click="load()">
|
||||
<div class="fa fa-caret-left mr-1" />
|
||||
Back
|
||||
</b-btn>
|
||||
<b-btn size="sm" class="float-right ml-1" variant="primary" @click="finalize" :disabled="!hasValue">
|
||||
Ok
|
||||
</b-btn>
|
||||
<b-btn size="sm" class="float-right" @click="modalShow = false"> Cancel </b-btn>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import DataDialogSearch from "./DataDialogSearch.vue";
|
||||
import DataDialogTable from "./DataDialogTable.vue";
|
||||
import { UrlTracker } from "./utilities.js";
|
||||
import { Model } from "./model.js";
|
||||
import { Services } from "./services.js";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"data-dialog-search": DataDialogSearch,
|
||||
"data-dialog-table": DataDialogTable
|
||||
},
|
||||
props: {
|
||||
callback: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: "download"
|
||||
},
|
||||
library: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
root: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
host: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
history: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: null,
|
||||
filter: null,
|
||||
items: [],
|
||||
modalShow: true,
|
||||
optionsShow: false,
|
||||
undoShow: false,
|
||||
hasValue: false,
|
||||
oldnewItems: false
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.services = new Services({ root: this.root, host: this.host });
|
||||
this.urlTracker = new UrlTracker(this.getHistoryUrl());
|
||||
this.model = new Model({ multiple: this.multiple, format: this.format });
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
/** Returns the default url i.e. the url of the current history **/
|
||||
getHistoryUrl: function() {
|
||||
return `${this.root}api/histories/${this.history}/contents?deleted=false`;
|
||||
},
|
||||
/** Add highlighting for record variations, i.e. datasets vs. libraries/collections **/
|
||||
formatRows() {
|
||||
for (let item of this.items) {
|
||||
let _rowVariant = "active";
|
||||
if (item.isDataset) {
|
||||
_rowVariant = this.model.exists(item.id) ? "success" : "default";
|
||||
}
|
||||
Vue.set(item, "_rowVariant", _rowVariant);
|
||||
}
|
||||
},
|
||||
/** Collects selected datasets in value array **/
|
||||
clicked: function(record) {
|
||||
if (record.isDataset) {
|
||||
this.model.add(record);
|
||||
this.hasValue = this.model.count() > 0;
|
||||
if (this.multiple) {
|
||||
this.formatRows();
|
||||
} else {
|
||||
this.finalize();
|
||||
}
|
||||
}
|
||||
},
|
||||
/** Called when selection is complete, values are formatted and parsed to external callback **/
|
||||
finalize: function() {
|
||||
let results = this.model.finalize();
|
||||
this.modalShow = false;
|
||||
this.callback(results);
|
||||
},
|
||||
/** Performs server request to retrieve data records **/
|
||||
load: function(url) {
|
||||
url = this.urlTracker.getUrl(url);
|
||||
this.filter = null;
|
||||
this.optionsShow = false;
|
||||
this.undoShow = !this.urlTracker.atRoot();
|
||||
this.services
|
||||
.get(url)
|
||||
.then(items => {
|
||||
if (this.library && this.urlTracker.atRoot()) {
|
||||
items.unshift({
|
||||
name: "Data Libraries",
|
||||
url: `${this.root}api/libraries`
|
||||
});
|
||||
}
|
||||
this.items = items;
|
||||
this.formatRows();
|
||||
this.optionsShow = true;
|
||||
})
|
||||
.catch(errorMessage => {
|
||||
this.errorMessage = errorMessage;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.data-dialog-modal .modal-body {
|
||||
max-height: 50vh;
|
||||
height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<b-input-group>
|
||||
<b-input v-model="filter" placeholder="Type to Search" />
|
||||
<b-input-group-append>
|
||||
<b-btn :disabled="!filter" @click="filter = ''">Clear</b-btn>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: ["value"],
|
||||
computed: {
|
||||
filter: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-table
|
||||
small
|
||||
hover
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
:filter="filter"
|
||||
:per-page="perPage"
|
||||
:current-page="currentPage"
|
||||
@row-clicked="clicked"
|
||||
@filtered="filtered"
|
||||
>
|
||||
<template slot="name" slot-scope="data">
|
||||
<i v-if="data.item.isDataset" class="fa fa-file-o" /> <i v-else class="fa fa-copy" />
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="details" slot-scope="data">
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="time" slot-scope="data">
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="arrow" slot-scope="data">
|
||||
<b-button
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="py-0"
|
||||
v-if="!data.item.isDataset"
|
||||
@click.stop="load(data.item.url)"
|
||||
>
|
||||
View
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
<div v-if="nItems === 0">
|
||||
<div v-if="filter">
|
||||
No search results found for: <b>{{ this.filter }}</b
|
||||
>.
|
||||
</div>
|
||||
<div v-else>No entries.</div>
|
||||
</div>
|
||||
<b-pagination v-if="nItems > perPage" v-model="currentPage" :per-page="perPage" :total-rows="nItems" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
filter: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1,
|
||||
fields: {
|
||||
name: {
|
||||
sortable: true
|
||||
},
|
||||
details: {
|
||||
sortable: true
|
||||
},
|
||||
time: {
|
||||
sortable: true
|
||||
},
|
||||
arrow: {
|
||||
label: "",
|
||||
sortable: false,
|
||||
class: "text-right"
|
||||
}
|
||||
},
|
||||
nItems: 0,
|
||||
perPage: 100
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
items: {
|
||||
immediate: true,
|
||||
handler(items) {
|
||||
this.filtered(items);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** Resets pagination when a filter/search word is entered **/
|
||||
filtered: function(items) {
|
||||
this.nItems = items.length;
|
||||
this.currentPage = 1;
|
||||
},
|
||||
/** Collects selected datasets in value array **/
|
||||
clicked: function(record) {
|
||||
this.$emit("clicked", record);
|
||||
},
|
||||
/** Performs server request to retrieve data records **/
|
||||
load: function(url) {
|
||||
this.$emit("load", url);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
export class Model {
|
||||
constructor(options = {}) {
|
||||
this.values = {};
|
||||
this.multiple = options.multiple || false;
|
||||
this.format = options.format || null;
|
||||
}
|
||||
|
||||
/** Adds a new record to the value stack **/
|
||||
add(record) {
|
||||
if (!this.multiple) {
|
||||
this.values = {};
|
||||
}
|
||||
let key = record && record.id;
|
||||
if (key) {
|
||||
if (!this.values[key]) {
|
||||
this.values[key] = record;
|
||||
} else {
|
||||
delete this.values[key];
|
||||
}
|
||||
} else {
|
||||
throw "Invalid record with no <id>.";
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the number of added records **/
|
||||
count() {
|
||||
return Object.keys(this.values).length;
|
||||
}
|
||||
|
||||
/** Returns true if a record is available for a given key **/
|
||||
exists(key) {
|
||||
return !!this.values[key];
|
||||
}
|
||||
|
||||
/** Finalizes the results from added records **/
|
||||
finalize() {
|
||||
let results = [];
|
||||
Object.values(this.values).forEach(v => {
|
||||
let value = null;
|
||||
if (this.format) {
|
||||
value = v[this.format];
|
||||
} else {
|
||||
value = v;
|
||||
}
|
||||
results.push(value);
|
||||
});
|
||||
if (results.length > 0 && !this.multiple) {
|
||||
results = results[0];
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import axios from "axios";
|
||||
|
||||
/** Data populator traverses raw server responses **/
|
||||
export class Services {
|
||||
constructor(options = {}) {
|
||||
this.root = options.root;
|
||||
this.host = options.host;
|
||||
}
|
||||
|
||||
get(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
let items = this.getItems(response.data);
|
||||
resolve(items);
|
||||
})
|
||||
.catch(e => {
|
||||
let errorMessage = "Request failed.";
|
||||
if (e.response) {
|
||||
errorMessage = e.response.data.err_msg || `${e.response.statusText} (${e.response.status})`;
|
||||
}
|
||||
reject(errorMessage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns the formatted results **/
|
||||
getItems(data) {
|
||||
let items = [];
|
||||
let stack = [data];
|
||||
while (stack.length > 0) {
|
||||
let root = stack.pop();
|
||||
if (Array.isArray(root)) {
|
||||
root.forEach(element => {
|
||||
stack.push(element);
|
||||
});
|
||||
} else if (root.elements) {
|
||||
stack.push(root.elements);
|
||||
} else if (root.object) {
|
||||
stack.push(root.object);
|
||||
} else {
|
||||
let record = this.getRecord(root);
|
||||
if (record) {
|
||||
items.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/** Populate record data from raw record source **/
|
||||
getRecord(record) {
|
||||
record.details = record.extension || record.description;
|
||||
record.time = record.update_time || record.create_time;
|
||||
record.isDataset = this.isDataset(record);
|
||||
if (record.time) {
|
||||
record.time = record.time.substring(0, 16).replace("T", " ");
|
||||
}
|
||||
if (record.model_class == "Library") {
|
||||
record.url = `${this.root}api/libraries/${record.id}/contents`;
|
||||
return record;
|
||||
} else if (record.hid) {
|
||||
record.name = `${record.hid}: ${record.name}`;
|
||||
record.download = `${this.host}/api/histories/${record.history_id}/contents/${record.id}/display`;
|
||||
return record;
|
||||
} else if (record.type == "file") {
|
||||
if (record.name && record.name[0] === "/") {
|
||||
record.name = record.name.substring(1);
|
||||
}
|
||||
record.download = `${this.host}${this.root}api/libraries/datasets/download/uncompressed?ld_ids=${
|
||||
record.id
|
||||
}`;
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks if record is a dataset or drillable **/
|
||||
isDataset(record) {
|
||||
return record.history_content_type == "dataset" || record.type == "file";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/** This helps track urls for data drilling **/
|
||||
export class UrlTracker {
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
this.navigation = [];
|
||||
}
|
||||
|
||||
/** Returns urls for data drilling **/
|
||||
getUrl(url) {
|
||||
if (url) {
|
||||
this.navigation.push(url);
|
||||
} else {
|
||||
this.navigation.pop();
|
||||
let navigationLength = this.navigation.length;
|
||||
if (navigationLength > 0) {
|
||||
url = this.navigation[navigationLength - 1];
|
||||
} else {
|
||||
url = this.root;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/** Returns true if the last data is at navigation root **/
|
||||
atRoot() {
|
||||
return this.navigation.length == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<span class="badge badge-tags" :style="tagStyles" @click.prevent="$emit('tag-click', tag)"> {{ tagLabel }} </span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { keyedColorScheme } from "utils/color";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tag: { type: String, required: true }
|
||||
},
|
||||
computed: {
|
||||
tagLabel() {
|
||||
return this.tag.startsWith("name:") ? this.tag.replace("name:", "") : this.tag;
|
||||
},
|
||||
tagStyles() {
|
||||
let { primary, contrasting, darker } = keyedColorScheme(this.tag);
|
||||
return {
|
||||
"background-color": primary,
|
||||
color: contrasting,
|
||||
"border-color": darker
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.badge {
|
||||
padding: 0.2em 0.6em 0.3em;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 0.15rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="nametags" :title="title"><nametag v-for="tag in nameTags" :key="tag" :tag="tag" /></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Nametag from "./Nametag";
|
||||
import { mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Nametag
|
||||
},
|
||||
props: {
|
||||
storeKey: { type: String, required: true },
|
||||
tags: { type: Array, required: false, default: () => [] }
|
||||
},
|
||||
computed: {
|
||||
// only display tags that start with name:
|
||||
nameTags() {
|
||||
return this.$store.getters.getTagsById(this.storeKey).filter(tag => tag.startsWith("name:"));
|
||||
},
|
||||
title() {
|
||||
return `${this.nameTags.length} nametags`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(["updateTags", "initializeTags"])
|
||||
},
|
||||
mounted() {
|
||||
this.initializeTags({ key: this.storeKey, tags: this.tags });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.nametags:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nametags .badge {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
import Nametags from "./Nametags";
|
||||
import { mountVueComponent } from "utils/mountVueComponent";
|
||||
|
||||
export { default as Nametag } from "./Nametag.vue";
|
||||
export { default as Nametags } from "./Nametags.vue";
|
||||
export const mountNametags = mountVueComponent(Nametags);
|
||||
@@ -576,52 +576,13 @@ import JobStatesModel from "mvc/history/job-states-model";
|
||||
import RuleDefs from "mvc/rules/rule-definitions";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import Select2 from "components/Select2";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
const RULES = RuleDefs.RULES;
|
||||
const MAPPING_TARGETS = RuleDefs.MAPPING_TARGETS;
|
||||
|
||||
// Local components...
|
||||
|
||||
// Based on https://vuejs.org/v2/examples/select2.html but adapted to handle list values
|
||||
// with "multiple: true" set.
|
||||
const Select2 = {
|
||||
props: ["options", "value", "placeholder"],
|
||||
template: `<select>
|
||||
<slot></slot>
|
||||
</select>`,
|
||||
mounted: function() {
|
||||
var vm = this;
|
||||
$(this.$el)
|
||||
// init select2
|
||||
.select2({ data: this.options, placeholder: this.placeholder, allowClear: this.placeholder })
|
||||
.val(this.value)
|
||||
.trigger("change")
|
||||
// emit event on change.
|
||||
.on("change", function(event) {
|
||||
vm.$emit("input", event.val);
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
value: function(value) {
|
||||
// update value
|
||||
$(this.$el).val(value);
|
||||
},
|
||||
options: function(options) {
|
||||
// update options
|
||||
$(this.$el)
|
||||
.empty()
|
||||
.select2({ data: options });
|
||||
}
|
||||
},
|
||||
destroyed: function() {
|
||||
$(this.$el)
|
||||
.off()
|
||||
.select2("destroy");
|
||||
}
|
||||
};
|
||||
|
||||
const ColumnSelector = {
|
||||
template: `
|
||||
<div class="rule-column-selector" v-if="!multiple || !ordered">
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Based on https://vuejs.org/v2/examples/select2.html but adapted to handle list values
|
||||
// with "multiple: true" set.
|
||||
import $ from "jquery";
|
||||
|
||||
export default {
|
||||
props: ["options", "value", "placeholder"],
|
||||
template: `<select><slot></slot></select>`,
|
||||
mounted: function() {
|
||||
var vm = this;
|
||||
$(this.$el)
|
||||
// init select2
|
||||
.select2({ data: this.options, placeholder: this.placeholder, allowClear: this.placeholder })
|
||||
.val(this.value)
|
||||
.trigger("change")
|
||||
// emit event on change.
|
||||
.on("change", function(event) {
|
||||
vm.$emit("input", event.val);
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
value: function(value) {
|
||||
// update value
|
||||
$(this.$el).val(value);
|
||||
},
|
||||
options: function(options) {
|
||||
// update options
|
||||
$(this.$el)
|
||||
.empty()
|
||||
.select2({ data: options });
|
||||
}
|
||||
},
|
||||
destroyed: function() {
|
||||
$(this.$el)
|
||||
.off()
|
||||
.select2("destroy");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { mount, createLocalVue } from "@vue/test-utils";
|
||||
import StatelessTags from "./StatelessTags";
|
||||
import _l from "utils/localization";
|
||||
import Vue from "vue";
|
||||
|
||||
describe("Tags/StatelessTags.vue", () => {
|
||||
const localVue = createLocalVue();
|
||||
localVue.filter("localize", value => _l(value));
|
||||
|
||||
let testTags = ["abc", "def", "ghi"];
|
||||
let wrapper, emitted;
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(StatelessTags, { localVue });
|
||||
wrapper.setProps({
|
||||
value: testTags
|
||||
});
|
||||
emitted = wrapper.emitted();
|
||||
await Vue.nextTick();
|
||||
});
|
||||
|
||||
it("should render a div for each tag", () => {
|
||||
let tags = wrapper.findAll(".ti-tag-center");
|
||||
assert(tags.length == testTags.length, "Wrong number of tags");
|
||||
for (let i = 0; i < testTags.length; i++) {
|
||||
assert(tags.at(i).is("div"), "button not a div");
|
||||
assert(tags.at(i).text() == testTags[i], "rendered tag label doesn't match test data");
|
||||
}
|
||||
});
|
||||
|
||||
it("should emit a click event when the tag is clicked", () => {
|
||||
let tags = wrapper.findAll(".ti-tag-center > div");
|
||||
tags.at(0).trigger("click");
|
||||
assert(emitted["tag-click"], "click event not detected");
|
||||
assert(emitted["tag-click"].length == 1, "wrong event count");
|
||||
});
|
||||
|
||||
it("should emit a tag model payload when tag is clicked", () => {
|
||||
let tags = wrapper.findAll(".ti-tag-center > div");
|
||||
tags.at(0).trigger("click");
|
||||
let firstEvent = emitted["tag-click"][0];
|
||||
let firstArg = firstEvent[0];
|
||||
assert((firstArg.text = testTags[0]), "returned tag model doesn't match test data");
|
||||
});
|
||||
|
||||
it("should change internal model representation when new tag list assigned", async () => {
|
||||
assert(wrapper.vm.tagModels.length == 3);
|
||||
let newTags = ["floob", "clown", "hoohah", "doodoo"];
|
||||
wrapper.setProps({ value: newTags });
|
||||
assert(wrapper.vm.tagModels.length == newTags.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
<!-- This is intended to be a stateless UI-only component. All data storage and
|
||||
retrieval as well as and specific event handling should be managed from an
|
||||
upstream component or environment that is accessed through props and events -->
|
||||
|
||||
<template>
|
||||
<div class="tags-display" :class="tagContainerClasses">
|
||||
<a v-if="linkVisible" href="#" class="toggle-link" @click.prevent="toggleTagDisplay">
|
||||
{{ linkText | localize }}
|
||||
</a>
|
||||
<vue-tags-input
|
||||
v-if="tagsVisible"
|
||||
class="tags-input tag-area"
|
||||
v-model="tagText"
|
||||
:tags="tagModels"
|
||||
:autocomplete-items="autocompleteTags"
|
||||
:disabled="disabled"
|
||||
:placeholder="'Add Tags' | localize"
|
||||
:add-on-key="triggerKeys"
|
||||
@before-adding-tag="beforeAddingTag"
|
||||
@before-deleting-tag="beforeDeletingTag"
|
||||
@tags-changed="tagsChanged"
|
||||
>
|
||||
<template slot="tag-center" slot-scope="t">
|
||||
<div class="tag-name" @click="$emit('tag-click', t.tag)">{{ t.tag.label }}</div>
|
||||
</template>
|
||||
</vue-tags-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueTagsInput from "@johmun/vue-tags-input";
|
||||
import { createTag } from "./model";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VueTagsInput
|
||||
},
|
||||
props: {
|
||||
value: { type: Array, required: false, default: () => [] },
|
||||
autocompleteItems: { type: Array, required: false, default: () => [] },
|
||||
maxVisibleTags: { type: Number, required: false, default: 5 },
|
||||
useToggleLink: { type: Boolean, required: false, default: true },
|
||||
disabled: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
data() {
|
||||
// initialize toggle value
|
||||
let isClosed = this.useToggleLink && this.value.length > this.maxVisibleTags;
|
||||
|
||||
return {
|
||||
tagText: "",
|
||||
tagToggle: !isClosed,
|
||||
triggerKeys: [13, " "]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tagContainerClasses() {
|
||||
return {
|
||||
disabled: this.disabled
|
||||
};
|
||||
},
|
||||
tagModels() {
|
||||
return this.value.map(createTag);
|
||||
},
|
||||
autocompleteTags() {
|
||||
return this.autocompleteItems.map(createTag);
|
||||
},
|
||||
linkText() {
|
||||
return `${this.tagModels.length} Tags`;
|
||||
},
|
||||
linkVisible() {
|
||||
return this.useToggleLink && this.tagModels.length > this.maxVisibleTags;
|
||||
},
|
||||
tagsVisible() {
|
||||
return this.useToggleLink ? this.tagToggle : true;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tagText(newValue) {
|
||||
this.$emit("tag-input-changed", newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tagsChanged(newTags) {
|
||||
this.$emit("input", this.pluckLabels(newTags));
|
||||
},
|
||||
pluckLabels(newTags) {
|
||||
return newTags.map(t => createTag(t).toString());
|
||||
},
|
||||
toggleTagDisplay() {
|
||||
this.tagToggle = !this.tagToggle;
|
||||
this.$emit("show", this.tagToggle);
|
||||
},
|
||||
beforeAddingTag($event) {
|
||||
if (!this.emitHookEvent("before-adding-tag", $event)) {
|
||||
let { tag, addTag } = $event;
|
||||
addTag(tag);
|
||||
}
|
||||
},
|
||||
beforeDeletingTag($event) {
|
||||
if (!this.emitHookEvent("before-deleting-tag", $event)) {
|
||||
let { tag, deleteTag } = $event;
|
||||
deleteTag(tag);
|
||||
}
|
||||
},
|
||||
emitHookEvent(eventName, $event) {
|
||||
if (this.hasHandler(eventName)) {
|
||||
this.$emit(eventName, $event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hasHandler(eventName) {
|
||||
return Object.keys(this.$listeners).includes(eventName);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// Most styling of the tags should happen in here.
|
||||
|
||||
@import "theme/blue";
|
||||
@import "scss/mixins";
|
||||
|
||||
// Puts a little graphic in place of the text-input
|
||||
// when the input is not in focus
|
||||
@mixin newTagHoverButton() {
|
||||
.vue-tags-input .ti-tags .ti-new-tag-input-wrapper {
|
||||
input {
|
||||
background-color: transparent;
|
||||
}
|
||||
input:not(:focus) {
|
||||
background: url("/static/images/fugue/tag--plus.png");
|
||||
background-repeat: no-repeat;
|
||||
color: transparent;
|
||||
&::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// general style butchering
|
||||
@mixin matchBootstrapStyling() {
|
||||
// TODO: actually match the bootstrap button classes in
|
||||
// here either by importing mixins or just using colors
|
||||
// from the boostrap .scss files
|
||||
.vue-tags-input {
|
||||
@include fill();
|
||||
.ti-input {
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
.ti-tag {
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
&.tag-area {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version of the tags that only allow clicking
|
||||
// existing tags instead of the full editing UI
|
||||
@mixin forDisplayOnly() {
|
||||
.vue-tags-input {
|
||||
.ti-actions,
|
||||
.ti-new-tag-input-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags-display {
|
||||
// adds in a graphic in place of the text input
|
||||
@include newTagHoverButton();
|
||||
|
||||
// match bootstrap tag styles/colors
|
||||
@include matchBootstrapStyling();
|
||||
|
||||
// display-only tags
|
||||
&.disabled {
|
||||
@include forDisplayOnly();
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
/* global expect */
|
||||
|
||||
import Vuex from "vuex";
|
||||
import { mount, createLocalVue } from "@vue/test-utils";
|
||||
import Tags from "./Tags";
|
||||
import _l from "utils/localization";
|
||||
import store from "../../store";
|
||||
import { TagService } from "./tagService";
|
||||
import { createTag } from "./model";
|
||||
|
||||
describe("Tags/Tags.vue", () => {
|
||||
const localVue = createLocalVue();
|
||||
localVue.filter("localize", value => _l(value));
|
||||
localVue.use(Vuex);
|
||||
|
||||
let id = "testId",
|
||||
itemClass = "TestObject",
|
||||
context = "testing",
|
||||
tagService,
|
||||
wrapper,
|
||||
emitted,
|
||||
startingTags = ["a", "b", "c"],
|
||||
storeKey = "testingTagSet";
|
||||
|
||||
let clickFirstTag = () => {
|
||||
if (wrapper) {
|
||||
let firstTag = wrapper.find(".ti-tag-center > div");
|
||||
firstTag.trigger("click");
|
||||
} else {
|
||||
console.log("missing tag");
|
||||
}
|
||||
};
|
||||
|
||||
// Run bef oreEach as async so the lifecycle methods can run
|
||||
beforeEach(async () => {
|
||||
// TODO: this mocking mechanism is no good.
|
||||
|
||||
tagService = new TagService({ id, itemClass, context });
|
||||
|
||||
tagService.save = async function(tag) {
|
||||
return createTag(tag);
|
||||
};
|
||||
|
||||
tagService.delete = async function(tag) {
|
||||
return createTag(tag);
|
||||
};
|
||||
|
||||
tagService.autocomplete = async function(txt) {
|
||||
return [txt].map(createTag);
|
||||
};
|
||||
|
||||
// Mount the tags with sample props
|
||||
|
||||
wrapper = mount(Tags, {
|
||||
store,
|
||||
localVue,
|
||||
propsData: {
|
||||
tags: startingTags,
|
||||
storeKey,
|
||||
tagService
|
||||
}
|
||||
});
|
||||
|
||||
emitted = wrapper.emitted();
|
||||
|
||||
// Waits for lifecycle handlers to execute
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tagService = null;
|
||||
});
|
||||
|
||||
it("should display the tags I give it", async () => {
|
||||
let tags = wrapper.findAll(".ti-tag-center .tag-name");
|
||||
expect(tags.length).to.equal(3);
|
||||
expect(tags.at(0).text()).to.equal(startingTags[0]);
|
||||
});
|
||||
|
||||
it("should put the initialized tags into the store at initialization", async () => {
|
||||
let storedTags = store.getters.getTagsById(storeKey);
|
||||
expect(storedTags.length).to.equal(3);
|
||||
startingTags.forEach((tag, i) => {
|
||||
expect(tag).to.equal(storedTags[i]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should respond to click events on the tags", async () => {
|
||||
clickFirstTag();
|
||||
assert(emitted["tag-click"], "click event not detected");
|
||||
assert(emitted["tag-click"].length == 1, "wrong event count");
|
||||
});
|
||||
|
||||
it("should reflect changes in the store", async () => {
|
||||
let newTags = ["asdfadsadf", "gfhjfghjf"];
|
||||
store.dispatch("updateTags", { key: storeKey, tags: newTags });
|
||||
|
||||
// TODO: figure out how to make the computed observableTags
|
||||
// prop update when the store does. This works in the real code,
|
||||
// but does not update in this test environment. The following
|
||||
// brute force mechanism of changing a different dependency works
|
||||
// and effectively recalculates the computed value, but it should
|
||||
// not be necessary
|
||||
wrapper.setProps({ storeKey: "thisshouldbeunnecessary" });
|
||||
wrapper.setProps({ storeKey });
|
||||
|
||||
let observed = wrapper.vm.observedTags;
|
||||
expect(observed.length).to.equal(newTags.length);
|
||||
observed.forEach((tagLabel, i) => {
|
||||
expect(newTags[i]).to.equal(tagLabel);
|
||||
});
|
||||
});
|
||||
|
||||
describe("autocomplete list", () => {
|
||||
let subscription;
|
||||
|
||||
afterEach(() => {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
it("should generate autocomplete items when you set the search text", done => {
|
||||
let sampleTxt = "floobar";
|
||||
subscription = tagService.autocompleteOptions.subscribe(results => {
|
||||
expect(results.length).equal(1);
|
||||
results.forEach(r => {
|
||||
expect(r.text).equal(sampleTxt);
|
||||
});
|
||||
done();
|
||||
});
|
||||
tagService.autocompleteSearchText = sampleTxt;
|
||||
});
|
||||
|
||||
it("should debounce autocomplete requests", done => {
|
||||
let sampleTxt = "floobar";
|
||||
subscription = tagService.autocompleteOptions.subscribe(results => {
|
||||
expect(results.length).equal(1);
|
||||
results.forEach(r => {
|
||||
expect(r.text).equal(sampleTxt);
|
||||
});
|
||||
done();
|
||||
});
|
||||
tagService.autocompleteSearchText = "foo";
|
||||
tagService.autocompleteSearchText = sampleTxt;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<stateless-tags
|
||||
v-model="observedTags"
|
||||
:disabled="disabled"
|
||||
:autocomplete-items="autocompleteItems"
|
||||
@tag-click="onClick"
|
||||
@tag-input-changed="updateTagSearch"
|
||||
@before-adding-tag="beforeAddingTag"
|
||||
@before-deleting-tag="beforeDeletingTag"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import VueRx from "vue-rx";
|
||||
import { mapActions } from "vuex";
|
||||
import { map } from "rxjs/operators";
|
||||
import StatelessTags from "./StatelessTags";
|
||||
import { diffTags } from "./model";
|
||||
import { TagService } from "./tagService";
|
||||
|
||||
Vue.use(VueRx);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatelessTags
|
||||
},
|
||||
|
||||
props: {
|
||||
// initialization value
|
||||
tags: { type: Array, required: false, default: () => [] },
|
||||
|
||||
// data requests go through this object
|
||||
tagService: { type: TagService, required: true },
|
||||
|
||||
// store key, usually a model ID or something like that
|
||||
storeKey: { type: String, required: true },
|
||||
|
||||
// allows user to edit tag list
|
||||
disabled: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
|
||||
computed: {
|
||||
observedTags: {
|
||||
get() {
|
||||
return this.$store.getters.getTagsById(this.storeKey);
|
||||
},
|
||||
set(tags) {
|
||||
this.updateTags({ key: this.storeKey, tags });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
subscriptions() {
|
||||
return {
|
||||
autocompleteItems: this.tagService.autocompleteOptions.pipe(
|
||||
// without the ones we've already selected
|
||||
map(resultTags => diffTags(resultTags, this.tags))
|
||||
)
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick(tag) {
|
||||
this.$emit("tag-click", tag);
|
||||
},
|
||||
|
||||
beforeAddingTag({ tag, addTag }) {
|
||||
this.tagService
|
||||
.save(tag)
|
||||
.then(() => addTag(tag))
|
||||
.catch(err => console.warn("Unable to save tag", err));
|
||||
},
|
||||
|
||||
beforeDeletingTag({ tag, deleteTag }) {
|
||||
this.tagService
|
||||
.delete(tag)
|
||||
.then(() => deleteTag(tag))
|
||||
.catch(err => console.warn("Unable to delete tag", err));
|
||||
},
|
||||
|
||||
// Set search value on tag service input proprety and eventually search
|
||||
// results will appear on the tagService.autocompleteOptions observable
|
||||
// object which is subscribed to above
|
||||
|
||||
updateTagSearch(searchTxt) {
|
||||
this.tagService.autocompleteSearchText = searchTxt;
|
||||
},
|
||||
|
||||
...mapActions(["updateTags", "initializeTags"])
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initializeTags({ key: this.storeKey, tags: this.tags });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createTag } from "./model";
|
||||
import { TagService } from "./tagService";
|
||||
|
||||
// Some parts of the application save tags by saving model so that
|
||||
// it will update other parts of the app, which is a rudimentary way
|
||||
// of recreating reactivity, so I expect this will probably die soon
|
||||
|
||||
export class BackboneTagService extends TagService {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.model = props.model;
|
||||
}
|
||||
|
||||
async save(rawTag) {
|
||||
let tag = createTag(rawTag);
|
||||
if (!tag.valid) {
|
||||
throw new Error("Invalid tag");
|
||||
}
|
||||
|
||||
// update model
|
||||
let tags = new Set(this.model.attributes.tags);
|
||||
tags.add(tag.text);
|
||||
tags = Array.from(tags);
|
||||
|
||||
await this.model.save({ tags });
|
||||
return tag;
|
||||
}
|
||||
|
||||
async delete(rawTag) {
|
||||
let tag = createTag(rawTag);
|
||||
|
||||
let tags = new Set(this.model.attributes.tags);
|
||||
tags.delete(tag.text);
|
||||
tags = Array.from(tags);
|
||||
|
||||
await this.model.save({ tags });
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Low level stateless UI component
|
||||
export { default as StatelessTags } from "./StatelessTags";
|
||||
|
||||
// Implements data storage logic and click event handling
|
||||
// This is usually what you'll want to use.
|
||||
export { default as Tags } from "./Tags";
|
||||
|
||||
// functions for mounting the tag editor in non-Vue environments
|
||||
export { mountMakoTags, mountModelTags } from "./mounts";
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* The tag model is pretty simple, so this extra file might be overkill, but
|
||||
* it's good practice to separate data modeling from data retrieval
|
||||
*/
|
||||
|
||||
import { keyedColorScheme } from "utils/color";
|
||||
|
||||
function TagModel(props = {}) {
|
||||
this.text = "";
|
||||
|
||||
// special handling for name:thing tags
|
||||
if (props.text && props.text.startsWith("#")) {
|
||||
props.text = props.text.replace("#", "name:");
|
||||
}
|
||||
|
||||
Object.assign(this, props);
|
||||
|
||||
// Need to do Object.defineProperty instead of a class getter to make
|
||||
// style enumerable for vue-tags-input
|
||||
Object.defineProperty(this, "style", {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
if (this.text.startsWith("name:")) {
|
||||
let { primary, contrasting, darker } = keyedColorScheme(this.text);
|
||||
|
||||
let styles = {
|
||||
"background-color": primary,
|
||||
color: contrasting,
|
||||
"border-color": darker
|
||||
};
|
||||
|
||||
return Object.keys(styles)
|
||||
.map(prop => `${prop}: ${styles[prop]}`)
|
||||
.join(";");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
// Changes name:foo to #foo
|
||||
Object.defineProperty(this, "label", {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return this.text.startsWith("name:") ? this.text.replace("name:", "#") : this.text;
|
||||
}
|
||||
});
|
||||
|
||||
// valid flag
|
||||
Object.defineProperty(this, "valid", {
|
||||
enumerable: false,
|
||||
get: function() {
|
||||
if (!this.text.length) return false;
|
||||
if (this.text == "name:") return false;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TagModel.prototype.equals = function(otherTag) {
|
||||
return this.text == otherTag.text;
|
||||
};
|
||||
|
||||
TagModel.prototype.toString = function() {
|
||||
return this.text;
|
||||
};
|
||||
|
||||
// Public factory
|
||||
|
||||
export function createTag(data) {
|
||||
let props = {};
|
||||
switch (typeof data) {
|
||||
case "string":
|
||||
props = { text: data };
|
||||
break;
|
||||
case "object":
|
||||
props = Object.assign({}, data);
|
||||
break;
|
||||
}
|
||||
return new TagModel(props);
|
||||
}
|
||||
|
||||
// Returns tags in "newTags" that aren't present in "existingTags"
|
||||
|
||||
export const diffTags = (newTags, existingTags) => {
|
||||
let newModels = newTags.map(createTag);
|
||||
let existingModels = existingTags.map(createTag);
|
||||
return newModels.filter(tag => !existingModels.some(st => st.equals(tag)));
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
/* global expect */
|
||||
import { createTag, diffTags } from "./model";
|
||||
|
||||
describe("Tags/model.js", () => {
|
||||
// Basic props
|
||||
|
||||
describe("tag model", () => {
|
||||
it("should have a string representation equal to text prop", () => {
|
||||
let testLabel = "abc";
|
||||
let model = createTag(testLabel);
|
||||
assert.equal(model, testLabel);
|
||||
assert.equal(model.text, testLabel);
|
||||
assert.equal(model.toString(), testLabel);
|
||||
});
|
||||
});
|
||||
|
||||
// Factory Function
|
||||
|
||||
describe("createTag", () => {
|
||||
it("should build a model from a string", () => {
|
||||
let label = "floob";
|
||||
let model = createTag(label);
|
||||
expect(model.text).to.equal(label);
|
||||
});
|
||||
|
||||
it("should build a model from an object", () => {
|
||||
let data = { text: "floob" };
|
||||
let model = createTag(data);
|
||||
expect(model.text).to.equal(data.text);
|
||||
});
|
||||
});
|
||||
|
||||
// Filtered select function, currently used in component to remove
|
||||
// selected items from a list of returned autocomplete options
|
||||
|
||||
describe("diffTags", () => {
|
||||
let source, selected;
|
||||
|
||||
beforeEach(() => {
|
||||
source = ["a", "b", "c", "d"].map(createTag);
|
||||
selected = ["a", "d", "f"].map(createTag);
|
||||
});
|
||||
|
||||
it("should remove duplicates from a passed array", () => {
|
||||
let result = diffTags(source, selected);
|
||||
expect(result.length).to.equal(2);
|
||||
assert(result[0].equals(source[1]), true);
|
||||
assert(result[0].equals(createTag("b")), true);
|
||||
assert(result[1].equals(source[2]), true);
|
||||
assert(result[1].equals(createTag("c")), true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handles name tags", () => {
|
||||
it("should accept a #label", () => {
|
||||
let testLabel = "#abc";
|
||||
let expectedLabel = "name:abc";
|
||||
let model = createTag(testLabel);
|
||||
assert.equal(model, expectedLabel);
|
||||
assert.equal(model.text, expectedLabel);
|
||||
assert.equal(model.toString(), expectedLabel);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* These functions are for mounting the tag display/editor in non-Vue
|
||||
* environments such as the existing python scripts and Backbone views.
|
||||
*/
|
||||
|
||||
import Tags from "./Tags";
|
||||
import { mountVueComponent } from "utils/mountVueComponent";
|
||||
import { redirectToUrl } from "utils/redirect";
|
||||
import { TagService } from "./tagService";
|
||||
import { BackboneTagService } from "./backboneTagService";
|
||||
|
||||
/**
|
||||
* General mount function for the tags that were previously rendered
|
||||
* by the tagging_common.mako file
|
||||
*/
|
||||
export const mountMakoTags = (options = {}, el) => {
|
||||
let { id, itemClass, tags = [], disabled = false, context = "unspecified" } = options;
|
||||
|
||||
let propData = {
|
||||
storeKey: `${itemClass}-${id}`,
|
||||
tagService: new TagService({ id, itemClass, context }),
|
||||
tags,
|
||||
disabled
|
||||
};
|
||||
|
||||
let fn = mountVueComponent(Tags);
|
||||
let vm = fn(propData, el);
|
||||
vm.$on("tag-click", makoClickHandler(options, vm));
|
||||
return vm;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a click handler for the tags
|
||||
*
|
||||
* @param {object} options Passed options from mount fn
|
||||
*/
|
||||
const makoClickHandler = (options, vm) =>
|
||||
function(tag) {
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { tagClickFn = "none", clickUrl } = options;
|
||||
|
||||
switch (tagClickFn) {
|
||||
// I made this match the existing behavior, but I am not clear on
|
||||
// the reason why this link redirects to a raw json page
|
||||
case "community_tag_click":
|
||||
if (undefined !== clickUrl) {
|
||||
let suffix = tag.value ? `:${tag.value}` : "";
|
||||
let href = `${clickUrl}?f-tags=${tag.text}${suffix}`;
|
||||
redirectToUrl(href);
|
||||
}
|
||||
break;
|
||||
|
||||
case "add_tag_to_grid_filter":
|
||||
vm.$store.dispatch("toggleSearchTag", tag);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mount function when a backbone model is provided.
|
||||
*/
|
||||
export const mountModelTags = (options = {}, el) => {
|
||||
let { model, disabled = false, context = "unspecified" } = options;
|
||||
|
||||
if (!model) {
|
||||
console.warn("Missing model in mountModelTags");
|
||||
return;
|
||||
}
|
||||
|
||||
let { id, model_class: itemClass, tags = [] } = model.attributes;
|
||||
|
||||
let propData = {
|
||||
storeKey: `${itemClass}-${id}`,
|
||||
tagService: new BackboneTagService({ id, itemClass, context, model }),
|
||||
tags,
|
||||
disabled
|
||||
};
|
||||
|
||||
let fn = mountVueComponent(Tags);
|
||||
return fn(propData, el);
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Generates a service object used by the tagging component to save, delete and
|
||||
* lookup potential tag options for the autocomplete feature. Standard typeahead
|
||||
* debouncing and ajax cancelling functionality is also provided here, though an
|
||||
* argument can be made that it more properly belongs in the component that is
|
||||
* handling the inputs.
|
||||
*
|
||||
* TODO: convert the associated python endpoint to a legit json REST service
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { createTag } from "./model";
|
||||
import { Subject } from "rxjs";
|
||||
import { map, filter, debounceTime, switchMap, distinctUntilChanged } from "rxjs/operators";
|
||||
|
||||
export class TagService {
|
||||
constructor({ id, itemClass, context, debounceInterval = 150 }) {
|
||||
this.id = id;
|
||||
this.itemClass = itemClass;
|
||||
this.context = context;
|
||||
this.debounceInterval = debounceInterval;
|
||||
|
||||
// Buffer for autocomplete text changes
|
||||
this._searchText = new Subject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Autocomplete observable. Subscribe to this object to get updates to
|
||||
* matching autocomplete results as the autocompleteSearchText property is
|
||||
* changed
|
||||
*/
|
||||
get autocompleteOptions() {
|
||||
return this._searchText.pipe(
|
||||
map(txt => txt.replace("name:", "")),
|
||||
filter(txt => txt.length),
|
||||
debounceTime(this.debounceInterval),
|
||||
distinctUntilChanged(),
|
||||
switchMap(txt => this.autocomplete(txt))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this property to start process of retrieving autocomplete results.
|
||||
*/
|
||||
set autocompleteSearchText(txt) {
|
||||
this._searchText.next(txt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tag, input can be text string or tag model
|
||||
* @param {string|Tag} tag
|
||||
* @returns Promise yielding new tag model
|
||||
*/
|
||||
async save(rawTag) {
|
||||
let { id, itemClass, context } = this;
|
||||
let tag = createTag(rawTag);
|
||||
if (!tag.valid) {
|
||||
throw new Error("Invalid tag");
|
||||
}
|
||||
let url = `/tag/add_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&new_tag=${tag.text}`;
|
||||
let response = await axios.get(url);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Unable to save tag: ${tag}`);
|
||||
}
|
||||
return createTag(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete tag, input can be text string or tag model
|
||||
* @param {string|Tag} tag
|
||||
* @returns Promise yielding deleted tag model
|
||||
*/
|
||||
async delete(rawTag) {
|
||||
let { id, itemClass, context } = this;
|
||||
let tag = createTag(rawTag);
|
||||
let url = `/tag/remove_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&tag_name=${tag.text}`;
|
||||
let response = await axios.get(url);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Unable to delete tag: ${tag}`);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up autocomplete options based on search text
|
||||
* @param {string} searchText
|
||||
* @returns Promise yielding an array of tag models
|
||||
*/
|
||||
async autocomplete(searchText) {
|
||||
let { id, itemClass } = this;
|
||||
let url = `/tag/tag_autocomplete_data?item_id=${id}&item_class=${itemClass}&q=${searchText}`;
|
||||
let response = await axios.get(url);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Unable to retrieve autocomplete tags for search string: ${searchText}`);
|
||||
}
|
||||
return parseAutocompleteResults(response.data).map(createTag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function parser for the archaic result format in the current API. See
|
||||
* testData/autocompleteResponse.txt for a sample.
|
||||
* @param {string} rawResponse
|
||||
*/
|
||||
export function parseAutocompleteResults(rawResponse) {
|
||||
return rawResponse
|
||||
.split("\n")
|
||||
.filter(line => line.includes("|"))
|
||||
.map(line => line.split("|")[0])
|
||||
.filter(label => label.length)
|
||||
.filter(label => label !== "#Header");
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/* global expect */
|
||||
import sinon from "sinon";
|
||||
import { TagService, __RewireAPI__ as rewire } from "./tagService";
|
||||
import { createTag } from "./model";
|
||||
import { interval } from "rxjs";
|
||||
import { take, takeUntil } from "rxjs/operators";
|
||||
|
||||
// test response
|
||||
import autocompleteResponse from "./testData/autocompleteResponse.txt";
|
||||
|
||||
describe("Tags/tagService.js", () => {
|
||||
let svcParams = {
|
||||
id: 123,
|
||||
itemClass: "fooClass",
|
||||
context: "",
|
||||
debounceInterval: 50 // shorter value than default for unit tests
|
||||
};
|
||||
|
||||
let svc = new TagService(svcParams);
|
||||
|
||||
let mockAxios = {
|
||||
get: () => null
|
||||
};
|
||||
|
||||
let stub;
|
||||
|
||||
beforeEach(() => {
|
||||
rewire.__Rewire__("axios", mockAxios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (stub) stub.restore();
|
||||
});
|
||||
|
||||
describe("save", () => {
|
||||
let testLabel = "fo0bar123";
|
||||
let testTag = createTag(testLabel);
|
||||
|
||||
let { id, itemClass, context } = svcParams;
|
||||
let expectedSaveUrl = `/tag/add_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&new_tag=${testLabel}`;
|
||||
|
||||
it("should save a string tag", async () => {
|
||||
stub = sinon.stub(mockAxios, "get").resolves({ status: 200 });
|
||||
let savedTag = await svc.save(testLabel);
|
||||
expect(savedTag.text).to.equal(testLabel);
|
||||
assert(stub.calledWith(expectedSaveUrl));
|
||||
});
|
||||
|
||||
it("should save an object tag", async () => {
|
||||
stub = sinon.stub(mockAxios, "get").resolves({ status: 200 });
|
||||
let savedTag = await svc.save(testTag);
|
||||
expect(savedTag.text).to.equal(testLabel);
|
||||
assert(stub.calledWith(expectedSaveUrl));
|
||||
});
|
||||
|
||||
// TODO: test error conditions
|
||||
});
|
||||
|
||||
describe("delete", () => {
|
||||
let testLabel = "fo0bar123";
|
||||
let testTag = createTag(testLabel);
|
||||
|
||||
let { id, itemClass, context } = svcParams;
|
||||
let expectedDeleteUrl = `/tag/remove_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&tag_name=${testLabel}`;
|
||||
|
||||
it("should delete a text tag", async () => {
|
||||
stub = sinon.stub(mockAxios, "get").resolves({ status: 200 });
|
||||
let result = await svc.delete(testTag);
|
||||
assert(result);
|
||||
assert(stub.calledWith(expectedDeleteUrl));
|
||||
});
|
||||
|
||||
it("should delete an object tag", async () => {
|
||||
stub = sinon.stub(mockAxios, "get").resolves({ status: 200 });
|
||||
let result = await svc.delete(testTag);
|
||||
assert(result);
|
||||
assert(stub.calledWith(expectedDeleteUrl));
|
||||
});
|
||||
|
||||
// TODO: test error conditions
|
||||
});
|
||||
|
||||
describe("autocomplete", () => {
|
||||
let searchString = "foo";
|
||||
let { id, itemClass } = svcParams;
|
||||
let expectedSearchUrl = `/tag/tag_autocomplete_data?item_id=${id}&item_class=${itemClass}&q=${searchString}`;
|
||||
|
||||
let successResponse = {
|
||||
status: 200,
|
||||
data: autocompleteResponse
|
||||
};
|
||||
|
||||
let checkAutocompleteResult = result => {
|
||||
assert(result);
|
||||
assert(result instanceof Array);
|
||||
assert(result.length == 2);
|
||||
assert(result[0] instanceof Object);
|
||||
assert((result[0].text = "abc"));
|
||||
};
|
||||
|
||||
// straight ajax request, unused in practice, but it's easier to
|
||||
// test this call if we just expose it
|
||||
it("ajax call should return tag objects", async () => {
|
||||
stub = sinon.stub(mockAxios, "get").resolves(successResponse);
|
||||
let result = await svc.autocomplete(searchString);
|
||||
assert(stub.calledWith(expectedSearchUrl), "Called with wrong search url");
|
||||
checkAutocompleteResult(result);
|
||||
});
|
||||
|
||||
// hit the search input with multiple entries, only one ajax call
|
||||
// should result because of debouncing
|
||||
it("should debounce autocomplete search inputs", done => {
|
||||
let searchResult;
|
||||
|
||||
// ends subscription to observable so test doesn't go on forever
|
||||
let spamCount = Math.floor(Math.random() * 10);
|
||||
let timer = interval(1500).pipe(take(1));
|
||||
|
||||
// stub ajax request to return the success response if the
|
||||
// searchString is the expected input
|
||||
stub = sinon.stub(mockAxios, "get").resolves(successResponse);
|
||||
|
||||
svc.autocompleteOptions.pipe(takeUntil(timer)).subscribe(
|
||||
result => (searchResult = result),
|
||||
err => console.log("error", err),
|
||||
() => {
|
||||
assert(stub.called, "Ajax call not made");
|
||||
assert(stub.callCount == 1, `Wrong number of ajax calls: ${stub.callCount}`);
|
||||
checkAutocompleteResult(searchResult);
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
// spam a bunch of key inputs
|
||||
for (var i = 0; i < spamCount; i++) svc.autocompleteSearchText = new String(Math.random());
|
||||
svc.autocompleteSearchText = searchString;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
#Header|Your Tags
|
||||
abc|abc
|
||||
def|def
|
||||
@@ -24,12 +24,10 @@
|
||||
</b-card-footer>
|
||||
</b-card>
|
||||
</b-form>
|
||||
<b-button v-for="idp in oidc_idps" :key="idp" class="d-block mt-3" @click="submitOIDCLogin(idp)">
|
||||
<i v-bind:class="oidc_idps_icons[idp]" /> Sign in with {{ idp.charAt(0).toUpperCase() + idp.slice(1) }}
|
||||
</b-button>
|
||||
</div>
|
||||
<b-card header="Other Login Options">
|
||||
<b-form id="oidc" @submit.prevent="submitOIDCLogin()">
|
||||
<b-form-group> <b-button type="submit">Log in with Google</b-button> </b-form-group>
|
||||
</b-form>
|
||||
</b-card>
|
||||
<div v-if="show_welcome_with_login" class="col">
|
||||
<b-embed type="iframe" :src="welcome_url" aspect="1by1" />
|
||||
</div>
|
||||
@@ -58,6 +56,13 @@ export default {
|
||||
},
|
||||
data() {
|
||||
let galaxy = getGalaxyInstance();
|
||||
let oidc_idps = galaxy.config.oidc;
|
||||
// Icons to use for each IdP
|
||||
let oidc_idps_icons = {'google': 'fa fa-google'};
|
||||
// Add default icons to IdPs without icons
|
||||
oidc_idps.filter(function(key) { return oidc_idps_icons[key] === undefined; }).forEach(function(idp) {
|
||||
oidc_idps_icons[idp] = 'fa fa-id-card'
|
||||
});
|
||||
return {
|
||||
login: null,
|
||||
password: null,
|
||||
@@ -66,16 +71,16 @@ export default {
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
redirect: galaxy.params.redirect,
|
||||
session_csrf_token: galaxy.session_csrf_token
|
||||
session_csrf_token: galaxy.session_csrf_token,
|
||||
enable_oidc: galaxy.config.enable_oidc,
|
||||
oidc_idps: oidc_idps,
|
||||
oidc_idps_icons: oidc_idps_icons
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageShow() {
|
||||
return this.messageText != null;
|
||||
},
|
||||
showOIDC() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
return Galaxy.config.enable_oidc == true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -106,13 +111,13 @@ export default {
|
||||
this.messageText = message || "Login failed for an unknown reason.";
|
||||
});
|
||||
},
|
||||
submitOIDCLogin: function(method) {
|
||||
submitOIDCLogin: function(idp) {
|
||||
let rootUrl = getAppRoot();
|
||||
axios
|
||||
.post(`${rootUrl}authnz/google/login`)
|
||||
.post(`${rootUrl}authnz/${idp}/login`)
|
||||
.then(response => {
|
||||
if (response.data.redirect_uri) {
|
||||
window.location = encodeURI(response.data.redirect_uri);
|
||||
window.location = response.data.redirect_uri;
|
||||
}
|
||||
// Else do something intelligent or maybe throw an error -- what else does this endpoint possibly return?
|
||||
})
|
||||
|
||||
@@ -172,7 +172,8 @@ export const getAnalysisRouter = Galaxy =>
|
||||
},
|
||||
|
||||
show_histories: function(action_id) {
|
||||
this.page.display(new HistoryList.View({ action_id: action_id }));
|
||||
let view = new HistoryList.View({ action_id: action_id });
|
||||
this.page.display(view);
|
||||
},
|
||||
|
||||
show_history_citations: function() {
|
||||
|
||||
@@ -8,8 +8,8 @@ import CurrentHistoryView from "mvc/history/history-view-edit-current";
|
||||
/** the right hand panel in the analysis page that shows the current history */
|
||||
var HistoryPanel = Backbone.View.extend({
|
||||
initialize: function(page, options) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let self = this;
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const panelHeaderButtons = [];
|
||||
|
||||
this.userIsAnonymous = Galaxy.user.isAnonymous();
|
||||
this.allow_user_dataset_purge = options.config.allow_user_dataset_purge;
|
||||
@@ -32,10 +32,34 @@ var HistoryPanel = Backbone.View.extend({
|
||||
title: _l("Refresh history"),
|
||||
cls: "panel-header-button",
|
||||
icon: "fa fa-refresh",
|
||||
onclick: function() {
|
||||
self.historyView.loadCurrentHistory();
|
||||
onclick: () => {
|
||||
this.historyView.loadCurrentHistory();
|
||||
}
|
||||
});
|
||||
panelHeaderButtons.push(this.buttonRefresh);
|
||||
|
||||
if (!this.userIsAnonymous){
|
||||
this.buttonNew = new Ui.ButtonLink({
|
||||
id: "history-new-button",
|
||||
title: _l("Create new history"),
|
||||
cls: "panel-header-button",
|
||||
icon: "fa fa-plus",
|
||||
onclick: function() {
|
||||
Galaxy.currHistoryPanel.createNewHistory();
|
||||
}
|
||||
});
|
||||
panelHeaderButtons.push(this.buttonNew);
|
||||
}
|
||||
|
||||
this.buttonViewMulti = new Ui.ButtonLink({
|
||||
id: "history-view-multi-button",
|
||||
title: _l("View all histories"),
|
||||
cls: "panel-header-button",
|
||||
icon: "fa fa-columns",
|
||||
href: `${this.root}history/view_multiple`
|
||||
});
|
||||
panelHeaderButtons.push(this.buttonViewMulti);
|
||||
|
||||
this.buttonOptions = new Ui.ButtonLink({
|
||||
id: "history-options-button",
|
||||
title: _l("History options"),
|
||||
@@ -44,19 +68,13 @@ var HistoryPanel = Backbone.View.extend({
|
||||
icon: "fa fa-cog",
|
||||
href: `${this.root}root/history_options`
|
||||
});
|
||||
this.buttonViewMulti = new Ui.ButtonLink({
|
||||
id: "history-view-multi-button",
|
||||
title: _l("View all histories"),
|
||||
cls: "panel-header-button",
|
||||
icon: "fa fa-columns",
|
||||
href: `${this.root}history/view_multiple`
|
||||
});
|
||||
panelHeaderButtons.push(this.buttonOptions);
|
||||
|
||||
// define components
|
||||
this.model = new Backbone.Model({
|
||||
// define components
|
||||
cls: "history-right-panel",
|
||||
title: _l("History"),
|
||||
buttons: [this.buttonRefresh, this.buttonOptions, this.buttonViewMulti]
|
||||
buttons: panelHeaderButtons
|
||||
});
|
||||
|
||||
// build body template and connect history view
|
||||
|
||||
@@ -50,7 +50,9 @@ var ToolPanel = Backbone.View.extend({
|
||||
title: _l("Show favorites"),
|
||||
icon: "fa fa-star-o",
|
||||
onclick: e => {
|
||||
$("#tool-search-query").val("#favorites").trigger("change");
|
||||
$("#tool-search-query")
|
||||
.val("#favorites")
|
||||
.trigger("change");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -210,3 +210,38 @@ export function load_when_ready(url, success_callback) {
|
||||
var spin_state = make_spin_state("IE container readiness");
|
||||
spin_until(url, true, messages, success_callback, spin_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the container alive by pinging `notebookAccessURL` every 10 seconds.
|
||||
* If the user leaves this site this function is not constantly pinging the
|
||||
* container, the container will terminate itself.
|
||||
*/
|
||||
export function keepAlive(notebookAccessURL) {
|
||||
var request_count = 0;
|
||||
var interval = window.setInterval(function() {
|
||||
$.ajax({
|
||||
url: notebookAccessURL,
|
||||
xhrFields: {
|
||||
withCredentials: true
|
||||
},
|
||||
type: "GET",
|
||||
timeout: 500,
|
||||
success: function() {
|
||||
console.log("Connected to IE, returning");
|
||||
},
|
||||
error: function(jqxhr, status, error) {
|
||||
request_count++;
|
||||
console.log("Request " + request_count);
|
||||
if (request_count > 30) {
|
||||
window.clearInterval(interval);
|
||||
clear_main_area();
|
||||
toastr.error("Could not connect to IE, contact your administrator", "Error", {
|
||||
closeButton: true,
|
||||
timeOut: 20000,
|
||||
tapToDismiss: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
// import mod_utils from "utils/utils";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import mod_baseMVC from "mvc/base-mvc";
|
||||
// import mod_library_model from "mvc/library/library-model";
|
||||
import mod_folderlist_view from "mvc/library/library-folderlist-view";
|
||||
@@ -179,7 +179,7 @@ var GalaxyLibrary = Backbone.View.extend({
|
||||
|
||||
this.library_router.on("route:download", (folder_id, format) => {
|
||||
if ($("#folder_list_body").find(":checked").length === 0) {
|
||||
mod_toastr.info("You must select at least one dataset to download");
|
||||
Toast.info("You must select at least one dataset to download");
|
||||
Galaxy.libraries.library_router.navigate(`folders/${folder_id}`, { trigger: true, replace: true });
|
||||
} else {
|
||||
Galaxy.libraries.folderToolbarView.download(format);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import $ from "jquery";
|
||||
import DataDialog from "components/DataDialog.vue";
|
||||
import DataDialog from "components/DataDialog/DataDialog.vue";
|
||||
import Vue from "vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
@@ -9,14 +9,20 @@ export default class Data {
|
||||
* Opens a modal dialog for data selection
|
||||
* @param {function} callback - Result function called with selection
|
||||
*/
|
||||
dialog(callback) {
|
||||
dialog(callback, options = {}) {
|
||||
let galaxy = getGalaxyInstance();
|
||||
let host = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
|
||||
Object.assign(options, {
|
||||
callback: callback,
|
||||
history: galaxy.currHistoryPanel && galaxy.currHistoryPanel.model.id,
|
||||
root: galaxy.root,
|
||||
host: host
|
||||
});
|
||||
var instance = Vue.extend(DataDialog);
|
||||
var vm = document.createElement("div");
|
||||
$("body").append(vm);
|
||||
new instance({
|
||||
propsData: {
|
||||
callback: callback
|
||||
}
|
||||
propsData: options
|
||||
}).$mount(vm);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,12 @@ function logoutClick() {
|
||||
let galaxy = getGalaxyInstance();
|
||||
let session_csrf_token = galaxy.session_csrf_token;
|
||||
let url = `${galaxy.root}user/logout?session_csrf_token=${session_csrf_token}`;
|
||||
axios
|
||||
.get(url)
|
||||
.then(() => {
|
||||
if (galaxy.user) {
|
||||
galaxy.user.clearSessionStorage();
|
||||
}
|
||||
window.top.location.href = `${galaxy.root}login`;
|
||||
});
|
||||
axios.get(url).then(() => {
|
||||
if (galaxy.user) {
|
||||
galaxy.user.clearSessionStorage();
|
||||
}
|
||||
window.top.location.href = `${galaxy.root}login`;
|
||||
});
|
||||
}
|
||||
|
||||
var Collection = Backbone.Collection.extend({
|
||||
@@ -222,20 +220,6 @@ var Collection = Backbone.Collection.extend({
|
||||
target: "_blank"
|
||||
});
|
||||
}
|
||||
if (options.biostar_url) {
|
||||
helpTab.menu.unshift({
|
||||
title: _l("Ask a question"),
|
||||
url: "biostar/biostar_question_redirect",
|
||||
target: "_blank"
|
||||
});
|
||||
}
|
||||
if (options.biostar_url) {
|
||||
helpTab.menu.unshift({
|
||||
title: _l("Galaxy Biostar"),
|
||||
url: options.biostar_url_redirect,
|
||||
target: "_blank"
|
||||
});
|
||||
}
|
||||
if (options.helpsite_url) {
|
||||
helpTab.menu.unshift({
|
||||
title: _l("Galaxy Help"),
|
||||
@@ -421,7 +405,7 @@ var Tab = Backbone.View.extend({
|
||||
} else {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
if (options.target == "__use_router__" && typeof Galaxy.page != "undefined") {
|
||||
Galaxy.page.router.push(options.url);
|
||||
Galaxy.page.router.executeUseRouter(options.url);
|
||||
} else {
|
||||
try {
|
||||
Galaxy.frame.add(options);
|
||||
@@ -460,7 +444,7 @@ var Tab = Backbone.View.extend({
|
||||
} else {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
if (model.attributes.target == "__use_router__" && typeof Galaxy.page != "undefined") {
|
||||
Galaxy.page.router.push(model.attributes.url);
|
||||
Galaxy.page.router.executeUseRouter(model.attributes.url);
|
||||
} else {
|
||||
Galaxy.frame.add(model.attributes);
|
||||
}
|
||||
|
||||
@@ -231,9 +231,18 @@ var CenterPanel = Backbone.View.extend({
|
||||
|
||||
/** Display a view in the center panel, hide iframe */
|
||||
display: function(view) {
|
||||
var contentWindow = this.$frame[0].contentWindow || {};
|
||||
var message = contentWindow.onbeforeunload && contentWindow.onbeforeunload();
|
||||
var Galaxy = getGalaxyInstance();
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let contentWindow = this.$frame[0].contentWindow || {};
|
||||
let message;
|
||||
try {
|
||||
message = contentWindow.onbeforeunload && contentWindow.onbeforeunload();
|
||||
} catch (err) {
|
||||
// This can happen when external content is displayed in this iframe // CORS violation
|
||||
contentWindow = {};
|
||||
console.warn(
|
||||
"Iframe unload exception. This can happen when external content is displayed in the page iframe and causes a CORS violation -- likely harmless."
|
||||
);
|
||||
}
|
||||
if (!message || confirm(message)) {
|
||||
contentWindow.onbeforeunload = undefined;
|
||||
this.$frame.attr("src", "about:blank").hide();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import QUERY_STRING from "utils/query-string-parsing";
|
||||
import Ui from "mvc/ui/ui-misc";
|
||||
@@ -12,6 +13,14 @@ var Router = Backbone.Router.extend({
|
||||
this.options = options;
|
||||
},
|
||||
|
||||
executeUseRouter: function(url) {
|
||||
let prefix = getAppRoot();
|
||||
if (url.startsWith(prefix)) {
|
||||
url = url.replace(prefix, "/");
|
||||
}
|
||||
return this.push(url);
|
||||
},
|
||||
|
||||
/** helper to push a new navigation state */
|
||||
push: function(url, data) {
|
||||
data = data || {};
|
||||
@@ -19,10 +28,12 @@ var Router = Backbone.Router.extend({
|
||||
.toString(36)
|
||||
.substr(2);
|
||||
url += url.indexOf("?") == -1 ? "?" : "&";
|
||||
url += $.param(data, true);
|
||||
let bustParam = $.param(data, true);
|
||||
url += bustParam;
|
||||
let Galaxy = getGalaxyInstance();
|
||||
Galaxy.params = data;
|
||||
this.navigate(url, { trigger: true });
|
||||
window.history.replaceState(window.history.state, "", window.location.pathname.replace(bustParam, ""));
|
||||
},
|
||||
|
||||
/** override to parse query string into obj and send to each route */
|
||||
|
||||
@@ -178,7 +178,7 @@ drag = $special.drag = {
|
||||
case !dd.dragging && 'touchmove':
|
||||
event.preventDefault();
|
||||
case !dd.dragging && 'mousemove':
|
||||
// drag tolerance, x² + y² = distance²
|
||||
// drag tolerance, x² + y² = distance²
|
||||
if ( Math.pow( event.pageX-dd.pageX, 2 ) + Math.pow( event.pageY-dd.pageY, 2 ) < Math.pow( dd.distance, 2 ) )
|
||||
break; // distance tolerance not reached
|
||||
event.target = dd.target; // force target from "mousedown" event (fix distance issue)
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
;(function($){ // secure $ jQuery alias
|
||||
/*******************************************************************************************/
|
||||
// jquery.event.hover.js
|
||||
// Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)
|
||||
// Licensed under the MIT License (MIT-LICENSE.txt)
|
||||
// http://www.opensource.org/licenses/mit-license.php
|
||||
//
|
||||
// JQuery 1.9+ compatible version
|
||||
//
|
||||
// Optional settings :
|
||||
// $.event.special.hover.delay = 100;
|
||||
// Defines the delay (msec) while mouse is inside the element before checking the speed
|
||||
// $.event.special.hover.speed = 100;
|
||||
// Defines the maximum speed (px/sec) the mouse may be moving to trigger the hover event
|
||||
/*******************************************************************************************/
|
||||
|
||||
// save the old jquery "hover" method
|
||||
$.fn._hover = $.fn.hover;
|
||||
|
||||
// jquery method
|
||||
$.fn.hover = function( fn1, fn2, fn3 ) {
|
||||
if ( fn3 ) this.bind('hoverstart', fn1 ); // 3 args
|
||||
if ( fn2 ) this.bind('hoverend', fn3 ? fn3 : fn2 ); // 2+ args
|
||||
return !fn1 ? this.trigger('hover') // 0 args
|
||||
: this.bind('hover', fn3 ? fn2 : fn1 ); // 1+ args
|
||||
};
|
||||
|
||||
// special event configuration
|
||||
var hover = $.event.special.hover = {
|
||||
delay: 100, // milliseconds
|
||||
speed: 100, // pixels per second
|
||||
setup: function( data ){
|
||||
data = $.extend({ speed: hover.speed, delay: hover.delay, hovered:0 }, data||{} );
|
||||
$.event.add( this, "mouseenter mouseleave", hoverHandler, data );
|
||||
},
|
||||
teardown: function(){
|
||||
$.event.remove( this, "mouseenter mouseleave", hoverHandler );
|
||||
}
|
||||
};
|
||||
|
||||
// shared event handler
|
||||
function hoverHandler( event ){
|
||||
var data = event.data || event;
|
||||
switch ( event.type ){
|
||||
case 'mouseenter': // mouseover
|
||||
data.dist2 = 0; // init mouse distance²
|
||||
data.event = event; // store the event
|
||||
event.type = "hoverstart"; // hijack event
|
||||
if($.event.dispatch.call(this, event) !== false) {
|
||||
data.elem = this; // ref to the current element
|
||||
$.event.add( this, "mousemove", hoverHandler, data ); // track the mouse
|
||||
data.timer = setTimeout( compare, data.delay ); // start async compare
|
||||
}
|
||||
break;
|
||||
case 'mousemove': // track the event, mouse distance² = x² + y²
|
||||
data.dist2 += Math.pow( event.pageX-data.event.pageX, 2 )
|
||||
+ Math.pow( event.pageY-data.event.pageY, 2 );
|
||||
data.event = event; // store current event
|
||||
break;
|
||||
case 'mouseleave': // mouseout
|
||||
clearTimeout( data.timer ); // uncompare
|
||||
if ( data.hovered ){
|
||||
event.type = "hoverend"; // hijack event
|
||||
$.event.dispatch.call(this, event); // handle "hoverend"
|
||||
data.hovered--; // reset flag
|
||||
}
|
||||
else $.event.remove( data.elem, "mousemove", hoverHandler ); // untrack
|
||||
break;
|
||||
default: // timeout compare // distance² = x² + y² = ( speed * time )²
|
||||
if ( data.dist2 <= Math.pow( data.speed*( data.delay/1e3 ), 2 ) ){ // speed acceptable
|
||||
$.event.remove( data.elem, "mousemove", hoverHandler ); // untrack
|
||||
data.event.type = "hover"; // hijack event
|
||||
if($.event.dispatch.call(data.elem, data.event) !== false) // handle "hover"
|
||||
data.hovered++; // flag for "hoverend"
|
||||
}
|
||||
else data.timer = setTimeout( compare, data.delay ); // async recurse
|
||||
data.dist2 = 0; // reset distance² for next compare
|
||||
break;
|
||||
}
|
||||
function compare(){ hoverHandler( data ); }; // timeout/recursive function
|
||||
};
|
||||
|
||||
/*******************************************************************************************/
|
||||
;(function($){ // secure $ jQuery alias
|
||||
/*******************************************************************************************/
|
||||
// jquery.event.hover.js
|
||||
// Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)
|
||||
// Licensed under the MIT License (MIT-LICENSE.txt)
|
||||
// http://www.opensource.org/licenses/mit-license.php
|
||||
//
|
||||
// JQuery 1.9+ compatible version
|
||||
//
|
||||
// Optional settings :
|
||||
// $.event.special.hover.delay = 100;
|
||||
// Defines the delay (msec) while mouse is inside the element before checking the speed
|
||||
// $.event.special.hover.speed = 100;
|
||||
// Defines the maximum speed (px/sec) the mouse may be moving to trigger the hover event
|
||||
/*******************************************************************************************/
|
||||
|
||||
// save the old jquery "hover" method
|
||||
$.fn._hover = $.fn.hover;
|
||||
|
||||
// jquery method
|
||||
$.fn.hover = function( fn1, fn2, fn3 ) {
|
||||
if ( fn3 ) this.bind('hoverstart', fn1 ); // 3 args
|
||||
if ( fn2 ) this.bind('hoverend', fn3 ? fn3 : fn2 ); // 2+ args
|
||||
return !fn1 ? this.trigger('hover') // 0 args
|
||||
: this.bind('hover', fn3 ? fn2 : fn1 ); // 1+ args
|
||||
};
|
||||
|
||||
// special event configuration
|
||||
var hover = $.event.special.hover = {
|
||||
delay: 100, // milliseconds
|
||||
speed: 100, // pixels per second
|
||||
setup: function( data ){
|
||||
data = $.extend({ speed: hover.speed, delay: hover.delay, hovered:0 }, data||{} );
|
||||
$.event.add( this, "mouseenter mouseleave", hoverHandler, data );
|
||||
},
|
||||
teardown: function(){
|
||||
$.event.remove( this, "mouseenter mouseleave", hoverHandler );
|
||||
}
|
||||
};
|
||||
|
||||
// shared event handler
|
||||
function hoverHandler( event ){
|
||||
var data = event.data || event;
|
||||
switch ( event.type ){
|
||||
case 'mouseenter': // mouseover
|
||||
data.dist2 = 0; // init mouse distance²
|
||||
data.event = event; // store the event
|
||||
event.type = "hoverstart"; // hijack event
|
||||
if($.event.dispatch.call(this, event) !== false) {
|
||||
data.elem = this; // ref to the current element
|
||||
$.event.add( this, "mousemove", hoverHandler, data ); // track the mouse
|
||||
data.timer = setTimeout( compare, data.delay ); // start async compare
|
||||
}
|
||||
break;
|
||||
case 'mousemove': // track the event, mouse distance² = x² + y²
|
||||
data.dist2 += Math.pow( event.pageX-data.event.pageX, 2 )
|
||||
+ Math.pow( event.pageY-data.event.pageY, 2 );
|
||||
data.event = event; // store current event
|
||||
break;
|
||||
case 'mouseleave': // mouseout
|
||||
clearTimeout( data.timer ); // uncompare
|
||||
if ( data.hovered ){
|
||||
event.type = "hoverend"; // hijack event
|
||||
$.event.dispatch.call(this, event); // handle "hoverend"
|
||||
data.hovered--; // reset flag
|
||||
}
|
||||
else $.event.remove( data.elem, "mousemove", hoverHandler ); // untrack
|
||||
break;
|
||||
default: // timeout compare // distance² = x² + y² = ( speed * time )²
|
||||
if ( data.dist2 <= Math.pow( data.speed*( data.delay/1e3 ), 2 ) ){ // speed acceptable
|
||||
$.event.remove( data.elem, "mousemove", hoverHandler ); // untrack
|
||||
data.event.type = "hover"; // hijack event
|
||||
if($.event.dispatch.call(data.elem, data.event) !== false) // handle "hover"
|
||||
data.hovered++; // flag for "hoverend"
|
||||
}
|
||||
else data.timer = setTimeout( compare, data.delay ); // async recurse
|
||||
data.dist2 = 0; // reset distance² for next compare
|
||||
break;
|
||||
}
|
||||
function compare(){ hoverHandler( data ); }; // timeout/recursive function
|
||||
};
|
||||
|
||||
/*******************************************************************************************/
|
||||
})(jQuery); // confine scope
|
||||
@@ -1,307 +0,0 @@
|
||||
/*
|
||||
* Toastr
|
||||
* Version 2.0.1
|
||||
* Copyright 2012 John Papa and Hans Fjällemark.
|
||||
* All Rights Reserved.
|
||||
* Use, reproduction, distribution, and modification of this code is subject to the terms and
|
||||
* conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* Author: John Papa and Hans Fjällemark
|
||||
* Project: https://github.com/CodeSeven/toastr
|
||||
*/
|
||||
; (function (define) {
|
||||
define([], function () {
|
||||
var $ = jQuery;
|
||||
return (function () {
|
||||
var version = '2.0.1';
|
||||
var $container;
|
||||
var listener;
|
||||
var toastId = 0;
|
||||
var toastType = {
|
||||
error: 'error',
|
||||
info: 'info',
|
||||
success: 'success',
|
||||
warning: 'warning'
|
||||
};
|
||||
|
||||
var toastr = {
|
||||
clear: clear,
|
||||
error: error,
|
||||
getContainer: getContainer,
|
||||
info: info,
|
||||
options: {},
|
||||
subscribe: subscribe,
|
||||
success: success,
|
||||
version: version,
|
||||
warning: warning
|
||||
};
|
||||
|
||||
return toastr;
|
||||
|
||||
//#region Accessible Methods
|
||||
function error(message, title, optionsOverride) {
|
||||
return notify({
|
||||
type: toastType.error,
|
||||
iconClass: getOptions().iconClasses.error,
|
||||
message: message,
|
||||
optionsOverride: optionsOverride,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
function info(message, title, optionsOverride) {
|
||||
return notify({
|
||||
type: toastType.info,
|
||||
iconClass: getOptions().iconClasses.info,
|
||||
message: message,
|
||||
optionsOverride: optionsOverride,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
function subscribe(callback) {
|
||||
listener = callback;
|
||||
}
|
||||
|
||||
function success(message, title, optionsOverride) {
|
||||
return notify({
|
||||
type: toastType.success,
|
||||
iconClass: getOptions().iconClasses.success,
|
||||
message: message,
|
||||
optionsOverride: optionsOverride,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
function warning(message, title, optionsOverride) {
|
||||
return notify({
|
||||
type: toastType.warning,
|
||||
iconClass: getOptions().iconClasses.warning,
|
||||
message: message,
|
||||
optionsOverride: optionsOverride,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
function clear($toastElement) {
|
||||
var options = getOptions();
|
||||
if (!$container) { getContainer(options); }
|
||||
if ($toastElement && $(':focus', $toastElement).length === 0) {
|
||||
$toastElement[options.hideMethod]({
|
||||
duration: options.hideDuration,
|
||||
easing: options.hideEasing,
|
||||
complete: function () { removeToast($toastElement); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
if ($container.children().length) {
|
||||
$container[options.hideMethod]({
|
||||
duration: options.hideDuration,
|
||||
easing: options.hideEasing,
|
||||
complete: function () { $container.remove(); }
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Internal Methods
|
||||
|
||||
function getDefaults() {
|
||||
return {
|
||||
tapToDismiss: true,
|
||||
toastClass: 'toast',
|
||||
containerId: 'toast-container',
|
||||
debug: false,
|
||||
|
||||
showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery
|
||||
showDuration: 300,
|
||||
showEasing: 'swing', //swing and linear are built into jQuery
|
||||
onShown: undefined,
|
||||
hideMethod: 'fadeOut',
|
||||
hideDuration: 1000,
|
||||
hideEasing: 'swing',
|
||||
onHidden: undefined,
|
||||
|
||||
extendedTimeOut: 1000,
|
||||
iconClasses: {
|
||||
error: 'toast-error',
|
||||
info: 'toast-info',
|
||||
success: 'toast-success',
|
||||
warning: 'toast-warning'
|
||||
},
|
||||
iconClass: 'toast-info',
|
||||
positionClass: 'toast-top-right',
|
||||
timeOut: 5000, // Set timeOut and extendedTimeout to 0 to make it sticky
|
||||
titleClass: 'toast-title',
|
||||
messageClass: 'toast-message',
|
||||
target: 'body',
|
||||
closeHtml: '<button>×</button>',
|
||||
newestOnTop: true
|
||||
};
|
||||
}
|
||||
|
||||
function publish(args) {
|
||||
if (!listener) {
|
||||
return;
|
||||
}
|
||||
listener(args);
|
||||
}
|
||||
|
||||
function notify(map) {
|
||||
var
|
||||
options = getOptions(),
|
||||
iconClass = map.iconClass || options.iconClass;
|
||||
|
||||
if (typeof (map.optionsOverride) !== 'undefined') {
|
||||
options = $.extend(options, map.optionsOverride);
|
||||
iconClass = map.optionsOverride.iconClass || iconClass;
|
||||
}
|
||||
|
||||
toastId++;
|
||||
|
||||
$container = getContainer(options);
|
||||
var
|
||||
intervalId = null,
|
||||
$toastElement = $('<div/>'),
|
||||
$titleElement = $('<div/>'),
|
||||
$messageElement = $('<div/>'),
|
||||
$closeElement = $(options.closeHtml),
|
||||
response = {
|
||||
toastId: toastId,
|
||||
state: 'visible',
|
||||
startTime: new Date(),
|
||||
options: options,
|
||||
map: map
|
||||
};
|
||||
|
||||
if (map.iconClass) {
|
||||
$toastElement.addClass(options.toastClass).addClass(iconClass);
|
||||
}
|
||||
|
||||
if (map.title) {
|
||||
$titleElement.append(map.title).addClass(options.titleClass);
|
||||
$toastElement.append($titleElement);
|
||||
}
|
||||
|
||||
if (map.message) {
|
||||
$messageElement.append(map.message).addClass(options.messageClass);
|
||||
$toastElement.append($messageElement);
|
||||
}
|
||||
|
||||
if (options.closeButton) {
|
||||
$closeElement.addClass('toast-close-button');
|
||||
$toastElement.prepend($closeElement);
|
||||
}
|
||||
|
||||
$toastElement.hide();
|
||||
if (options.newestOnTop) {
|
||||
$container.prepend($toastElement);
|
||||
} else {
|
||||
$container.append($toastElement);
|
||||
}
|
||||
|
||||
|
||||
$toastElement[options.showMethod](
|
||||
{ duration: options.showDuration, easing: options.showEasing, complete: options.onShown }
|
||||
);
|
||||
if (options.timeOut > 0) {
|
||||
intervalId = setTimeout(hideToast, options.timeOut);
|
||||
}
|
||||
|
||||
$toastElement.hover(stickAround, delayedhideToast);
|
||||
if (!options.onclick && options.tapToDismiss) {
|
||||
$toastElement.click(hideToast);
|
||||
}
|
||||
if (options.closeButton && $closeElement) {
|
||||
$closeElement.click(function (event) {
|
||||
event.stopPropagation();
|
||||
hideToast(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.onclick) {
|
||||
$toastElement.click(function () {
|
||||
options.onclick();
|
||||
hideToast();
|
||||
});
|
||||
}
|
||||
|
||||
publish(response);
|
||||
|
||||
if (options.debug && console) {
|
||||
console.log(response);
|
||||
}
|
||||
|
||||
return $toastElement;
|
||||
|
||||
function hideToast(override) {
|
||||
if ($(':focus', $toastElement).length && !override) {
|
||||
return;
|
||||
}
|
||||
return $toastElement[options.hideMethod]({
|
||||
duration: options.hideDuration,
|
||||
easing: options.hideEasing,
|
||||
complete: function () {
|
||||
removeToast($toastElement);
|
||||
if (options.onHidden) {
|
||||
options.onHidden();
|
||||
}
|
||||
response.state = 'hidden';
|
||||
response.endTime = new Date(),
|
||||
publish(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function delayedhideToast() {
|
||||
if (options.timeOut > 0 || options.extendedTimeOut > 0) {
|
||||
intervalId = setTimeout(hideToast, options.extendedTimeOut);
|
||||
}
|
||||
}
|
||||
|
||||
function stickAround() {
|
||||
clearTimeout(intervalId);
|
||||
$toastElement.stop(true, true)[options.showMethod](
|
||||
{ duration: options.showDuration, easing: options.showEasing }
|
||||
);
|
||||
}
|
||||
}
|
||||
function getContainer(options) {
|
||||
if (!options) { options = getOptions(); }
|
||||
$container = $('#' + options.containerId);
|
||||
if ($container.length) {
|
||||
return $container;
|
||||
}
|
||||
$container = $('<div/>')
|
||||
.attr('id', options.containerId)
|
||||
.addClass(options.positionClass);
|
||||
$container.appendTo($(options.target));
|
||||
return $container;
|
||||
}
|
||||
|
||||
function getOptions() {
|
||||
return $.extend({}, getDefaults(), toastr.options);
|
||||
}
|
||||
|
||||
function removeToast($toastElement) {
|
||||
if (!$container) { $container = getContainer(); }
|
||||
if ($toastElement.is(':visible')) {
|
||||
return;
|
||||
}
|
||||
$toastElement.remove();
|
||||
$toastElement = null;
|
||||
if ($container.children().length === 0) {
|
||||
$container.remove();
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
})();
|
||||
});
|
||||
}(typeof define === 'function' && define.amd ? define : function (deps, factory) {
|
||||
if (typeof module !== 'undefined' && module.exports) { //Node
|
||||
module.exports = factory(require(deps[0]));
|
||||
} else {
|
||||
window['toastr'] = factory(window['jQuery']);
|
||||
}
|
||||
}));
|
||||
@@ -1,7 +1,8 @@
|
||||
import $ from "jquery";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import DC_VIEW from "mvc/collection/collection-view";
|
||||
import DC_EDIT from "mvc/collection/collection-li-edit";
|
||||
import TAGS from "mvc/tag";
|
||||
import { mountModelTags } from "components/Tags";
|
||||
import _l from "utils/localization";
|
||||
import "ui/editable-text";
|
||||
|
||||
@@ -76,15 +77,27 @@ var CollectionViewEdit = _super.extend(
|
||||
}
|
||||
}
|
||||
});
|
||||
this.tagsEditor = new TAGS.TagsEditor({
|
||||
|
||||
let el = $where.find(".tags-display")[0];
|
||||
let propsData = {
|
||||
model: this.model,
|
||||
el: $where.find(".tags-display"),
|
||||
onshowFirstTime: function() {
|
||||
this.render();
|
||||
},
|
||||
usePrompt: false
|
||||
});
|
||||
this.tagsEditor.toggle(true);
|
||||
disabled: false,
|
||||
context: "collection-view-edit"
|
||||
};
|
||||
|
||||
let vm = mountModelTags(propsData, el);
|
||||
|
||||
let toggleEditor = () => {
|
||||
$(vm.$el).toggleClass("active");
|
||||
this.tagsEditorShown = $(vm.$el).hasClass("active");
|
||||
};
|
||||
|
||||
if (this.tagsEditorShown) {
|
||||
let editorIsOpen = $(vm.$el).hasClass("active");
|
||||
if (!editorIsOpen) {
|
||||
toggleEditor();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ........................................................................ misc
|
||||
|
||||
@@ -7,7 +7,6 @@ import BASE_MVC from "mvc/base-mvc";
|
||||
import baseCreator from "mvc/collection/base-creator";
|
||||
import UI_MODAL from "mvc/ui/ui-modal";
|
||||
import _l from "utils/localization";
|
||||
import RuleCollectionBuilder from "components/RuleCollectionBuilder.vue";
|
||||
import Vue from "vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
|
||||
@@ -1077,24 +1076,28 @@ var ruleBasedCollectionCreatorModal = function _ruleBasedCollectionCreatorModal(
|
||||
title: title
|
||||
});
|
||||
const { deferred, creatorOptions, showEl } = collectionCreatorModalSetup(options); // eslint-disable-line no-unused-vars
|
||||
var ruleCollectionBuilderInstance = Vue.extend(RuleCollectionBuilder);
|
||||
var vm = document.createElement("div");
|
||||
showEl(vm);
|
||||
new ruleCollectionBuilderInstance({
|
||||
propsData: {
|
||||
initialElements: elements,
|
||||
elementsType: elementsType,
|
||||
importType: importType,
|
||||
ftpUploadSite: options.ftpUploadSite,
|
||||
creationFn: options.creationFn,
|
||||
oncancel: options.oncancel,
|
||||
oncreate: options.oncreate,
|
||||
defaultHideSourceItems: options.defaultHideSourceItems,
|
||||
saveRulesFn: options.saveRulesFn,
|
||||
initialRules: options.initialRules
|
||||
return import(/* webpackChunkName: "ruleCollectionBuilder" */ "components/RuleCollectionBuilder.vue").then(
|
||||
module => {
|
||||
var ruleCollectionBuilderInstance = Vue.extend(module.default);
|
||||
var vm = document.createElement("div");
|
||||
showEl(vm);
|
||||
new ruleCollectionBuilderInstance({
|
||||
propsData: {
|
||||
initialElements: elements,
|
||||
elementsType: elementsType,
|
||||
importType: importType,
|
||||
ftpUploadSite: options.ftpUploadSite,
|
||||
creationFn: options.creationFn,
|
||||
oncancel: options.oncancel,
|
||||
oncreate: options.oncreate,
|
||||
defaultHideSourceItems: options.defaultHideSourceItems,
|
||||
saveRulesFn: options.saveRulesFn,
|
||||
initialRules: options.initialRules
|
||||
}
|
||||
}).$mount(vm);
|
||||
return deferred;
|
||||
}
|
||||
}).$mount(vm);
|
||||
return deferred;
|
||||
);
|
||||
};
|
||||
|
||||
/** List collection flavor of collectionCreatorModal. */
|
||||
|
||||
@@ -267,8 +267,30 @@ var TabularDatasetChunkedView = Backbone.View.extend({
|
||||
);
|
||||
row.append(this._renderCell(cells.slice(num_columns - 1).join("\t"), num_columns - 1));
|
||||
} else if (cells.length === 1) {
|
||||
// Comment line, just return the one cell.
|
||||
row.append(this._renderCell(line, 0, num_columns));
|
||||
cells = line.split(",");
|
||||
if (cells.length === num_columns) {
|
||||
_.each(
|
||||
cells,
|
||||
function(cell_contents, index) {
|
||||
row.append(this._renderCell(cell_contents, index));
|
||||
},
|
||||
this
|
||||
);
|
||||
} else {
|
||||
cells = line.split(" ");
|
||||
if (cells.length === num_columns) {
|
||||
_.each(
|
||||
cells,
|
||||
function(cell_contents, index) {
|
||||
row.append(this._renderCell(cell_contents, index));
|
||||
},
|
||||
this
|
||||
);
|
||||
} else {
|
||||
// Comment line, just return the one cell.
|
||||
row.append(this._renderCell(line, 0, num_columns));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// cells.length is greater than one, but less than num_columns. Render cells and pad tds.
|
||||
// Possibly a SAM file or like format with optional metadata missing.
|
||||
|
||||
@@ -32,6 +32,7 @@ var View = Backbone.View.extend({
|
||||
url: job_url,
|
||||
success: job => {
|
||||
this.render_error_page(dataset, job);
|
||||
this.find_common_problems(job);
|
||||
},
|
||||
error: response => {
|
||||
var error_response = {
|
||||
@@ -56,15 +57,53 @@ var View = Backbone.View.extend({
|
||||
});
|
||||
},
|
||||
|
||||
find_common_problems: function(job) {
|
||||
var job_url = `${getAppRoot()}api/jobs/${job.id}/common_problems`;
|
||||
Utils.get({
|
||||
url: job_url,
|
||||
success: common_problems => {
|
||||
this.render_common_problems(job, common_problems);
|
||||
},
|
||||
error: response => {
|
||||
console.log("error");
|
||||
console.log(response);
|
||||
}
|
||||
});
|
||||
return;
|
||||
},
|
||||
|
||||
render_common_problems: function(job, common_problems) {
|
||||
const has_duplicate_inputs = common_problems.has_duplicate_inputs;
|
||||
const has_empty_inputs = common_problems.has_empty_inputs;
|
||||
if (has_duplicate_inputs || has_empty_inputs) {
|
||||
const reportEl = this.$el.find(".common_problems");
|
||||
reportEl.text("Detected Common Potential Problems");
|
||||
if (has_empty_inputs) {
|
||||
reportEl.after(`
|
||||
<p>
|
||||
The tool was executed with one or more empty input datasets, this isn't
|
||||
always a problem and may not have been but frequently this is a source
|
||||
of error.
|
||||
<p>`);
|
||||
}
|
||||
if (has_duplicate_inputs) {
|
||||
reportEl.after(`
|
||||
<p>
|
||||
The tool was executed with one or more duplicate input datasets, this isn't
|
||||
always a problem and may not have been but frequently this is a source
|
||||
of error.
|
||||
<p>`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** Render the view */
|
||||
render_error_page: function(dataset, job) {
|
||||
this.$el.empty().append(`
|
||||
${this._templateHeader()}
|
||||
<h2>Dataset Error</h2>
|
||||
<p>An error occurred while running the tool <b>${job.tool_id}</b>.</p>
|
||||
<p>Tool execution generated the following messages:</p>
|
||||
<pre class="code">${_.escape(job.stderr)}</pre>
|
||||
|
||||
${this.job_summary(job)}
|
||||
<h3>Troubleshoot This Error</h3>
|
||||
<p>
|
||||
There are a number of help resources to self diagnose and
|
||||
@@ -85,6 +124,32 @@ var View = Backbone.View.extend({
|
||||
this.$el.append(this._getBugFormTemplate(dataset, job));
|
||||
},
|
||||
|
||||
job_summary: function(job) {
|
||||
const tool_stderr = job.tool_stderr;
|
||||
const job_stderr = job.job_stderr;
|
||||
const job_messages = job.job_messages;
|
||||
if (!tool_stderr && !job_stderr && !job_messages) {
|
||||
return '<h3 class="common_problems"></h3>';
|
||||
}
|
||||
var message = "<h3>Error Details</h3>";
|
||||
if (job_messages) {
|
||||
message += "<p>Execution resulted in the following messages:</p>";
|
||||
for (let job_message of job_messages) {
|
||||
message += `<p><pre>${_.escape(job_message["desc"])}</pre></p>`;
|
||||
}
|
||||
}
|
||||
if (tool_stderr) {
|
||||
message += "<p>Tool generated the following standard error:</p>";
|
||||
message += `<pre class="code">${_.escape(tool_stderr)}</pre>`;
|
||||
}
|
||||
if (job_stderr) {
|
||||
message += "<p>Galaxy job runner generated the following standard error:</p>";
|
||||
message += `<pre class="code">${_.escape(job_stderr)}</pre>`;
|
||||
}
|
||||
message += `<h3 class="common_problems"></h3>`;
|
||||
return message;
|
||||
},
|
||||
|
||||
/** Display actions messages */
|
||||
display_message: function(response, $el, doNotClear, safe) {
|
||||
if (!safe) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import STATES from "mvc/dataset/states";
|
||||
import DATASET_LI from "mvc/dataset/dataset-li";
|
||||
import TAGS from "mvc/tag";
|
||||
import { mountModelTags } from "components/Tags";
|
||||
import ANNOTATIONS from "mvc/annotation";
|
||||
import faIconButton from "ui/fa-icon-button";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
@@ -297,35 +297,44 @@ var DatasetListItemEdit = _super.extend(
|
||||
}
|
||||
},
|
||||
|
||||
//TODO: if possible move these to readonly view - but display the owner's tags/annotation (no edit)
|
||||
/** Render the tags list/control */
|
||||
_renderTags: function($where) {
|
||||
if (!this.hasUser) {
|
||||
return;
|
||||
}
|
||||
var view = this;
|
||||
this.tagsEditor = new TAGS.TagsEditor({
|
||||
|
||||
let el = $where.find(".tags-display")[0];
|
||||
|
||||
let propsData = {
|
||||
model: this.model,
|
||||
el: $where.find(".tags-display"),
|
||||
onshowFirstTime: function() {
|
||||
this.render();
|
||||
},
|
||||
// persist state on the hda view (and not the editor) since these are currently re-created each time
|
||||
onshow: function() {
|
||||
view.tagsEditorShown = true;
|
||||
},
|
||||
onhide: function() {
|
||||
view.tagsEditorShown = false;
|
||||
},
|
||||
$activator: faIconButton({
|
||||
title: _l("Edit dataset tags"),
|
||||
classes: "tag-btn",
|
||||
faIcon: "fa-tags"
|
||||
}).appendTo($where.find(".actions .right"))
|
||||
});
|
||||
disabled: false,
|
||||
context: "dataset-li-edit"
|
||||
};
|
||||
|
||||
let vm = mountModelTags(propsData, el);
|
||||
|
||||
// tag icon button open/closes
|
||||
let activator = faIconButton({
|
||||
title: _l("Edit dataset tags"),
|
||||
classes: "tag-btn",
|
||||
faIcon: "fa-tags"
|
||||
}).appendTo($where.find(".actions .right"));
|
||||
|
||||
let toggleEditor = () => {
|
||||
$(vm.$el).toggleClass("active");
|
||||
this.tagsEditorShown = $(vm.$el).hasClass("active");
|
||||
};
|
||||
|
||||
activator.on("click", toggleEditor);
|
||||
|
||||
if (this.tagsEditorShown) {
|
||||
this.tagsEditor.toggle(true);
|
||||
let editorIsOpen = $(vm.$el).hasClass("active");
|
||||
if (!editorIsOpen) {
|
||||
toggleEditor();
|
||||
}
|
||||
}
|
||||
|
||||
return vm;
|
||||
},
|
||||
|
||||
/** Render the annotation display/control */
|
||||
|
||||
@@ -7,6 +7,7 @@ import STATES from "mvc/dataset/states";
|
||||
import faIconButton from "ui/fa-icon-button";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import _l from "utils/localization";
|
||||
import { mountNametags } from "components/Nametags";
|
||||
|
||||
var logNamespace = "dataset";
|
||||
/*==============================================================================
|
||||
@@ -42,6 +43,20 @@ export var DatasetListItemView = _super.extend(
|
||||
this.linkTarget = attributes.linkTarget || "_blank";
|
||||
},
|
||||
|
||||
// mount new vue component for tags
|
||||
render: function() {
|
||||
let result = _super.prototype.render.apply(this, arguments);
|
||||
this._mountVueNametags();
|
||||
return result;
|
||||
},
|
||||
|
||||
_mountVueNametags: function() {
|
||||
let container = this.$(".nametags")[0];
|
||||
let { id, model_class, tags } = this.model.attributes;
|
||||
let storeKey = `${model_class}-${id}`;
|
||||
mountNametags({ storeKey, tags }, container);
|
||||
},
|
||||
|
||||
/** event listeners */
|
||||
_setUpListeners: function() {
|
||||
_super.prototype._setUpListeners.call(this);
|
||||
@@ -64,11 +79,11 @@ export var DatasetListItemView = _super.extend(
|
||||
self.render();
|
||||
});
|
||||
} else {
|
||||
if (_.has(model.changed, "tags") && _.keys(model.changed).length === 1) {
|
||||
// If only the tags have changed, rerender specifically
|
||||
// the titlebar region. Otherwise default to the full
|
||||
// render.
|
||||
self.$(".nametags").html(self._renderNametags());
|
||||
if (_.has(self.model.changed, "tags") && _.keys(self.model.changed).length === 2) {
|
||||
// If only the tags and update time have changed,
|
||||
// rerender specifically the titlebar region.
|
||||
// Otherwise default to the full render.
|
||||
self._mountVueNametags();
|
||||
} else {
|
||||
self.render();
|
||||
}
|
||||
@@ -327,19 +342,6 @@ export var DatasetListItemView = _super.extend(
|
||||
</div>`);
|
||||
},
|
||||
|
||||
_renderNametags: function() {
|
||||
var tpl = _.template(
|
||||
[
|
||||
"<% _.each(_.sortBy(_.uniq(tags), function(x) { return x }), function(tag){ %>",
|
||||
'<% if (tag.indexOf("name:") == 0){ %>',
|
||||
'<span class="badge badge-primary badge-tags"><%- tag.slice(5) %></span>',
|
||||
"<% } %>",
|
||||
"<% }); %>"
|
||||
].join("")
|
||||
);
|
||||
return tpl({ tags: this.model.get("tags") });
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
events: _.extend(_.clone(_super.prototype.events), {
|
||||
"click .display-btn": function(ev) {
|
||||
|
||||
@@ -203,7 +203,7 @@ export default Backbone.View.extend({
|
||||
.append(
|
||||
$("<div/>")
|
||||
.addClass("ui-form-field")
|
||||
.append($("<span/>").addClass("ui-form-info form-text text-muted"))
|
||||
.append($("<span/>").addClass("ui-form-info form-text text-muted mt-2"))
|
||||
.append($("<div/>").addClass("ui-form-backdrop"))
|
||||
)
|
||||
.append($("<div/>").addClass("ui-form-preview"));
|
||||
|
||||
@@ -29,6 +29,7 @@ export default Backbone.Model.extend({
|
||||
boolean: "_fieldBoolean",
|
||||
drill_down: "_fieldDrilldown",
|
||||
color: "_fieldColor",
|
||||
group_tag: "_fieldSelect",
|
||||
hidden: "_fieldHidden",
|
||||
hidden_data: "_fieldHidden",
|
||||
baseurl: "_fieldHidden",
|
||||
@@ -125,7 +126,7 @@ export default Backbone.Model.extend({
|
||||
/** Text input field */
|
||||
_fieldText: function(input_def) {
|
||||
// field replaces e.g. a select field
|
||||
if (input_def.options && input_def.data) {
|
||||
if (input_def.model_class === 'SelectTagParameter' || input_def.options && input_def.data) {
|
||||
input_def.area = input_def.multiple;
|
||||
if (Utils.isEmpty(input_def.value)) {
|
||||
input_def.value = null;
|
||||
|
||||
@@ -8,6 +8,8 @@ import Templates from "mvc/grid/grid-template";
|
||||
import PopupMenu from "mvc/ui/popup-menu";
|
||||
import LoadingIndicator from "ui/loading-indicator";
|
||||
import { init_refresh_on_change } from "onload/globalInits/init_refresh_on_change";
|
||||
import store from "../../store";
|
||||
import slugify from "slugify";
|
||||
|
||||
// This is necessary so that, when nested arrays are used in ajax/post/get methods, square brackets ('[]') are
|
||||
// not appended to the identifier of a nested array.
|
||||
@@ -25,17 +27,21 @@ export default Backbone.View.extend({
|
||||
this.active_tab = grid_config.active_tab;
|
||||
var self = this;
|
||||
|
||||
// Why is this a global?
|
||||
window.add_tag_to_grid_filter = (tag_name, tag_value) => {
|
||||
// Put tag name and value together.
|
||||
var tag = tag_name + (tag_value !== undefined && tag_value !== "" ? `:${tag_value}` : "");
|
||||
var advanced_search = $("#advanced-search").is(":visible");
|
||||
if (!advanced_search) {
|
||||
$("#standard-search").slideToggle("fast");
|
||||
$("#advanced-search").slideToggle("fast");
|
||||
// Subscribe to changes in the store, currently just storing
|
||||
// tag changes from the tagging components, but that will change
|
||||
// when we rework the grid. This subscription ties this older grid
|
||||
// code to the new vue components
|
||||
store.watch(
|
||||
state => state.gridSearch.searchTags,
|
||||
newTags => {
|
||||
let tagArray = Array.from(newTags);
|
||||
self.grid.add_filter("tags", tagArray, false);
|
||||
self.openAdvancedSearch();
|
||||
self.render_filter_button("tags", tagArray);
|
||||
self.go_page_one();
|
||||
self.execute();
|
||||
}
|
||||
self.add_filter_condition("tags", tag);
|
||||
};
|
||||
);
|
||||
|
||||
if (grid_config.url_base && !grid_config.items) {
|
||||
LoadingIndicator.markViewAsLoading(this);
|
||||
@@ -66,6 +72,14 @@ export default Backbone.View.extend({
|
||||
}
|
||||
},
|
||||
|
||||
openAdvancedSearch: function() {
|
||||
var isOpen = $("#advanced-search").is(":visible");
|
||||
if (!isOpen) {
|
||||
$("#standard-search").slideToggle("fast");
|
||||
$("#advanced-search").slideToggle("fast");
|
||||
}
|
||||
},
|
||||
|
||||
// refresh frames
|
||||
handle_refresh: function(refresh_frames) {
|
||||
if (refresh_frames) {
|
||||
@@ -99,6 +113,9 @@ export default Backbone.View.extend({
|
||||
// append main template
|
||||
this.$el.html(Templates.grid(options));
|
||||
|
||||
// add a class identifier for styling purposes
|
||||
this.$el.addClass(this.getRootClassName(grid_config));
|
||||
|
||||
// update div contents
|
||||
this.$el.find("#grid-table-header").html(Templates.header(options));
|
||||
this.$el.find("#grid-table-body").html(Templates.body(options));
|
||||
@@ -310,6 +327,14 @@ export default Backbone.View.extend({
|
||||
// Add condition to grid.
|
||||
this.grid.add_filter(name, value, true);
|
||||
|
||||
this.render_filter_button(name, value);
|
||||
|
||||
// execute
|
||||
this.go_page_one();
|
||||
this.execute();
|
||||
},
|
||||
|
||||
render_filter_button: function(name, value) {
|
||||
// Add button that displays filter and provides a button to delete it.
|
||||
var t = $(Templates.filter_element(name, value));
|
||||
var self = this;
|
||||
@@ -324,10 +349,6 @@ export default Backbone.View.extend({
|
||||
// append to container
|
||||
var container = this.$el.find(`#${name}-filtering-criteria`);
|
||||
container.append(t);
|
||||
|
||||
// execute
|
||||
this.go_page_one();
|
||||
this.execute();
|
||||
},
|
||||
|
||||
// Remove a condition to the grid filter; this adds the condition and refreshes the grid.
|
||||
@@ -335,6 +356,11 @@ export default Backbone.View.extend({
|
||||
// Remove filter condition.
|
||||
this.grid.remove_filter(name, value);
|
||||
|
||||
// update vuex if the one criteria we're currently tracking changes
|
||||
if (name == "tags") {
|
||||
store.dispatch("removeSearchTag", { text: value });
|
||||
}
|
||||
|
||||
// Execute
|
||||
this.go_page_one();
|
||||
this.execute();
|
||||
@@ -665,5 +691,12 @@ export default Backbone.View.extend({
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Generates a class name at the root of the view that we can
|
||||
// use for conditional styling in the various kinds of grids
|
||||
// instead of acres of if/then statements in javascript
|
||||
getRootClassName({ title = "grid" }) {
|
||||
return slugify(title).toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import _ from "underscore";
|
||||
import DATASET_LI from "mvc/dataset/dataset-li";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import HISTORY_ITEM_LI from "mvc/history/history-item-li";
|
||||
import _l from "utils/localization";
|
||||
|
||||
//==============================================================================
|
||||
@@ -39,7 +38,7 @@ HDAListItemView.prototype.templates = (() => {
|
||||
<span class="name">${_.escape(dataset.name)}</span>
|
||||
</div>
|
||||
</br>
|
||||
${HISTORY_ITEM_LI.nametagTemplate(dataset)}
|
||||
<div class="nametags"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import _ from "underscore";
|
||||
import STATES from "mvc/dataset/states";
|
||||
import DC_LI from "mvc/collection/collection-li";
|
||||
import DC_VIEW from "mvc/collection/collection-view";
|
||||
import HISTORY_ITEM_LI from "mvc/history/history-item-li";
|
||||
import _l from "utils/localization";
|
||||
import { mountNametags } from "components/Nametags";
|
||||
|
||||
//==============================================================================
|
||||
var _super = DC_LI.DCListItemView;
|
||||
@@ -13,12 +13,19 @@ var HDCAListItemView = _super.extend(
|
||||
/** @lends HDCAListItemView.prototype */ {
|
||||
className: `${_super.prototype.className} history-content`,
|
||||
|
||||
render: function() {
|
||||
let result = _super.prototype.render.apply(this, arguments);
|
||||
this._mountNametags("initialize");
|
||||
return result;
|
||||
},
|
||||
|
||||
/** event listeners */
|
||||
_setUpListeners: function() {
|
||||
_super.prototype._setUpListeners.call(this);
|
||||
var renderListen = (model, options) => {
|
||||
// We want this to swap immediately without extra animations.
|
||||
this.render(0);
|
||||
this._mountNametags("listener");
|
||||
};
|
||||
if (this.model.jobStatesSummary) {
|
||||
this.listenTo(this.model.jobStatesSummary, "change", renderListen);
|
||||
@@ -28,6 +35,15 @@ var HDCAListItemView = _super.extend(
|
||||
});
|
||||
},
|
||||
|
||||
_mountNametags(context) {
|
||||
let container = this.$el.find(".nametags")[0];
|
||||
if (container) {
|
||||
let { id, model_class, tags } = this.model.attributes;
|
||||
let storeKey = `${model_class}-${id}`;
|
||||
mountNametags({ storeKey, tags }, container);
|
||||
}
|
||||
},
|
||||
|
||||
/** Override to provide the proper collections panels as the foldout */
|
||||
_getFoldoutPanelClass: function() {
|
||||
return DC_VIEW.CollectionView;
|
||||
@@ -137,7 +153,7 @@ HDCAListItemView.prototype.templates = (() => {
|
||||
</div>
|
||||
<div class="state-description">
|
||||
</div>
|
||||
${HISTORY_ITEM_LI.nametagTemplate(collection)}
|
||||
<div class="nametags"><!-- Nametags mount here (hdca-li) --></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import _ from "underscore";
|
||||
import Utils from "utils/utils";
|
||||
|
||||
function _templateNametag(tag) {
|
||||
return `<span style="${Utils.generateTagStyle(tag.slice(5))}" class="badge badge-primary badge-tags">${_.escape(
|
||||
tag.slice(5)
|
||||
)}</span>`;
|
||||
}
|
||||
|
||||
function nametagTemplate(historyItem) {
|
||||
let uniqueNametags = _.filter(_.uniq(historyItem.tags), t => t.indexOf("name:") === 0);
|
||||
let nametagsDisplay = _.sortBy(uniqueNametags).map(_templateNametag);
|
||||
return `
|
||||
<div class="nametags" title="${uniqueNametags.length} nametags">
|
||||
${nametagsDisplay.join("")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export default {
|
||||
nametagTemplate: nametagTemplate
|
||||
};
|
||||
@@ -224,16 +224,7 @@ var CurrentHistoryView = _super.extend(
|
||||
|
||||
/** In this override, get and set current panel preferences when editor is used */
|
||||
_renderTags: function($where) {
|
||||
var panel = this;
|
||||
// render tags and show/hide based on preferences
|
||||
_super.prototype._renderTags.call(panel, $where);
|
||||
if (panel.preferences.get("tagsEditorShown")) {
|
||||
panel.tagsEditor.toggle(true);
|
||||
}
|
||||
// store preference when shown or hidden
|
||||
panel.listenTo(panel.tagsEditor, "hiddenUntilActivated:shown hiddenUntilActivated:hidden", tagsEditor => {
|
||||
panel.preferences.set("tagsEditorShown", tagsEditor.hidden);
|
||||
});
|
||||
return _super.prototype._renderTags.call(this, $where);
|
||||
},
|
||||
|
||||
/** In this override, get and set current panel preferences when editor is used */
|
||||
|
||||
@@ -5,7 +5,7 @@ import HISTORY_VIEW from "mvc/history/history-view";
|
||||
import HDA_MODEL from "mvc/history/hda-model";
|
||||
import HDA_LI_EDIT from "mvc/history/hda-li-edit";
|
||||
import HDCA_LI_EDIT from "mvc/history/hdca-li-edit";
|
||||
import TAGS from "mvc/tag";
|
||||
import { mountModelTags } from "components/Tags";
|
||||
import ANNOTATIONS from "mvc/annotation";
|
||||
import LIST_COLLECTION_CREATOR from "mvc/collection/list-collection-creator";
|
||||
import PAIR_COLLECTION_CREATOR from "mvc/collection/pair-collection-creator";
|
||||
@@ -155,28 +155,31 @@ var HistoryViewEdit = _super.extend(
|
||||
|
||||
/** render the tags sub-view controller */
|
||||
_renderTags: function($where) {
|
||||
var panel = this;
|
||||
this.tagsEditor = new TAGS.TagsEditor({
|
||||
let el = $where.find(".controls .tags-display")[0];
|
||||
|
||||
let propsData = {
|
||||
model: this.model,
|
||||
el: $where.find(".controls .tags-display"),
|
||||
onshowFirstTime: function() {
|
||||
this.render();
|
||||
},
|
||||
// show hide sub-view tag editors when this is shown/hidden
|
||||
onshow: function() {
|
||||
panel.toggleHDATagEditors(true, panel.fxSpeed);
|
||||
},
|
||||
onhide: function() {
|
||||
panel.toggleHDATagEditors(false, panel.fxSpeed);
|
||||
},
|
||||
$activator: faIconButton({
|
||||
title: _l("Edit history tags"),
|
||||
classes: "history-tag-btn",
|
||||
faIcon: "fa-tags",
|
||||
tooltipConfig: { placement: "top" }
|
||||
}).appendTo($where.find(".controls .actions"))
|
||||
disabled: false,
|
||||
context: "history-view-edit"
|
||||
};
|
||||
|
||||
let vm = mountModelTags(propsData, el);
|
||||
|
||||
// tag icon button open/closes
|
||||
let activator = faIconButton({
|
||||
title: _l("Edit history tags"),
|
||||
classes: "history-tag-btn",
|
||||
faIcon: "fa-tags",
|
||||
tooltipConfig: { placement: "top" }
|
||||
}).appendTo($where.find(".controls .actions"));
|
||||
|
||||
activator.on("click", () => {
|
||||
$(vm.$el).toggleClass("active");
|
||||
});
|
||||
|
||||
return vm;
|
||||
},
|
||||
|
||||
/** render the annotation sub-view controller */
|
||||
_renderAnnotation: function($where) {
|
||||
var panel = this;
|
||||
@@ -244,35 +247,76 @@ var HistoryViewEdit = _super.extend(
|
||||
* ajax calls made for multiple datasets are queued
|
||||
*/
|
||||
multiselectActions: function() {
|
||||
var Galaxy = getGalaxyInstance();
|
||||
var panel = this;
|
||||
|
||||
const batchUpdate = (key, value) => {
|
||||
const actionModels = {};
|
||||
const items = panel
|
||||
.getSelectedModels()
|
||||
.filter(model => {
|
||||
console.log(model.attributes[key]);
|
||||
return model.attributes[key] != value;
|
||||
})
|
||||
.map(model => {
|
||||
const res = {
|
||||
history_content_type: model.attributes.history_content_type,
|
||||
id: model.attributes.id
|
||||
};
|
||||
actionModels[model.id] = model;
|
||||
// Tried "optimistically" deleting the dataset - but it didn't quite work -
|
||||
// maybe history polling restored the previous state?
|
||||
return res;
|
||||
});
|
||||
|
||||
if (items.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {};
|
||||
options.url = `${Galaxy.root}api/histories/${this.model.id}/contents`;
|
||||
options.type = "PUT";
|
||||
const requestData = { items: items };
|
||||
requestData[key] = value;
|
||||
options.data = JSON.stringify(requestData);
|
||||
options.contentType = "application/json";
|
||||
|
||||
var xhr = jQuery.ajax(options);
|
||||
xhr.done((message, status, responseObj) => {
|
||||
for (let updated of message) {
|
||||
const typeId = updated.history_content_type + "-" + updated.id;
|
||||
actionModels[typeId].set(updated);
|
||||
}
|
||||
});
|
||||
xhr.fail((xhr, status, message) => {
|
||||
console.error(message);
|
||||
});
|
||||
return xhr;
|
||||
};
|
||||
|
||||
var actions = [
|
||||
{
|
||||
html: _l("Hide datasets"),
|
||||
func: function() {
|
||||
var action = HDA_MODEL.HistoryDatasetAssociation.prototype.hide;
|
||||
panel.getSelectedModels().ajaxQueue(action);
|
||||
batchUpdate("visible", false);
|
||||
}
|
||||
},
|
||||
{
|
||||
html: _l("Unhide datasets"),
|
||||
func: function() {
|
||||
var action = HDA_MODEL.HistoryDatasetAssociation.prototype.unhide;
|
||||
panel.getSelectedModels().ajaxQueue(action);
|
||||
batchUpdate("visible", true);
|
||||
}
|
||||
},
|
||||
{
|
||||
html: _l("Delete datasets"),
|
||||
func: function() {
|
||||
var action = HDA_MODEL.HistoryDatasetAssociation.prototype["delete"];
|
||||
panel.getSelectedModels().ajaxQueue(action);
|
||||
func: () => {
|
||||
batchUpdate("deleted", true);
|
||||
}
|
||||
},
|
||||
{
|
||||
html: _l("Undelete datasets"),
|
||||
func: function() {
|
||||
var action = HDA_MODEL.HistoryDatasetAssociation.prototype.undelete;
|
||||
panel.getSelectedModels().ajaxQueue(action);
|
||||
batchUpdate("deleted", false);
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -338,7 +382,7 @@ var HistoryViewEdit = _super.extend(
|
||||
} else {
|
||||
console.warn(`Unknown collectionType encountered ${collectionType}`);
|
||||
}
|
||||
createFunc(selection, hideSourceItems).done(() => {
|
||||
createFunc(selection, hideSourceItems).then(() => {
|
||||
panel.model.refresh();
|
||||
});
|
||||
},
|
||||
@@ -418,13 +462,13 @@ var HistoryViewEdit = _super.extend(
|
||||
},
|
||||
|
||||
/** toggle the visibility of each content's tagsEditor applying all the args sent to this function */
|
||||
toggleHDATagEditors: function(showOrHide, speed) {
|
||||
_.each(this.views, view => {
|
||||
if (view.tagsEditor) {
|
||||
view.tagsEditor.toggle(showOrHide, speed);
|
||||
}
|
||||
});
|
||||
},
|
||||
// toggleHDATagEditors: function(showOrHide, speed) {
|
||||
// _.each(this.views, view => {
|
||||
// if (view.tagsEditor) {
|
||||
// view.tagsEditor.toggle(showOrHide, speed);
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
|
||||
/** toggle the visibility of each content's annotationEditor applying all the args sent to this function */
|
||||
toggleHDAAnnotationEditors: function(showOrHide, speed) {
|
||||
|
||||
@@ -28,15 +28,6 @@ var menu = [
|
||||
header: true,
|
||||
anon: true
|
||||
},
|
||||
{
|
||||
html: _l("Create New"),
|
||||
func: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
if (Galaxy && Galaxy.currHistoryPanel) {
|
||||
Galaxy.currHistoryPanel.createNewHistory();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
html: _l("Copy History"),
|
||||
func: function() {
|
||||
@@ -202,16 +193,6 @@ var menu = [
|
||||
html: _l("Export History to File"),
|
||||
href: "history/export_archive?preview=True",
|
||||
anon: true
|
||||
},
|
||||
|
||||
{
|
||||
html: _l("Other Actions"),
|
||||
header: true
|
||||
},
|
||||
{
|
||||
html: _l("Import from File"),
|
||||
href: "histories/import",
|
||||
target: "_top"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -260,8 +241,9 @@ function buildMenu(isAnon, purgeAllowed, urlRoot) {
|
||||
|
||||
if (menuOption.confirm) {
|
||||
menuOption.func = () => {
|
||||
if (confirm(menuOption.confirm) && window.parent.frames && window.parent.frames.galaxy_main) {
|
||||
window.parent.frames.galaxy_main = menuOption.href;
|
||||
const galaxy_main = window.parent.document.getElementById("galaxy_main");
|
||||
if (confirm(menuOption.confirm) && galaxy_main){
|
||||
galaxy_main.src = menuOption.href;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -274,7 +256,6 @@ var create = ($button, options) => {
|
||||
var isAnon = options.anonymous === undefined ? true : options.anonymous;
|
||||
var purgeAllowed = options.purgeAllowed || false;
|
||||
var menu = buildMenu(isAnon, purgeAllowed, getAppRoot());
|
||||
//console.debug( 'menu:', menu );
|
||||
return new PopupMenu($button, menu);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import _l from "utils/localization";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import mod_library_model from "mvc/library/library-model";
|
||||
import mod_utils from "utils/utils";
|
||||
import mod_select from "mvc/ui/ui-select";
|
||||
@@ -29,7 +29,8 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
"click .make-private": "makeDatasetPrivate",
|
||||
"click .remove-restrictions": "removeDatasetRestrictions",
|
||||
"click .toolbtn_save_permissions": "savePermissions",
|
||||
"click .toolbtn_save_modifications": "saveModifications"
|
||||
"click .toolbtn_save_modifications": "saveModifications",
|
||||
"click .toolbtn_detect_datatype": "detectDatatype"
|
||||
},
|
||||
|
||||
// genome select
|
||||
@@ -84,13 +85,13 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(`${response.responseJSON.err_msg} Click this to go back.`, "", {
|
||||
Toast.error(`${response.responseJSON.err_msg} Click this to go back.`, "", {
|
||||
onclick: function() {
|
||||
Galaxy.libraries.library_router.back();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mod_toastr.error("An error occurred. Click this to go back.", "", {
|
||||
Toast.error("An error occurred. Click this to go back.", "", {
|
||||
onclick: function() {
|
||||
Galaxy.libraries.library_router.back();
|
||||
}
|
||||
@@ -114,7 +115,7 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
var self = this;
|
||||
if (!this.options.ldda_id) {
|
||||
this.render();
|
||||
mod_toastr.error("Library dataset version requested but no id provided.");
|
||||
Toast.error("Library dataset version requested but no id provided.");
|
||||
} else {
|
||||
this.ldda = new mod_library_model.Ldda({
|
||||
id: this.options.ldda_id
|
||||
@@ -126,9 +127,9 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -177,7 +178,7 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
.submit()
|
||||
.remove();
|
||||
|
||||
mod_toastr.info("Your download will begin soon.");
|
||||
Toast.info("Your download will begin soon.");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -208,7 +209,7 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
this.histories.fetch({
|
||||
success: function(histories) {
|
||||
if (histories.length === 0) {
|
||||
mod_toastr.warning("You have to create history first. Click this to do so.", "", {
|
||||
Toast.warning("You have to create history first. Click this to do so.", "", {
|
||||
onclick: function() {
|
||||
window.location = getAppRoot();
|
||||
}
|
||||
@@ -219,9 +220,9 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -239,7 +240,7 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
self.processImportToHistory(new_history.id);
|
||||
})
|
||||
.fail((xhr, status, error) => {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
})
|
||||
.always(() => {
|
||||
self.modal.enableButton("Import");
|
||||
@@ -266,7 +267,7 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
{
|
||||
success: function() {
|
||||
Galaxy.modal.hide();
|
||||
mod_toastr.success("Dataset imported. Click this to start analyzing it.", "", {
|
||||
Toast.success("Dataset imported. Click this to start analyzing it.", "", {
|
||||
onclick: function() {
|
||||
window.location = getAppRoot();
|
||||
}
|
||||
@@ -274,9 +275,9 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(`Dataset not imported. ${response.responseJSON.err_msg}`);
|
||||
Toast.error(`Dataset not imported. ${response.responseJSON.err_msg}`);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred. Dataset not imported. Please try again.");
|
||||
Toast.error("An error occurred. Dataset not imported. Please try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,7 +311,7 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
});
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to fetch dataset permissions.");
|
||||
Toast.error("An error occurred while attempting to fetch dataset permissions.");
|
||||
});
|
||||
$('#center [data-toggle="tooltip"]').tooltip({ trigger: "hover" });
|
||||
$("#center").css("overflow", "auto");
|
||||
@@ -398,6 +399,12 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
return select_options;
|
||||
},
|
||||
|
||||
detectDatatype: function(options) {
|
||||
let ld = this.model;
|
||||
ld.set("file_ext", "auto");
|
||||
this._submitModification(ld);
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the changes made to the library dataset.
|
||||
*/
|
||||
@@ -410,7 +417,7 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
ld.set("name", new_name);
|
||||
is_changed = true;
|
||||
} else {
|
||||
mod_toastr.warning("Library dataset name has to be at least 1 character long.");
|
||||
Toast.warning("Library dataset name has to be at least 1 character long.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -434,28 +441,31 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
ld.set("file_ext", new_ext);
|
||||
is_changed = true;
|
||||
}
|
||||
var dataset_view = this;
|
||||
if (is_changed) {
|
||||
ld.save(null, {
|
||||
patch: true,
|
||||
success: function(ld) {
|
||||
dataset_view.render();
|
||||
mod_toastr.success("Changes to library dataset saved.");
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred while attempting to update the library dataset.");
|
||||
}
|
||||
}
|
||||
});
|
||||
this._submitModification(ld);
|
||||
} else {
|
||||
dataset_view.render();
|
||||
mod_toastr.info("Nothing has changed.");
|
||||
this.render();
|
||||
Toast.info("Nothing has changed.");
|
||||
}
|
||||
},
|
||||
|
||||
_submitModification(library_dataset) {
|
||||
library_dataset.save(null, {
|
||||
patch: true,
|
||||
success: library_dataset => {
|
||||
this.render();
|
||||
Toast.success("Changes to library dataset saved.");
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
Toast.error("An error occurred while attempting to update the library dataset.");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
copyToClipboard: function(e) {
|
||||
e.preventDefault();
|
||||
var href = Backbone.history.location.href;
|
||||
@@ -473,10 +483,10 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
self.showPermissions({
|
||||
fetched_permissions: fetched_permissions
|
||||
});
|
||||
mod_toastr.success("The dataset is now private to you.");
|
||||
Toast.success("The dataset is now private to you.");
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to make dataset private.");
|
||||
Toast.error("An error occurred while attempting to make dataset private.");
|
||||
});
|
||||
},
|
||||
|
||||
@@ -488,10 +498,10 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
self.showPermissions({
|
||||
fetched_permissions: fetched_permissions
|
||||
});
|
||||
mod_toastr.success("Access to this dataset is now unrestricted.");
|
||||
Toast.success("Access to this dataset is now unrestricted.");
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to make dataset unrestricted.");
|
||||
Toast.error("An error occurred while attempting to make dataset unrestricted.");
|
||||
});
|
||||
},
|
||||
|
||||
@@ -523,10 +533,10 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
self.showPermissions({
|
||||
fetched_permissions: fetched_permissions
|
||||
});
|
||||
mod_toastr.success("Permissions saved.");
|
||||
Toast.success("Permissions saved.");
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to set dataset permissions.");
|
||||
Toast.error("An error occurred while attempting to set dataset permissions.");
|
||||
});
|
||||
},
|
||||
|
||||
@@ -618,6 +628,10 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
'<span class="fa fa-pencil"></span>',
|
||||
" Modify",
|
||||
"</button>",
|
||||
'<button data-toggle="tooltip" data-placement="top" title="Attempt to detect the format of dataset" class="btn btn-secondary toolbtn_detect_datatype toolbar-item mr-1" type="button">',
|
||||
'<span class="fa fa-undo"></span>',
|
||||
" Auto-detect datatype",
|
||||
"</button>",
|
||||
"<% } %>",
|
||||
'<% if (item.get("can_user_manage")) { %>',
|
||||
'<a href="#folders/<%- item.get("folder_id") %>/datasets/<%- item.id %>/permissions">',
|
||||
|
||||
@@ -3,7 +3,7 @@ import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import mod_library_model from "mvc/library/library-model";
|
||||
import mod_select from "mvc/ui/ui-select";
|
||||
|
||||
@@ -40,13 +40,13 @@ var FolderView = Backbone.View.extend({
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(`${response.responseJSON.err_msg} Click this to go back.`, "", {
|
||||
Toast.error(`${response.responseJSON.err_msg} Click this to go back.`, "", {
|
||||
onclick: function() {
|
||||
Galaxy.libraries.library_router.back();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mod_toastr.error("An error occurred. Click this to go back.", "", {
|
||||
Toast.error("An error occurred. Click this to go back.", "", {
|
||||
onclick: function() {
|
||||
Galaxy.libraries.library_router.back();
|
||||
}
|
||||
@@ -76,7 +76,7 @@ var FolderView = Backbone.View.extend({
|
||||
});
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to fetch folder permissions.");
|
||||
Toast.error("An error occurred while attempting to fetch folder permissions.");
|
||||
});
|
||||
|
||||
$('#center [data-toggle="tooltip"]').tooltip({ trigger: "hover" });
|
||||
@@ -193,10 +193,10 @@ var FolderView = Backbone.View.extend({
|
||||
self.showPermissions({
|
||||
fetched_permissions: fetched_permissions
|
||||
});
|
||||
mod_toastr.success("Permissions saved.");
|
||||
Toast.success("Permissions saved.");
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to set folder permissions.");
|
||||
Toast.error("An error occurred while attempting to set folder permissions.");
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import $ from "jquery";
|
||||
import _ from "underscore";
|
||||
import Backbone from "backbone";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import mod_library_model from "mvc/library/library-model";
|
||||
import mod_library_folderrow_view from "mvc/library/library-folderrow-view";
|
||||
import { getGalaxyInstance } from "app";
|
||||
@@ -79,13 +79,13 @@ var FolderListView = Backbone.View.extend({
|
||||
error: function(model, response) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(`${response.responseJSON.err_msg} Click this to go back.`, "", {
|
||||
Toast.error(`${response.responseJSON.err_msg} Click this to go back.`, "", {
|
||||
onclick: function() {
|
||||
Galaxy.libraries.library_router.back();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mod_toastr.error("An error occurred. Click this to go back.", "", {
|
||||
Toast.error("An error occurred. Click this to go back.", "", {
|
||||
onclick: function() {
|
||||
Galaxy.libraries.library_router.back();
|
||||
}
|
||||
@@ -128,7 +128,7 @@ var FolderListView = Backbone.View.extend({
|
||||
if (row) {
|
||||
row.showDatasetDetails();
|
||||
} else {
|
||||
mod_toastr.error("Requested dataset not found. Showing folder instead.");
|
||||
Toast.error("Requested dataset not found. Showing folder instead.");
|
||||
}
|
||||
} else {
|
||||
if (this.options.show_page === null || this.options.show_page < 1) {
|
||||
@@ -410,8 +410,7 @@ var FolderListView = Backbone.View.extend({
|
||||
"</table>",
|
||||
'<div class="empty-folder-message" style="display:none;">',
|
||||
"This folder is either empty or you do not have proper access permissions to see the contents. If you expected something to show up",
|
||||
' please consult the <a href="https://galaxyproject.org/data-libraries/#permissions" target="_blank">library security wikipage</a>',
|
||||
' or visit the <a href="https://biostar.usegalaxy.org/" target="_blank">Galaxy support site</a>.',
|
||||
' please consult the <a href="https://galaxyproject.org/data-libraries/#permissions" target="_blank">library security wikipage</a>.',
|
||||
"</div>"
|
||||
].join("")
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import mod_library_model from "mvc/library/library-model";
|
||||
import mod_library_dataset_view from "mvc/library/library-dataset-view";
|
||||
|
||||
@@ -125,7 +125,7 @@ var FolderRowView = Backbone.View.extend({
|
||||
var updated_dataset = new mod_library_model.Item(response);
|
||||
Galaxy.libraries.folderListView.collection.add(updated_dataset);
|
||||
Galaxy.libraries.folderListView.collection.sortFolder("name", "asc");
|
||||
mod_toastr.success("Dataset undeleted. Click this to see it.", "", {
|
||||
Toast.success("Dataset undeleted. Click this to see it.", "", {
|
||||
onclick: function() {
|
||||
var folder_id = that.model.get("folder_id");
|
||||
window.location = `${getAppRoot()}library/list#folders/${folder_id}/datasets/${that.id}`;
|
||||
@@ -134,9 +134,9 @@ var FolderRowView = Backbone.View.extend({
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(`Dataset was not undeleted. ${response.responseJSON.err_msg}`);
|
||||
Toast.error(`Dataset was not undeleted. ${response.responseJSON.err_msg}`);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred! Dataset was not undeleted. Please try again.");
|
||||
Toast.error("An error occurred! Dataset was not undeleted. Please try again.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -157,13 +157,13 @@ var FolderRowView = Backbone.View.extend({
|
||||
var updated_folder = new mod_library_model.FolderAsModel(response);
|
||||
Galaxy.libraries.folderListView.collection.add(updated_folder);
|
||||
Galaxy.libraries.folderListView.collection.sortFolder("name", "asc");
|
||||
mod_toastr.success("Folder undeleted.");
|
||||
Toast.success("Folder undeleted.");
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(`Folder was not undeleted. ${response.responseJSON.err_msg}`);
|
||||
Toast.error(`Folder was not undeleted. ${response.responseJSON.err_msg}`);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred! Folder was not undeleted. Please try again.");
|
||||
Toast.error("An error occurred! Folder was not undeleted. Please try again.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -191,7 +191,7 @@ var FolderRowView = Backbone.View.extend({
|
||||
folder.set("name", new_name);
|
||||
is_changed = true;
|
||||
} else {
|
||||
mod_toastr.warning("Folder name has to be at least 3 characters long.");
|
||||
Toast.warning("Folder name has to be at least 3 characters long.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -207,20 +207,20 @@ var FolderRowView = Backbone.View.extend({
|
||||
success: function(folder) {
|
||||
row_view.options.edit_mode = false;
|
||||
row_view.repaint(folder);
|
||||
mod_toastr.success("Changes to folder saved.");
|
||||
Toast.success("Changes to folder saved.");
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred while attempting to update the folder.");
|
||||
Toast.error("An error occurred while attempting to update the folder.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.options.edit_mode = false;
|
||||
this.repaint(folder);
|
||||
mod_toastr.info("Nothing has changed.");
|
||||
Toast.info("Nothing has changed.");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import _l from "utils/localization";
|
||||
import mod_utils from "utils/utils";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import mod_library_model from "mvc/library/library-model";
|
||||
import mod_select from "mvc/ui/ui-select";
|
||||
import LIST_CREATOR from "mvc/collection/list-collection-creator";
|
||||
@@ -190,21 +190,21 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
folder.save(folderDetails, {
|
||||
success: function(folder) {
|
||||
Galaxy.modal.hide();
|
||||
mod_toastr.success("Folder created.");
|
||||
Toast.success("Folder created.");
|
||||
folder.set({ type: "folder" });
|
||||
Galaxy.libraries.folderListView.collection.add(folder);
|
||||
},
|
||||
error: function(model, response) {
|
||||
Galaxy.modal.hide();
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mod_toastr.error("Folder's name is missing.");
|
||||
Toast.error("Folder's name is missing.");
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -226,7 +226,7 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
var $checkedValues = this.findCheckedRows();
|
||||
var template = this.templateImportIntoHistoryModal();
|
||||
if ($checkedValues.length === 0) {
|
||||
mod_toastr.info("You must select some datasets first.");
|
||||
Toast.info("You must select some datasets first.");
|
||||
} else {
|
||||
var promise = this.fetchUserHistories();
|
||||
promise
|
||||
@@ -250,9 +250,9 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
})
|
||||
.fail((model, response) => {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -273,7 +273,7 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
this.processImportToHistory(new_history.id, new_history.name);
|
||||
})
|
||||
.fail((xhr, status, error) => {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
})
|
||||
.always(() => {
|
||||
this.modal.enableButton("Import");
|
||||
@@ -366,9 +366,9 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
.appendTo("body")
|
||||
.submit()
|
||||
.remove();
|
||||
mod_toastr.info("Your download will begin soon.");
|
||||
Toast.info("Your download will begin soon.");
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -405,9 +405,9 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
})
|
||||
.fail((model, response) => {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -589,12 +589,12 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
error: (model, response) => {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
if (response.responseJSON.err_code === 404001) {
|
||||
mod_toastr.warning(response.responseJSON.err_msg);
|
||||
Toast.warning(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
}
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -616,7 +616,7 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
var paths = $("textarea#import_paths").val();
|
||||
var valid_paths = [];
|
||||
if (!paths) {
|
||||
mod_toastr.info("Please enter a path relative to Galaxy root.");
|
||||
Toast.info("Please enter a path relative to Galaxy root.");
|
||||
} else {
|
||||
this.modal.disableButton("Import");
|
||||
paths = paths.split("\n");
|
||||
@@ -706,7 +706,7 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
var selection_type = selected_nodes[0].type;
|
||||
var paths = [];
|
||||
if (selected_nodes.length < 1) {
|
||||
mod_toastr.info("Please select some items first.");
|
||||
Toast.info("Please select some items first.");
|
||||
} else {
|
||||
this.modal.disableButton("Import");
|
||||
for (let i = selected_nodes.length - 1; i >= 0; i--) {
|
||||
@@ -769,9 +769,9 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
},
|
||||
error: (model, response) => {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -786,7 +786,7 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
var history_item_types = [];
|
||||
var items_to_add = [];
|
||||
if (checked_hdas.length < 1) {
|
||||
mod_toastr.info("You must select some datasets first.");
|
||||
Toast.info("You must select some datasets first.");
|
||||
} else {
|
||||
this.modal.disableButton("Add");
|
||||
checked_hdas.each(function() {
|
||||
@@ -831,15 +831,15 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
var popped_item = history_item_set.pop();
|
||||
if (typeof popped_item == "undefined") {
|
||||
if (this.options.chain_call_control.failed_number === 0) {
|
||||
mod_toastr.success("Selected datasets imported into history. Click this to start analyzing it.", "", {
|
||||
Toast.success("Selected datasets imported into history. Click this to start analyzing it.", "", {
|
||||
onclick: () => {
|
||||
window.location = getAppRoot();
|
||||
}
|
||||
});
|
||||
} else if (this.options.chain_call_control.failed_number === this.options.chain_call_control.total_number) {
|
||||
mod_toastr.error("There was an error and no datasets were imported into history.");
|
||||
Toast.error("There was an error and no datasets were imported into history.");
|
||||
} else if (this.options.chain_call_control.failed_number < this.options.chain_call_control.total_number) {
|
||||
mod_toastr.warning(
|
||||
Toast.warning(
|
||||
"Some of the datasets could not be imported into history. Click this to see what was imported.",
|
||||
"",
|
||||
{
|
||||
@@ -879,27 +879,29 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
*/
|
||||
chainCallImportingUserdirFiles: function(options) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
var popped_item = options.paths.pop();
|
||||
let popped_item = options.paths.pop();
|
||||
if (typeof popped_item === "undefined") {
|
||||
if (this.options.chain_call_control.failed_number === 0) {
|
||||
mod_toastr.success("Selected files imported into the current folder");
|
||||
Toast.success("Selected files imported into the current folder");
|
||||
Galaxy.modal.hide();
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var promise = $.when(
|
||||
$.post(
|
||||
`${getAppRoot()}api/libraries/datasets?encoded_folder_id=${this.id}&source=${
|
||||
options.source
|
||||
}&path=${popped_item}&file_type=${options.file_type}&link_data=${options.link_data}&space_to_tab=${
|
||||
options.space_to_tab
|
||||
}&to_posix_lines=${options.to_posix_lines}&dbkey=${options.dbkey}&tag_using_filenames=${
|
||||
options.tag_using_filenames
|
||||
}`
|
||||
)
|
||||
);
|
||||
let post_url = `${getAppRoot()}api/libraries/datasets`;
|
||||
let post_data = {
|
||||
encoded_folder_id: this.id,
|
||||
source: options.source,
|
||||
path: popped_item,
|
||||
file_type: options.file_type,
|
||||
link_data: options.link_data,
|
||||
space_to_tab: options.space_to_tab,
|
||||
to_posix_lines: options.to_posix_lines,
|
||||
dbkey: options.dbkey,
|
||||
tag_using_filenames: options.tag_using_filenames
|
||||
};
|
||||
let promise = $.when($.post(post_url, post_data));
|
||||
promise
|
||||
.done(response => {
|
||||
this.updateProgress();
|
||||
@@ -927,28 +929,31 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
chainCallImportingFolders: function(options) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
// TODO need to check which paths to call
|
||||
var popped_item = options.paths.pop();
|
||||
let popped_item = options.paths.pop();
|
||||
if (typeof popped_item == "undefined") {
|
||||
if (this.options.chain_call_control.failed_number === 0) {
|
||||
mod_toastr.success("Selected folders and their contents imported into the current folder.");
|
||||
Toast.success("Selected folders and their contents imported into the current folder.");
|
||||
Galaxy.modal.hide();
|
||||
} else {
|
||||
// TODO better error report
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var promise = $.when(
|
||||
$.post(
|
||||
`${getAppRoot()}api/libraries/datasets?encoded_folder_id=${this.id}&source=${
|
||||
options.source
|
||||
}&path=${popped_item}&preserve_dirs=${options.preserve_dirs}&link_data=${
|
||||
options.link_data
|
||||
}&to_posix_lines=${options.to_posix_lines}&space_to_tab=${options.space_to_tab}&file_type=${
|
||||
options.file_type
|
||||
}&dbkey=${options.dbkey}&tag_using_filenames=${options.tag_using_filenames}`
|
||||
)
|
||||
);
|
||||
let post_url = `${getAppRoot()}api/libraries/datasets`;
|
||||
let post_data = {
|
||||
encoded_folder_id: this.id,
|
||||
source: options.source,
|
||||
path: popped_item,
|
||||
preserve_dirs: options.preserve_dirs,
|
||||
link_data: options.link_data,
|
||||
to_posix_lines: options.to_posix_lines,
|
||||
space_to_tab: options.space_to_tab,
|
||||
file_type: options.file_type,
|
||||
dbkey: options.dbkey,
|
||||
tag_using_filenames: options.tag_using_filenames
|
||||
};
|
||||
let promise = $.when($.post(post_url, post_data));
|
||||
promise
|
||||
.done(response => {
|
||||
this.updateProgress();
|
||||
@@ -972,11 +977,11 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
var popped_item = hdas_set.pop();
|
||||
if (typeof popped_item == "undefined") {
|
||||
if (this.options.chain_call_control.failed_number === 0) {
|
||||
mod_toastr.success("Selected datasets from history added to the folder");
|
||||
Toast.success("Selected datasets from history added to the folder");
|
||||
} else if (this.options.chain_call_control.failed_number === this.options.chain_call_control.total_number) {
|
||||
mod_toastr.error("There was an error and no datasets were added to the folder.");
|
||||
Toast.error("There was an error and no datasets were added to the folder.");
|
||||
} else if (this.options.chain_call_control.failed_number < this.options.chain_call_control.total_number) {
|
||||
mod_toastr.warning("Some of the datasets could not be added to the folder");
|
||||
Toast.warning("Some of the datasets could not be added to the folder");
|
||||
}
|
||||
Galaxy.modal.hide();
|
||||
return this.added_hdas;
|
||||
@@ -1011,13 +1016,13 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
var item_to_delete = items_to_delete.pop();
|
||||
if (typeof item_to_delete === "undefined") {
|
||||
if (this.options.chain_call_control.failed_number === 0) {
|
||||
mod_toastr.success("Selected items were deleted.");
|
||||
Toast.success("Selected items were deleted.");
|
||||
} else if (this.options.chain_call_control.failed_number === this.options.chain_call_control.total_number) {
|
||||
mod_toastr.error(
|
||||
Toast.error(
|
||||
"There was an error and no items were deleted. Please make sure you have sufficient permissions."
|
||||
);
|
||||
} else if (this.options.chain_call_control.failed_number < this.options.chain_call_control.total_number) {
|
||||
mod_toastr.warning(
|
||||
Toast.warning(
|
||||
"Some of the items could not be deleted. Please make sure you have sufficient permissions."
|
||||
);
|
||||
}
|
||||
@@ -1076,7 +1081,7 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
var folder_ids = [];
|
||||
var $checkedValues = this.findCheckedRows();
|
||||
if ($checkedValues.length === 0) {
|
||||
mod_toastr.info("You must select at least one item for deletion.");
|
||||
Toast.info("You must select at least one item for deletion.");
|
||||
} else {
|
||||
var template = this.templateDeletingItemsProgressBar();
|
||||
this.modal = Galaxy.modal;
|
||||
@@ -1148,9 +1153,9 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1194,7 +1199,7 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
break;
|
||||
default:
|
||||
Galaxy.libraries.library_router.back();
|
||||
mod_toastr.error("Invalid import source.");
|
||||
Toast.error("Invalid import source.");
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -1253,7 +1258,7 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
body: template({ selected_datasets: checked_items.dataset_ids.length }),
|
||||
buttons: {
|
||||
Continue: () => {
|
||||
this.showColectionBuilder(checked_items.dataset_ids);
|
||||
this.showCollectionBuilder(checked_items.dataset_ids);
|
||||
},
|
||||
Close: () => {
|
||||
Galaxy.modal.hide();
|
||||
@@ -1306,7 +1311,12 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
this.collectionType = collectionType;
|
||||
},
|
||||
|
||||
showColectionBuilder: function(checked_items) {
|
||||
/**
|
||||
* Note: The collection creation process expects ldda_ids as ids
|
||||
* in the collection_elements array but we operate on ld_ids in libraries.
|
||||
* The code below overwrites the id with ldda_id for this reason.
|
||||
*/
|
||||
showCollectionBuilder: function(checked_items) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let collection_elements = [];
|
||||
let elements_source = this.modal.$('input[type="radio"]:checked').val();
|
||||
@@ -1314,26 +1324,28 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
for (let i = checked_items.length - 1; i >= 0; i--) {
|
||||
let collection_item = {};
|
||||
let dataset = Galaxy.libraries.folderListView.folder_container.get("folder").get(checked_items[i]);
|
||||
collection_item.id = checked_items[i];
|
||||
collection_item.id = dataset.get("ldda_id");
|
||||
collection_item.name = dataset.get("name");
|
||||
collection_item.deleted = dataset.get("deleted");
|
||||
collection_item.state = dataset.get("state");
|
||||
collection_elements.push(collection_item);
|
||||
}
|
||||
} else if (elements_source === "folder") {
|
||||
collection_elements = new Backbone.Collection(
|
||||
Galaxy.libraries.folderListView.folder_container.get("folder").where({ type: "file" })
|
||||
).toJSON();
|
||||
let all_datasets = Galaxy.libraries.folderListView.folder_container.get("folder").where({ type: "file" });
|
||||
collection_elements = new Backbone.Collection(all_datasets).toJSON();
|
||||
for (var i = collection_elements.length - 1; i >= 0; i--) {
|
||||
collection_elements[i].id = collection_elements[i].ldda_id;
|
||||
}
|
||||
}
|
||||
let new_history_name = this.modal.$("input[name=history_name]").val();
|
||||
if (new_history_name !== "") {
|
||||
this.createNewHistory(new_history_name)
|
||||
.done(new_history => {
|
||||
mod_toastr.success("History created");
|
||||
Toast.success("History created");
|
||||
this.collectionImport(collection_elements, new_history.id, new_history.name);
|
||||
})
|
||||
.fail((xhr, status, error) => {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
});
|
||||
} else {
|
||||
let selected_history_id = this.select_collection_history.value();
|
||||
|
||||
@@ -3,7 +3,7 @@ import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import mod_library_model from "mvc/library/library-model";
|
||||
import mod_select from "mvc/ui/ui-select";
|
||||
|
||||
@@ -40,13 +40,13 @@ var LibraryView = Backbone.View.extend({
|
||||
error: function(model, response) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(`${response.responseJSON.err_msg} Click this to go back.`, "", {
|
||||
Toast.error(`${response.responseJSON.err_msg} Click this to go back.`, "", {
|
||||
onclick: function() {
|
||||
Galaxy.libraries.library_router.back();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mod_toastr.error("An error occurred. Click this to go back.", "", {
|
||||
Toast.error("An error occurred. Click this to go back.", "", {
|
||||
onclick: function() {
|
||||
Galaxy.libraries.library_router.back();
|
||||
}
|
||||
@@ -83,7 +83,7 @@ var LibraryView = Backbone.View.extend({
|
||||
});
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to fetch library permissions.");
|
||||
Toast.error("An error occurred while attempting to fetch library permissions.");
|
||||
});
|
||||
|
||||
$('#center [data-toggle="tooltip"]').tooltip({ trigger: "hover" });
|
||||
@@ -188,10 +188,10 @@ var LibraryView = Backbone.View.extend({
|
||||
self.showPermissions({
|
||||
fetched_permissions: fetched_permissions
|
||||
});
|
||||
mod_toastr.success("The dataset is now private to you.");
|
||||
Toast.success("The dataset is now private to you.");
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to make dataset private.");
|
||||
Toast.error("An error occurred while attempting to make dataset private.");
|
||||
});
|
||||
},
|
||||
|
||||
@@ -203,10 +203,10 @@ var LibraryView = Backbone.View.extend({
|
||||
self.showPermissions({
|
||||
fetched_permissions: fetched_permissions
|
||||
});
|
||||
mod_toastr.success("Access to this dataset is now unrestricted.");
|
||||
Toast.success("Access to this dataset is now unrestricted.");
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to make dataset unrestricted.");
|
||||
Toast.error("An error occurred while attempting to make dataset unrestricted.");
|
||||
});
|
||||
},
|
||||
|
||||
@@ -236,10 +236,10 @@ var LibraryView = Backbone.View.extend({
|
||||
self.showPermissions({
|
||||
fetched_permissions: fetched_permissions
|
||||
});
|
||||
mod_toastr.success("Permissions saved.");
|
||||
Toast.success("Permissions saved.");
|
||||
})
|
||||
.fail(() => {
|
||||
mod_toastr.error("An error occurred while attempting to set library permissions.");
|
||||
Toast.error("An error occurred while attempting to set library permissions.");
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import $ from "jquery";
|
||||
import _ from "libs/underscore";
|
||||
import Backbone from "backbone";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import mod_library_model from "mvc/library/library-model";
|
||||
import mod_library_libraryrow_view from "mvc/library/library-libraryrow-view";
|
||||
|
||||
@@ -37,9 +37,9 @@ var LibraryListView = Backbone.View.extend({
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -147,9 +147,9 @@ var LibraryListView = Backbone.View.extend({
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -240,8 +240,7 @@ var LibraryListView = Backbone.View.extend({
|
||||
"<% } else{ %>",
|
||||
"<div>",
|
||||
"There are no libraries visible to you here. If you expected some to show up please consult the",
|
||||
' <a href="https://galaxyproject.org/data-libraries/#permissions" target="_blank">library security wikipage</a>',
|
||||
' or visit the <a href="https://biostar.usegalaxy.org/" target="_blank">Galaxy support site</a>.',
|
||||
' <a href="https://galaxyproject.org/data-libraries/#permissions" target="_blank">library security wikipage</a>.',
|
||||
"</div>",
|
||||
"<% }%>",
|
||||
"<% } else{ %>",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from "underscore";
|
||||
import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import { getGalaxyInstance } from "app";
|
||||
|
||||
// galaxy library row view
|
||||
@@ -116,7 +116,7 @@ var LibraryRowView = Backbone.View.extend({
|
||||
|
||||
/* User clicked the 'cancel' button so we render normal rowView */
|
||||
cancel_library_modification: function() {
|
||||
// mod_toastr.info('Modifications canceled');
|
||||
// Toast.info('Modifications canceled');
|
||||
this.edit_mode = false;
|
||||
this.repaint();
|
||||
},
|
||||
@@ -132,7 +132,7 @@ var LibraryRowView = Backbone.View.extend({
|
||||
library.set("name", new_name);
|
||||
is_changed = true;
|
||||
} else {
|
||||
mod_toastr.warning("Library name has to be at least 3 characters long.");
|
||||
Toast.warning("Library name has to be at least 3 characters long.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -156,20 +156,20 @@ var LibraryRowView = Backbone.View.extend({
|
||||
success: function(library) {
|
||||
row_view.edit_mode = false;
|
||||
row_view.repaint(library);
|
||||
mod_toastr.success("Changes to library saved.");
|
||||
Toast.success("Changes to library saved.");
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred while attempting to update the library.");
|
||||
Toast.error("An error occurred while attempting to update the library.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.edit_mode = false;
|
||||
this.repaint(library);
|
||||
mod_toastr.info("Nothing has changed.");
|
||||
Toast.info("Nothing has changed.");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -191,13 +191,13 @@ var LibraryRowView = Backbone.View.extend({
|
||||
} else if (Galaxy.libraries.preferences.get("with_deleted") === true) {
|
||||
row_view.repaint(library);
|
||||
}
|
||||
mod_toastr.success("Library has been marked deleted.");
|
||||
Toast.success("Library has been marked deleted.");
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred during deleting the library.");
|
||||
Toast.error("An error occurred during deleting the library.");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -218,13 +218,13 @@ var LibraryRowView = Backbone.View.extend({
|
||||
Galaxy.libraries.libraryListView.collection.add(library);
|
||||
row_view.edit_mode = false;
|
||||
row_view.repaint(library);
|
||||
mod_toastr.success("Library has been undeleted.");
|
||||
Toast.success("Library has been undeleted.");
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred while undeleting the library.");
|
||||
Toast.error("An error occurred while undeleting the library.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from "underscore";
|
||||
import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import _l from "utils/localization";
|
||||
import mod_toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
import mod_library_model from "mvc/library/library-model";
|
||||
import { getGalaxyInstance } from "app";
|
||||
|
||||
@@ -106,18 +106,18 @@ var LibraryToolbarView = Backbone.View.extend({
|
||||
self.modal.hide();
|
||||
self.clearLibraryModal();
|
||||
Galaxy.libraries.libraryListView.render();
|
||||
mod_toastr.success("Library created.");
|
||||
Toast.success("Library created.");
|
||||
},
|
||||
error: function(model, response) {
|
||||
if (typeof response.responseJSON !== "undefined") {
|
||||
mod_toastr.error(response.responseJSON.err_msg);
|
||||
Toast.error(response.responseJSON.err_msg);
|
||||
} else {
|
||||
mod_toastr.error("An error occurred.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mod_toastr.error("Library's name is missing.");
|
||||
Toast.error("Library's name is missing.");
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -120,32 +120,34 @@ export default FormBase.extend({
|
||||
let in_favorites = Galaxy.user.getFavorites().tools.indexOf(options.id) >= 0;
|
||||
var favorite_button = new Ui.Button({
|
||||
icon: "fa-star-o",
|
||||
title: "Favorite",
|
||||
title: options.narrow ? null : "Favorite",
|
||||
tooltip: "Add to favorites",
|
||||
visible: !Galaxy.user.isAnonymous() && !in_favorites,
|
||||
onclick: () => {
|
||||
axios
|
||||
.put(`${Galaxy.root}api/users/${Galaxy.user.id}/favorites/tools`, {'object_id': options.id})
|
||||
.put(`${Galaxy.root}api/users/${Galaxy.user.id}/favorites/tools`, { object_id: options.id })
|
||||
.then(response => {
|
||||
favorite_button.hide();
|
||||
remove_favorite_button.show();
|
||||
Galaxy.user.updateFavorites("tools", response.data)
|
||||
Galaxy.user.updateFavorites("tools", response.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var remove_favorite_button = new Ui.Button({
|
||||
icon: "fa-star",
|
||||
title: "Added",
|
||||
title: options.narrow ? null : "Added",
|
||||
tooltip: "Remove from favorites",
|
||||
visible: !Galaxy.user.isAnonymous() && in_favorites,
|
||||
onclick: () => {
|
||||
axios
|
||||
.delete(`${Galaxy.root}api/users/${Galaxy.user.id}/favorites/tools/${encodeURIComponent(options.id)}`)
|
||||
.delete(
|
||||
`${Galaxy.root}api/users/${Galaxy.user.id}/favorites/tools/${encodeURIComponent(options.id)}`
|
||||
)
|
||||
.then(response => {
|
||||
remove_favorite_button.hide();
|
||||
favorite_button.show();
|
||||
Galaxy.user.updateFavorites("tools", response.data)
|
||||
Galaxy.user.updateFavorites("tools", response.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -153,7 +155,7 @@ export default FormBase.extend({
|
||||
// button for version selection
|
||||
var versions_button = new Ui.ButtonMenu({
|
||||
icon: "fa-cubes",
|
||||
title: (!options.narrow && "Versions") || null,
|
||||
title: options.narrow ? null : "Versions",
|
||||
tooltip: "Select another tool version"
|
||||
});
|
||||
|
||||
@@ -182,25 +184,9 @@ export default FormBase.extend({
|
||||
var menu_button = new Ui.ButtonMenu({
|
||||
id: "options",
|
||||
icon: "fa-caret-down",
|
||||
title: (!options.narrow && "Options") || null,
|
||||
title: options.narrow ? null : "Options",
|
||||
tooltip: "View available options"
|
||||
});
|
||||
if (options.biostar_url) {
|
||||
menu_button.addMenu({
|
||||
icon: "fa-question-circle",
|
||||
title: "Question?",
|
||||
onclick: function() {
|
||||
window.open(`${options.biostar_url}/p/new/post/`);
|
||||
}
|
||||
});
|
||||
menu_button.addMenu({
|
||||
icon: "fa-search",
|
||||
title: _l("Search"),
|
||||
onclick: function() {
|
||||
window.open(`${options.biostar_url}/local/search/page/?q=${options.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
menu_button.addMenu({
|
||||
icon: "fa-share",
|
||||
title: _l("Share"),
|
||||
@@ -305,8 +291,7 @@ export default FormBase.extend({
|
||||
/** Templates */
|
||||
_templateHelp: function(options) {
|
||||
var $tmpl = $("<div/>")
|
||||
.addClass("form-help")
|
||||
.addClass("form-text")
|
||||
.addClass("form-help form-text mt-4")
|
||||
.append(options.help);
|
||||
$tmpl.find("a").attr("target", "_blank");
|
||||
$tmpl.find("img").each(function() {
|
||||
|
||||
@@ -222,6 +222,7 @@ var View = Backbone.View.extend({
|
||||
_renderHeader: function() {
|
||||
var self = this;
|
||||
this.execute_btn = new Ui.Button({
|
||||
id: "run-workflow",
|
||||
icon: "fa-check",
|
||||
title: _l("Run workflow"),
|
||||
cls: "btn btn-primary",
|
||||
@@ -233,7 +234,7 @@ var View = Backbone.View.extend({
|
||||
.addClass("h4")
|
||||
.empty()
|
||||
.append(`<b>Workflow: ${this.model.get("name")}<b>`)
|
||||
.append(this.execute_btn.$el.addClass("float-right mt-3"));
|
||||
.append(this.execute_btn.$el);
|
||||
},
|
||||
|
||||
/** Render message */
|
||||
|
||||
@@ -344,7 +344,7 @@ _.extend(ToolSection.prototype, VisibilityMixin);
|
||||
* query.
|
||||
*/
|
||||
var ToolSearch = Backbone.Model.extend({
|
||||
SEARCH_RESERVED_TERMS_FAVORITES: ['#favs', '#favorites', '#favourites'],
|
||||
SEARCH_RESERVED_TERMS_FAVORITES: ["#favs", "#favorites", "#favourites"],
|
||||
|
||||
defaults: {
|
||||
search_hint_string: "search tools",
|
||||
@@ -383,9 +383,9 @@ var ToolSearch = Backbone.Model.extend({
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
// Catch reserved words
|
||||
if (this.SEARCH_RESERVED_TERMS_FAVORITES.indexOf(q) >= 0){
|
||||
if (this.SEARCH_RESERVED_TERMS_FAVORITES.indexOf(q) >= 0) {
|
||||
this.set("results", Galaxy.user.getFavorites().tools);
|
||||
} else{
|
||||
} else {
|
||||
// Start a new ajax-request in X ms
|
||||
$("#search-clear-btn").hide();
|
||||
$("#search-spinner").show();
|
||||
|
||||
@@ -87,7 +87,7 @@ var ToolShedCategories = Backbone.View.extend({
|
||||
"<% _.each(categories, function(category) { %>",
|
||||
"<tr>",
|
||||
"<td>",
|
||||
'<a href="#/category/s/<%= tool_shed %>/c/<%= category.get("id") %>"><%= category.get("name") %></a>',
|
||||
'<a href="#/category/s/<%= tool_shed %>/c/<%= category.get("id") %>/k/name/p/1/t/asc"><%= category.get("name") %></a>',
|
||||
"</td>",
|
||||
'<td><%= category.get("description") %></td>',
|
||||
'<td><%= category.get("repositories") %></td>',
|
||||
|
||||
@@ -10,23 +10,56 @@ var ToolShedCategoryContentsView = Backbone.View.extend({
|
||||
el: "#center",
|
||||
|
||||
initialize: function(params) {
|
||||
this.params = params;
|
||||
this.model = new toolshed_model.CategoryCollection();
|
||||
this.listenTo(this.model, "sync", this.render);
|
||||
var shed = params.tool_shed.replace(/\//g, "%2f");
|
||||
this.model.url += `?tool_shed_url=${shed}&category_id=${params.category_id}`;
|
||||
this.model.url += `?tool_shed_url=${shed}&category_id=${params.category_id}&sort_key=${
|
||||
params.sort_key
|
||||
}&sort_order=${params.sort_order}&page=${params.page}`;
|
||||
this.model.tool_shed = shed;
|
||||
this.model.category = params.category_id;
|
||||
this.model.fetch();
|
||||
},
|
||||
|
||||
render: function(options) {
|
||||
this.options = _.extend(this.options, options);
|
||||
this.options = _.defaults(this.options || {}, options);
|
||||
var category_contents_template = this.templateCategoryContents;
|
||||
var page_navigation_template = this.templatePageNavigation;
|
||||
var sorting = {
|
||||
class: { owner: "fa-sort", description: "fa-sort", name: "fa-sort" },
|
||||
direction: { owner: "asc", description: "asc", name: "asc" }
|
||||
};
|
||||
sorting.class[this.params.sort_key] = this.params.sort_order == "desc" ? "fa-sort-down" : "fa-sort-up";
|
||||
sorting.direction[this.params.sort_key] = this.params.sort_order == "asc" ? "desc" : "asc";
|
||||
var previous_page = Math.max(this.params.page - 1, 1);
|
||||
var pages = parseInt(this.model.models[0].get("repository_count") % 25) + 1;
|
||||
var next_page = Math.min(parseInt(this.params.page) + 1, pages);
|
||||
var pagination_params = {
|
||||
page: this.params.page,
|
||||
next: next_page,
|
||||
previous: previous_page,
|
||||
pages: pages
|
||||
};
|
||||
if (pages > 5) {
|
||||
var page = parseInt(this.params.page);
|
||||
var page_slice = Array();
|
||||
var slice_start = Math.max(page - 2, 2);
|
||||
var i = slice_start;
|
||||
var slice_end = Math.min(Math.min(page + 2, slice_start + 4), pages);
|
||||
for (i = slice_start; i <= slice_end; i++) {
|
||||
page_slice.push(i);
|
||||
}
|
||||
pagination_params["page_slice"] = page_slice;
|
||||
}
|
||||
this.$el.html(
|
||||
category_contents_template({
|
||||
page_navigation: page_navigation_template(pagination_params),
|
||||
category: this.model.models[0],
|
||||
tool_shed: this.model.tool_shed,
|
||||
queue: toolshed_util.queueLength()
|
||||
queue: toolshed_util.queueLength(),
|
||||
sorting: sorting,
|
||||
page: this.params.page
|
||||
})
|
||||
);
|
||||
$("#center").css("overflow", "auto");
|
||||
@@ -57,13 +90,89 @@ var ToolShedCategoryContentsView = Backbone.View.extend({
|
||||
});
|
||||
}
|
||||
});
|
||||
$(".fa-fw").on("click", ev => {
|
||||
var parameters = {};
|
||||
parameters.field = $(ev.target).attr("data-field");
|
||||
parameters.direction = $(ev.target).attr("data-direction");
|
||||
var new_route = `category/s/${this.model.tool_shed}/c/${this.model.category}/k/${parameters.field}/p/${
|
||||
this.params.page
|
||||
}/t/${parameters.direction}`;
|
||||
Backbone.history.navigate(new_route, {
|
||||
trigger: true,
|
||||
replace: true
|
||||
});
|
||||
});
|
||||
$(".pagenav").on("click", ev => {
|
||||
var page = $(ev.target).attr("data-page");
|
||||
var new_route = `category/s/${this.model.tool_shed}/c/${this.model.category}/k/${
|
||||
this.params.sort_key
|
||||
}/p/${page}/t/${this.params.sort_order}`;
|
||||
Backbone.history.navigate(new_route, {
|
||||
trigger: true,
|
||||
replace: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
templatePageNavigation: _.template(
|
||||
[
|
||||
'<div class="navigation">',
|
||||
"<% if (page != 1) { %>",
|
||||
'<a data-page="1" class="pagenav fa fa-fast-backward" />',
|
||||
'<a data-page="<%= previous %>" class="pagenav fa fa-step-backward" />',
|
||||
"<% } else { %>",
|
||||
'<a data-page="1" class="pagenav-inactive fa fa-fast-backward" />',
|
||||
'<a data-page="<%= previous %>" class="pagenav-inactive fa fa-step-backward" />',
|
||||
"<% } %>",
|
||||
"<% if (pages > 5) { %>",
|
||||
"<% if (page != 1) { %>",
|
||||
'<a data-page="1" class="pagenav fa"><a>1</a>',
|
||||
"<% if (page != 2) { %>",
|
||||
'<a class="fa">…</a>',
|
||||
"<% } %>",
|
||||
"<% } else { %>",
|
||||
'<a data-page="1" class="pagenav-inactive fa"><a>1</a>',
|
||||
"<% } %>",
|
||||
"<% _.each(page_slice, function(i) { %>",
|
||||
"<% if (i == page) { %>",
|
||||
'<a data-page="<%= i %>" class="fa"><strong><%= i %></strong></a>',
|
||||
"<% } else { %>",
|
||||
'<a data-page="<%= i %>" class="pagenav fa"><%= i %></a>',
|
||||
"<% } %>",
|
||||
"<% }); %>",
|
||||
"<% var last_pages = [pages - 2, pages - 1, pages]; %>",
|
||||
"<% if (last_pages.indexOf(parseInt(page)) == -1) { %>",
|
||||
'<a class="fa">…</a>',
|
||||
'<a data-page="<%= pages %>" class="pagenav fa"><%= pages %></a>',
|
||||
"<% } %>",
|
||||
"<% } else { %>",
|
||||
"<% for (i = 1; i <= pages; i++) { %>",
|
||||
"<% if (i == page) { %>",
|
||||
'<a data-page="<%= i %>" class="fa"><strong><%= i %></strong></a>',
|
||||
"<% } else { %>",
|
||||
'<a data-page="<%= i %>" class="pagenav fa"><%= i %></a>',
|
||||
"<% } %>",
|
||||
"<% } %>",
|
||||
"<% } %>",
|
||||
"<% if (page < pages) { %>",
|
||||
'<a data-page="<%= next %>" class="pagenav fa fa-step-forward" />',
|
||||
'<a data-page="<%= pages %>" class="pagenav fa fa-fast-forward" />',
|
||||
"<% } else { %>",
|
||||
'<a data-page="<%= next %>" class="pagenav-inactive fa fa-step-forward" />',
|
||||
'<a data-page="<%= pages %>" class="pagenav-inactive fa fa-fast-forward" />',
|
||||
"<% } %>",
|
||||
"</div>"
|
||||
].join("")
|
||||
),
|
||||
|
||||
templateCategoryContents: _.template(
|
||||
[
|
||||
'<style type="text/css">',
|
||||
".ui-autocomplete { background-color: #fff; }",
|
||||
"li.ui-menu-item { list-style-type: none; }",
|
||||
"div.navigation { width: 100%; text-align: center; }",
|
||||
"div.navigation a { margin-left: 0.5em; margin-right: 0.5em; display: inline; text-decoration: underline; }",
|
||||
"a.pagenav-inactive { opacity: 0.5; }",
|
||||
"</style>",
|
||||
'<div class="unified-panel-header" id="panel_header" unselectable="on">',
|
||||
'<div class="unified-panel-header-inner">Repositories in <%= category.get("name") %><a class="ml-auto" href="#/queue">Repository Queue (<%= queue %>)</a></div>',
|
||||
@@ -74,13 +183,16 @@ var ToolShedCategoryContentsView = Backbone.View.extend({
|
||||
'<input class="search-box-input" id="search_box" name="search" data-shedurl="<%= tool_shed.replace(/%2f/g, "/") %>" placeholder="Search repositories by name or id" size="60" type="text" />',
|
||||
"</span>",
|
||||
"</div>",
|
||||
"<% if (category.get('repository_count') > 25) { %>",
|
||||
"<%= page_navigation %>",
|
||||
"<% } %>",
|
||||
'<div style="clear: both; margin-top: 1em;">',
|
||||
'<table class="grid">',
|
||||
'<table class="grid table-striped">',
|
||||
'<thead id="grid-table-header">',
|
||||
"<tr>",
|
||||
'<th style="width: 10%;">Owner</th>',
|
||||
'<th style="width: 15%;">Name</th>',
|
||||
"<th>Synopsis</th>",
|
||||
'<th style="width: 10%;"><a class="fa fa-fw <%= sorting.class.owner %>" data-direction="<%= sorting.direction.owner %>" data-field="owner">Owner</a></th>',
|
||||
'<th style="width: 15%;"><a class="fa fa-fw <%= sorting.class.name %>" data-direction="<%= sorting.direction.name %>" data-field="name">Name</a></th>',
|
||||
'<th><a class="fa fa-fw <%= sorting.class.description %>" data-direction="<%= sorting.direction.description %>" data-field="description">Synopsis</a></th>',
|
||||
'<th style="width: 10%;">Type</th>',
|
||||
"</tr>",
|
||||
"</thead>",
|
||||
|
||||
@@ -14,6 +14,9 @@ import "libs/bootstrap-tour";
|
||||
// bootstrap-tour configures a window.Tour object; keep a local ref.
|
||||
let Tour = window.Tour;
|
||||
|
||||
// For buttons to show, we need to add them to bootstrap/popper's sanitizing filters.
|
||||
$.fn.tooltip.Constructor.Default.whiteList.button = ["data-role"];
|
||||
|
||||
var gxy_root = getAppRoot();
|
||||
|
||||
const TOURPAGE_TEMPLATE = `
|
||||
@@ -24,23 +27,19 @@ const TOURPAGE_TEMPLATE = `
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 btn-group" role="group" aria-label="Tag selector">
|
||||
<% _.each(tourtagorder, function(tag) { %>
|
||||
<button class="btn btn-primary tag-selector-button" tag-selector-button="<%- tag %>">
|
||||
<%- tag %>
|
||||
<button class="btn btn-primary tag-selector-button" tag-selector-button="<%- tag.key %>">
|
||||
<%- tag.name %>
|
||||
</button>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% _.each(tourtagorder, function(tourtagkey) { %>
|
||||
<div tag="<%- tourtagkey %>" class="row mb-3">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<% var tourtag = tourtags[tourtagkey]; %>
|
||||
<h4>
|
||||
<%- tourtag.name %>
|
||||
</h4>
|
||||
<h4>Tours</h4>
|
||||
<ul class="list-group">
|
||||
<% _.each(tourtag.tours, function(tour) { %>
|
||||
<li class="list-group-item">
|
||||
<% _.each(tours, function(tour) { %>
|
||||
<li class="list-group-item" tags="<%- tour.attributes.tags_lc %>">
|
||||
<a href="/tours/<%- tour.id %>" class="tourItem" data-tour.id=<%- tour.id %>>
|
||||
<%- tour.attributes.name || tour.id %>
|
||||
</a>
|
||||
@@ -54,8 +53,7 @@ const TOURPAGE_TEMPLATE = `
|
||||
<% }); %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>`;
|
||||
</div>`;
|
||||
|
||||
var tour_opts = {
|
||||
storage: window.sessionStorage,
|
||||
@@ -139,22 +137,28 @@ export var ToursView = Backbone.View.extend({
|
||||
|
||||
var tourtags = {};
|
||||
_.each(this.model.models, tour => {
|
||||
tour.attributes.tags_lc = [];
|
||||
if (tour.attributes.tags === null) {
|
||||
if (tourtags.Untagged === undefined) {
|
||||
tourtags.Untagged = { name: "Untagged", tours: [] };
|
||||
}
|
||||
tourtags.Untagged.tours.push(tour);
|
||||
} else {
|
||||
_.each(tour.attributes.tags, tag => {
|
||||
tag = tag.charAt(0).toUpperCase() + tag.slice(1);
|
||||
_.each(tour.attributes.tags, otag => {
|
||||
var tag = otag.charAt(0).toUpperCase() + otag.slice(1);
|
||||
if (tourtags[tag] === undefined) {
|
||||
tourtags[tag] = { name: tag, tours: [] };
|
||||
}
|
||||
tour.attributes.tags_lc.push(otag.toLowerCase());
|
||||
tourtags[tag].tours.push(tour);
|
||||
});
|
||||
}
|
||||
});
|
||||
var tourtagorder = Object.keys(tourtags).sort();
|
||||
//var tourtagorder = Object.keys(tourtags).sort();
|
||||
var tourtagorder = [];
|
||||
Object.keys(tourtags).forEach(function(tag, index) {
|
||||
tourtagorder.push({ name: tag, key: tag.toLowerCase() });
|
||||
});
|
||||
|
||||
this.$el
|
||||
.html(
|
||||
@@ -170,16 +174,33 @@ export var ToursView = Backbone.View.extend({
|
||||
})
|
||||
.on("click", ".tag-selector-button", e => {
|
||||
var elem = $(e.target);
|
||||
var display = "block";
|
||||
var tag = elem.attr("tag-selector-button");
|
||||
var active_tags = [];
|
||||
|
||||
// Switch classes for the buttons
|
||||
elem.toggleClass("btn-primary");
|
||||
elem.toggleClass("btn-secondary");
|
||||
|
||||
if (elem.hasClass("btn-secondary")) {
|
||||
display = "none";
|
||||
}
|
||||
$(`div[tag='${tag}']`).css({ display: display });
|
||||
// Get all non-disabled tags
|
||||
$(`.tag-selector-button.btn-primary`).each(function() {
|
||||
active_tags.push($(this).attr("tag-selector-button"));
|
||||
});
|
||||
|
||||
// Loop over all list items, subsequently determine these are
|
||||
// only the tours (tags should be unique). Then use the non-disabled tags to
|
||||
// determien whether or not to display this specific tour.
|
||||
$(`li.list-group-item`).each(function() {
|
||||
if ($(this).attr("tags")) {
|
||||
var tour_tags = [];
|
||||
var tour_tags_html = $(this).attr("tags");
|
||||
|
||||
tour_tags = tour_tags_html.split(",");
|
||||
var fil_tour_tags = tour_tags.filter(function(tag) {
|
||||
return active_tags.indexOf(tag.toLowerCase()) > -1;
|
||||
});
|
||||
|
||||
$(this).css("display", fil_tour_tags.length > 0 ? "block" : "none");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -130,18 +130,20 @@ var PopupMenu = Backbone.View.extend({
|
||||
// function to close popup and unbind itself
|
||||
function closePopup(event) {
|
||||
$(document).off("click.close_popup");
|
||||
if (window && window.parent !== window) {
|
||||
try {
|
||||
try {
|
||||
if (window && window.parent !== window) {
|
||||
$(window.parent.document).off("click.close_popup");
|
||||
} catch (err) {
|
||||
console.debug(err);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
} else {
|
||||
$("iframe#galaxy_main")
|
||||
.contents()
|
||||
.off("click.close_popup");
|
||||
} catch (err) {
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException) {
|
||||
console.debug(
|
||||
"Error clearing parent popups, likely cross-origin frame access from the toolshed and not problematic."
|
||||
);
|
||||
} else {
|
||||
console.debug(err);
|
||||
}
|
||||
}
|
||||
@@ -149,20 +151,22 @@ var PopupMenu = Backbone.View.extend({
|
||||
}
|
||||
|
||||
$("html").one("click.close_popup", closePopup);
|
||||
if (window && window.parent !== window) {
|
||||
try {
|
||||
try {
|
||||
if (window && window.parent !== window) {
|
||||
$(window.parent.document)
|
||||
.find("html")
|
||||
.one("click.close_popup", closePopup);
|
||||
} catch (err) {
|
||||
console.debug(err);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
} else {
|
||||
$("iframe#galaxy_main")
|
||||
.contents()
|
||||
.one("click.close_popup", closePopup);
|
||||
} catch (err) {
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException) {
|
||||
console.debug(
|
||||
"Error clearing parent popups, likely cross-origin frame access from the toolshed and not problematic."
|
||||
);
|
||||
} else {
|
||||
console.debug(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export default Backbone.View.extend({
|
||||
|
||||
/** Content template */
|
||||
_templateContent: function() {
|
||||
return '<div class="ui-color-picker-content">' + '<div class="line"/>' + "</div>";
|
||||
return '<div class="ui-color-picker-content"><div class="line"/></div>';
|
||||
},
|
||||
|
||||
/** Box template */
|
||||
|
||||
@@ -12,6 +12,7 @@ var Base = Backbone.View.extend({
|
||||
(options && options.model) ||
|
||||
new Backbone.Model({
|
||||
visible: true,
|
||||
cls: null,
|
||||
data: [],
|
||||
id: Utils.uid(),
|
||||
error_text: "No options available.",
|
||||
@@ -36,6 +37,7 @@ var Base = Backbone.View.extend({
|
||||
.empty()
|
||||
.removeClass()
|
||||
.addClass("ui-options")
|
||||
.addClass(this.model.get("cls"))
|
||||
.append((this.$message = $("<div/>").addClass("mt-2")))
|
||||
.append((this.$menu = $("<div/>").addClass("ui-options-menu")))
|
||||
.append((this.$options = $(this._template())));
|
||||
@@ -154,6 +156,16 @@ var Base = Backbone.View.extend({
|
||||
return this.$(".ui-option").length;
|
||||
},
|
||||
|
||||
/** Shows the options */
|
||||
show: function() {
|
||||
this.model.set("visible", true);
|
||||
},
|
||||
|
||||
/** Hides the options */
|
||||
hide: function() {
|
||||
this.model.set("visible", false);
|
||||
},
|
||||
|
||||
/** Set value to dom */
|
||||
_setValue: function(new_value) {
|
||||
var self = this;
|
||||
@@ -260,7 +272,7 @@ RadioButton.View = Base.extend({
|
||||
|
||||
/** Template for a single option */
|
||||
_templateOption: function(pair) {
|
||||
var $el = $("<label/>").addClass("btn btn-secondary");
|
||||
var $el = $("<label/>").addClass("btn btn-secondary m-0");
|
||||
if (pair.icon) {
|
||||
$el.append(
|
||||
$("<i/>")
|
||||
@@ -286,7 +298,7 @@ RadioButton.View = Base.extend({
|
||||
|
||||
/** Main template function */
|
||||
_template: function() {
|
||||
return $("<div/>").addClass("btn-group ui-radiobutton");
|
||||
return $("<div/>").addClass("btn-group ui-radiobutton d-flex");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -149,28 +149,11 @@ var View = Backbone.View.extend({
|
||||
}
|
||||
]
|
||||
});
|
||||
var $batch_div = $("<div/>")
|
||||
.addClass("form-text text-muted")
|
||||
.append($("<i/>").addClass("fa fa-sitemap"))
|
||||
.append(
|
||||
$("<span/>").html(
|
||||
"This is a batch mode input field. Separate jobs will be triggered for each dataset selection."
|
||||
)
|
||||
);
|
||||
this.$batch = {
|
||||
linked: $batch_div.clone(),
|
||||
enabled: $batch_div
|
||||
linked: $(this._templateBatch()).clone(),
|
||||
enabled: $(this._templateBatch())
|
||||
.clone()
|
||||
.append(
|
||||
$("<div/>")
|
||||
.append(
|
||||
$("<div/>")
|
||||
.addClass("ui-form-title")
|
||||
.html("Batch options:")
|
||||
)
|
||||
.append(this.button_product.$el)
|
||||
)
|
||||
.append($("<div/>").css("clear", "both"))
|
||||
.append(this.button_product.$el)
|
||||
};
|
||||
|
||||
// add drag-drop event handlers
|
||||
@@ -186,7 +169,13 @@ var View = Backbone.View.extend({
|
||||
this.lastenter === e.target && self.$el.removeClass("ui-dragover");
|
||||
});
|
||||
element.addEventListener("drop", e => {
|
||||
self._handleDrop(e);
|
||||
e.preventDefault();
|
||||
try {
|
||||
let drop_data = JSON.parse(e.dataTransfer.getData("text"))[0];
|
||||
this._handleDropValues(drop_data);
|
||||
} catch (e) {
|
||||
this._handleDropStatus("danger");
|
||||
}
|
||||
});
|
||||
|
||||
// track current history elements
|
||||
@@ -234,7 +223,7 @@ var View = Backbone.View.extend({
|
||||
|
||||
/** Return the currently selected dataset values */
|
||||
value: function(new_value) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let galaxy = getGalaxyInstance();
|
||||
new_value !== undefined && this.model.set("value", new_value);
|
||||
var current = this.model.get("current");
|
||||
if (this.config[current]) {
|
||||
@@ -248,7 +237,7 @@ var View = Backbone.View.extend({
|
||||
if (details) {
|
||||
result.values.push(details);
|
||||
} else {
|
||||
Galaxy.emit.debug(
|
||||
galaxy.emit.debug(
|
||||
"ui-select-content::value()",
|
||||
`Requested details not found for '${id_list[i]}'.`
|
||||
);
|
||||
@@ -260,7 +249,7 @@ var View = Backbone.View.extend({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Galaxy.emit.debug("ui-select-content::value()", `Invalid value/source '${new_value}'.`);
|
||||
galaxy.emit.debug("ui-select-content::value()", `Invalid value/source '${new_value}'.`);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -269,22 +258,37 @@ var View = Backbone.View.extend({
|
||||
_changeCurrent: function() {
|
||||
var self = this;
|
||||
_.each(this.fields, (field, i) => {
|
||||
let cnf = self.config[i];
|
||||
if (self.model.get("current") == i) {
|
||||
field.$el.show();
|
||||
_.each(self.$batch, ($batchfield, batchmode) => {
|
||||
$batchfield[self.config[i].batch == batchmode ? "show" : "hide"]();
|
||||
if (cnf.batch == batchmode) {
|
||||
$batchfield.show();
|
||||
} else {
|
||||
$batchfield.hide();
|
||||
}
|
||||
});
|
||||
if (cnf.showdialog) {
|
||||
self.button_dialog.show();
|
||||
} else {
|
||||
self.button_dialog.hide();
|
||||
}
|
||||
self.button_type.value(i);
|
||||
} else {
|
||||
field.$el.hide();
|
||||
}
|
||||
});
|
||||
if (this.fields.length > 1) {
|
||||
this.button_type.show();
|
||||
} else {
|
||||
this.button_type.hide();
|
||||
}
|
||||
},
|
||||
|
||||
/** Change of type */
|
||||
_changeType: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
var self = this;
|
||||
let self = this;
|
||||
let galaxy = getGalaxyInstance();
|
||||
|
||||
// identify selector type identifier i.e. [ flavor ]_[ type ]_[ multiple ]
|
||||
var config_id =
|
||||
@@ -295,7 +299,7 @@ var View = Backbone.View.extend({
|
||||
this.config = Configurations[config_id];
|
||||
} else {
|
||||
this.config = Configurations["data"];
|
||||
Galaxy.emit.debug("ui-select-content::_changeType()", `Invalid configuration/type id '${config_id}'.`);
|
||||
galaxy.emit.debug("ui-select-content::_changeType()", `Invalid configuration/type id '${config_id}'.`);
|
||||
}
|
||||
|
||||
// prepare extension component of error message
|
||||
@@ -303,7 +307,7 @@ var View = Backbone.View.extend({
|
||||
var extensions = Utils.textify(this.model.get("extensions"));
|
||||
var src_labels = this.model.get("src_labels");
|
||||
|
||||
// build views
|
||||
// build radio button for data selectors
|
||||
this.fields = [];
|
||||
this.button_data = [];
|
||||
_.each(this.config, (c, i) => {
|
||||
@@ -329,24 +333,47 @@ var View = Backbone.View.extend({
|
||||
this.button_type = new Ui.RadioButton.View({
|
||||
value: this.model.get("current"),
|
||||
data: this.button_data,
|
||||
cls: "mr-2",
|
||||
onchange: function(value) {
|
||||
self.model.set("current", value);
|
||||
self.trigger("change");
|
||||
}
|
||||
});
|
||||
|
||||
// build data dialog button
|
||||
this.button_dialog = new Ui.Button({
|
||||
icon: "fa-folder-open-o",
|
||||
tooltip: "Browse Datasets",
|
||||
cls: "ml-2",
|
||||
onclick: () => {
|
||||
let current = this.model.get("current");
|
||||
let cnf = this.config[current];
|
||||
galaxy.data.dialog(
|
||||
response => {
|
||||
this._handleDropValues(response, false);
|
||||
},
|
||||
{
|
||||
multiple: cnf.multiple,
|
||||
format: null,
|
||||
library: false
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// append views
|
||||
this.$el.empty();
|
||||
var button_width = 0;
|
||||
if (this.fields.length > 1) {
|
||||
this.$el.append(this.button_type.$el);
|
||||
button_width = `${Math.max(0, this.fields.length * 40)}px`;
|
||||
}
|
||||
let $fields = $("<div/>").addClass("w-100");
|
||||
this.$el
|
||||
.empty()
|
||||
.addClass("d-flex flex-row")
|
||||
.append($("<div/>").append(this.button_type.$el))
|
||||
.append($fields)
|
||||
.append($("<div/>").append(this.button_dialog.$el));
|
||||
_.each(this.fields, field => {
|
||||
self.$el.append(field.$el.css({ "margin-left": button_width }));
|
||||
$fields.append(field.$el);
|
||||
});
|
||||
_.each(this.$batch, ($batchfield, batchmode) => {
|
||||
self.$el.append($batchfield.css({ "margin-left": button_width }));
|
||||
$fields.append($batchfield);
|
||||
});
|
||||
this.model.set("current", 0);
|
||||
this._changeCurrent();
|
||||
@@ -412,52 +439,62 @@ var View = Backbone.View.extend({
|
||||
}
|
||||
},
|
||||
|
||||
/** Handles drop events e.g. from history panel */
|
||||
_handleDrop: function(ev) {
|
||||
try {
|
||||
var data = this.model.get("data");
|
||||
var current = this.model.get("current");
|
||||
var config = this.config[current];
|
||||
var field = this.fields[current];
|
||||
var drop_data = JSON.parse(ev.dataTransfer.getData("text"))[0];
|
||||
var new_id = drop_data.id;
|
||||
var new_src = drop_data.history_content_type == "dataset_collection" ? "hdca" : "hda";
|
||||
var new_value = { id: new_id, src: new_src };
|
||||
if (data && drop_data.history_id) {
|
||||
if (!_.findWhere(data[new_src], new_value)) {
|
||||
data[new_src].push({
|
||||
id: new_id,
|
||||
src: new_src,
|
||||
hid: drop_data.hid || "Dropped",
|
||||
name: drop_data.hid ? drop_data.name : new_id,
|
||||
keep: true,
|
||||
tags: []
|
||||
});
|
||||
/** Source helper matches history_content_types to source types */
|
||||
_getSource: function(v) {
|
||||
return v.history_content_type == "dataset_collection" ? "hdca" : "hda";
|
||||
},
|
||||
|
||||
/** Add values from drag/drop */
|
||||
_handleDropValues: function(drop_data, drop_partial = true) {
|
||||
let data = this.model.get("data");
|
||||
let current = this.model.get("current");
|
||||
let config = this.config[current];
|
||||
let field = this.fields[current];
|
||||
if (data) {
|
||||
let values = $.isArray(drop_data) ? drop_data : [drop_data];
|
||||
if (values.length > 0) {
|
||||
let data_changed = false;
|
||||
_.each(values, v => {
|
||||
let new_id = v.id;
|
||||
let new_src = (v.src = this._getSource(v));
|
||||
let new_value = { id: new_id, src: new_src };
|
||||
if (!_.findWhere(data[new_src], new_value)) {
|
||||
data_changed = true;
|
||||
data[new_src].push({
|
||||
id: new_id,
|
||||
src: new_src,
|
||||
hid: v.hid || "Selected",
|
||||
name: v.hid ? v.name : new_id,
|
||||
keep: true,
|
||||
tags: []
|
||||
});
|
||||
}
|
||||
});
|
||||
if (data_changed) {
|
||||
this._changeData();
|
||||
}
|
||||
if (config.src == new_src) {
|
||||
let first_id = values[0].id;
|
||||
let first_src = values[0].src;
|
||||
if (config.src == first_src && drop_partial) {
|
||||
var current_value = field.value();
|
||||
if (current_value && config.multiple) {
|
||||
if (current_value.indexOf(new_id) == -1) {
|
||||
current_value.push(new_id);
|
||||
}
|
||||
_.each(values, v => {
|
||||
if (current_value.indexOf(v.id) == -1) {
|
||||
current_value.push(v.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
current_value = new_id;
|
||||
current_value = first_id;
|
||||
}
|
||||
field.value(current_value);
|
||||
} else {
|
||||
this.model.set("value", { values: [new_value] });
|
||||
this.model.set("value", { values: values });
|
||||
this.model.trigger("change:value");
|
||||
}
|
||||
this.trigger("change");
|
||||
this._handleDropStatus("success");
|
||||
} else {
|
||||
this._handleDropStatus("danger");
|
||||
}
|
||||
} catch (e) {
|
||||
this._handleDropStatus("danger");
|
||||
}
|
||||
ev.preventDefault();
|
||||
this._handleDropStatus("success");
|
||||
},
|
||||
|
||||
/** Highlight drag result */
|
||||
@@ -487,6 +524,16 @@ var View = Backbone.View.extend({
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Template for batch mode execution options */
|
||||
_templateBatch: function() {
|
||||
return `<div class="form-text text-muted" style="clear: both;">
|
||||
<i class="fa fa-sitemap"/>
|
||||
<span>
|
||||
This is a batch mode input field. Separate jobs will be triggered for each dataset selection.
|
||||
</span>
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import $ from "jquery";
|
||||
import _ from "underscore";
|
||||
import Backbone from "backbone";
|
||||
import Utils from "utils/utils";
|
||||
import { keyedColorScheme } from "utils/color";
|
||||
import Buttons from "mvc/ui/ui-buttons";
|
||||
var View = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
@@ -211,9 +212,12 @@ var View = Backbone.View.extend({
|
||||
${_.reduce(
|
||||
filteredTags.slice(0, 5),
|
||||
(memo, tag) => {
|
||||
return `${memo} <div style="${Utils.generateTagStyle(
|
||||
tag.slice(5)
|
||||
)}" class="badge badge-primary badge-tags">${_.escape(tag)}</div>`;
|
||||
const tagColors = keyedColorScheme(tag.slice(5));
|
||||
return `${memo} <div style="background-color: ${tagColors.primary}; color: ${
|
||||
tagColors.contrasting
|
||||
}; border: 1px solid ${
|
||||
tagColors.darker
|
||||
}" class="badge badge-primary badge-tags">${_.escape(tag)}</div>`;
|
||||
},
|
||||
""
|
||||
)}
|
||||
|
||||
@@ -45,16 +45,14 @@ var View = Backbone.View.extend({
|
||||
|
||||
/** Template */
|
||||
_template: function() {
|
||||
return (
|
||||
`<div class="upload-button">
|
||||
return `<div class="upload-button">
|
||||
<div class="progress">
|
||||
<div class="progress-bar"/>
|
||||
<a class="upload-button-link" href="javascript:void(0)" id="tool-panel-upload-button">
|
||||
<span class="fa fa-upload"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
export default { View: View };
|
||||
|
||||
@@ -60,21 +60,21 @@ var User = Backbone.Model.extend(baseMVC.LoggableMixin).extend(
|
||||
this.preferences = preferences;
|
||||
},
|
||||
|
||||
getFavorites: function(){
|
||||
getFavorites: function() {
|
||||
let preferences = this.get("preferences");
|
||||
if (preferences && preferences.favorites){
|
||||
if (preferences && preferences.favorites) {
|
||||
return JSON.parse(preferences.favorites);
|
||||
} else {
|
||||
return {
|
||||
"tools": []
|
||||
tools: []
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
updateFavorites: function(object_type, new_favorites){
|
||||
updateFavorites: function(object_type, new_favorites) {
|
||||
let favorites = this.getFavorites();
|
||||
favorites[object_type] = new_favorites[object_type];
|
||||
this.updatePreferences("favorites", favorites)
|
||||
this.updatePreferences("favorites", favorites);
|
||||
},
|
||||
|
||||
/** Load a user with the API using an id.
|
||||
|
||||
@@ -106,6 +106,14 @@ var Model = Backbone.Model.extend({
|
||||
Galaxy.page.router.push(`${getAppRoot()}custom_builds`);
|
||||
}
|
||||
},
|
||||
genomespace: {
|
||||
title: _l("Request GenomeSpace token"),
|
||||
description: _l("Requests token through OpenID."),
|
||||
icon: "fa-openid",
|
||||
onclick: function() {
|
||||
window.location.href = `${getAppRoot()}openid/openid_auth?openid_provider=genomespace`;
|
||||
}
|
||||
},
|
||||
logout: {
|
||||
title: _l("Sign out"),
|
||||
description: _l("Click here to sign out of all sessions."),
|
||||
@@ -166,6 +174,9 @@ var View = Backbone.View.extend({
|
||||
self._addLink("permissions");
|
||||
self._addLink("make_data_private");
|
||||
self._addLink("api_key");
|
||||
if (config.enable_openid) {
|
||||
self._addLink("genomespace");
|
||||
}
|
||||
if (config.has_user_tool_filters) {
|
||||
self._addLink("toolbox_filters");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
|
||||
import * as Toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
|
||||
function Connector(handle1, handle2) {
|
||||
this.canvas = null;
|
||||
@@ -8,10 +8,7 @@ function Connector(handle1, handle2) {
|
||||
this.inner_color = "#FFFFFF";
|
||||
this.outer_color = "#25537b";
|
||||
if (handle1 && handle2) {
|
||||
this.connect(
|
||||
handle1,
|
||||
handle2
|
||||
);
|
||||
this.connect(handle1, handle2);
|
||||
}
|
||||
}
|
||||
$.extend(Connector.prototype, {
|
||||
@@ -35,9 +32,9 @@ $.extend(Connector.prototype, {
|
||||
$(this.canvas).remove();
|
||||
},
|
||||
destroyIfInvalid: function(warn) {
|
||||
if (this.handle1 && this.handle2 && !this.handle2.attachable(this.handle1)) {
|
||||
if (this.handle1 && this.handle2 && !this.handle2.attachable(this.handle1).canAccept) {
|
||||
if (warn) {
|
||||
Toastr.warning("Destroying a connection because collection type has changed.");
|
||||
Toast.warning("Destroying a connection because collection type has changed.");
|
||||
}
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import Connector from "mvc/workflow/workflow-connector";
|
||||
import * as Toastr from "libs/toastr";
|
||||
import { Toast } from "ui/toast";
|
||||
|
||||
class Workflow {
|
||||
constructor(app, canvas_container) {
|
||||
@@ -37,7 +37,7 @@ class Workflow {
|
||||
this.unregisterOutputLabel(fromLabel);
|
||||
}
|
||||
if (!this.canLabelOutputWith(toLabel)) {
|
||||
Toastr.warning(
|
||||
Toast.warning(
|
||||
`Workflow contains duplicate workflow output labels ${toLabel}. This must be fixed before it can be saved.`
|
||||
);
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class Workflow {
|
||||
var using_workflow_outputs = false;
|
||||
var has_existing_pjas = false;
|
||||
$.each(this.nodes, (k, node) => {
|
||||
if (node.workflow_outputs && node.workflow_outputs.length > 0) {
|
||||
if (node.type === "tool" && node.workflow_outputs && node.workflow_outputs.length > 0) {
|
||||
using_workflow_outputs = true;
|
||||
}
|
||||
$.each(node.post_job_actions, (pja_id, pja) => {
|
||||
@@ -102,43 +102,41 @@ class Workflow {
|
||||
if (using_workflow_outputs !== false || has_existing_pjas !== false) {
|
||||
// Using workflow outputs, or has existing pjas. Remove all PJAs and recreate based on outputs.
|
||||
$.each(this.nodes, (k, node) => {
|
||||
if (node.type === "tool") {
|
||||
var node_changed = false;
|
||||
if (node.post_job_actions === null) {
|
||||
node.post_job_actions = {};
|
||||
node_changed = true;
|
||||
var node_changed = false;
|
||||
if (node.post_job_actions === null) {
|
||||
node.post_job_actions = {};
|
||||
node_changed = true;
|
||||
}
|
||||
var pjas_to_rem = [];
|
||||
$.each(node.post_job_actions, (pja_id, pja) => {
|
||||
if (pja.action_type == "HideDatasetAction") {
|
||||
pjas_to_rem.push(pja_id);
|
||||
}
|
||||
var pjas_to_rem = [];
|
||||
$.each(node.post_job_actions, (pja_id, pja) => {
|
||||
if (pja.action_type == "HideDatasetAction") {
|
||||
pjas_to_rem.push(pja_id);
|
||||
});
|
||||
if (pjas_to_rem.length > 0) {
|
||||
$.each(pjas_to_rem, (i, pja_name) => {
|
||||
node_changed = true;
|
||||
delete node.post_job_actions[pja_name];
|
||||
});
|
||||
}
|
||||
if (using_workflow_outputs) {
|
||||
$.each(node.output_terminals, (ot_id, ot) => {
|
||||
var create_pja = !node.isWorkflowOutput(ot.name);
|
||||
if (create_pja === true) {
|
||||
node_changed = true;
|
||||
var pja = {
|
||||
action_type: "HideDatasetAction",
|
||||
output_name: ot.name,
|
||||
action_arguments: {}
|
||||
};
|
||||
node.post_job_actions[`HideDatasetAction${ot.name}`] = null;
|
||||
node.post_job_actions[`HideDatasetAction${ot.name}`] = pja;
|
||||
}
|
||||
});
|
||||
if (pjas_to_rem.length > 0) {
|
||||
$.each(pjas_to_rem, (i, pja_name) => {
|
||||
node_changed = true;
|
||||
delete node.post_job_actions[pja_name];
|
||||
});
|
||||
}
|
||||
if (using_workflow_outputs) {
|
||||
$.each(node.output_terminals, (ot_id, ot) => {
|
||||
var create_pja = !node.isWorkflowOutput(ot.name);
|
||||
if (create_pja === true) {
|
||||
node_changed = true;
|
||||
var pja = {
|
||||
action_type: "HideDatasetAction",
|
||||
output_name: ot.name,
|
||||
action_arguments: {}
|
||||
};
|
||||
node.post_job_actions[`HideDatasetAction${ot.name}`] = null;
|
||||
node.post_job_actions[`HideDatasetAction${ot.name}`] = pja;
|
||||
}
|
||||
});
|
||||
}
|
||||
// lastly, if this is the active node, and we made changes, reload the display at right.
|
||||
if (this.active_node == node && node_changed === true) {
|
||||
this.reload_active_node();
|
||||
}
|
||||
}
|
||||
// lastly, if this is the active node, and we made changes, reload the display at right.
|
||||
if (this.active_node == node && node_changed === true) {
|
||||
this.reload_active_node();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -263,10 +261,7 @@ class Workflow {
|
||||
$.each(v, (l, x) => {
|
||||
var other_node = wf.nodes[parseInt(x.id) + offset];
|
||||
var c = new Connector();
|
||||
c.connect(
|
||||
other_node.output_terminals[x.output_name],
|
||||
node.input_terminals[k]
|
||||
);
|
||||
c.connect(other_node.output_terminals[x.output_name], node.input_terminals[k]);
|
||||
c.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ function CollectionTypeDescription(collectionType) {
|
||||
this.rank = collectionType.split(":").length;
|
||||
}
|
||||
|
||||
function ConnectionAcceptable(canAccept, reason) {
|
||||
this.canAccept = canAccept;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
var NULL_COLLECTION_TYPE_DESCRIPTION = {
|
||||
isCollection: false,
|
||||
canMatch: function() {
|
||||
@@ -157,14 +162,21 @@ var Terminal = Backbone.Model.extend({
|
||||
});
|
||||
},
|
||||
setMapOver: function(val) {
|
||||
let output_val = val;
|
||||
if (this.multiple) {
|
||||
return; // Cannot set this to be multirun...
|
||||
// emulate list input
|
||||
let description = new CollectionTypeDescription("list");
|
||||
if (val.collectionType === description.collectionType) {
|
||||
// No mapping over necessary
|
||||
return;
|
||||
}
|
||||
output_val = val.effectiveMapOver ? val.effectiveMapOver(description) : val;
|
||||
}
|
||||
|
||||
if (!this.mapOver().equal(val)) {
|
||||
this.terminalMapping.setMapOver(val);
|
||||
_.each(this.node.output_terminals, outputTerminal => {
|
||||
outputTerminal.setMapOver(val);
|
||||
outputTerminal.setMapOver(output_val);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -239,7 +251,10 @@ var BaseInputTerminal = Terminal.extend({
|
||||
},
|
||||
canAccept: function(other) {
|
||||
if (this._inputFilled()) {
|
||||
return false;
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Input already filled with another connection, delete it before connecting another output."
|
||||
);
|
||||
} else {
|
||||
return this.attachable(other);
|
||||
}
|
||||
@@ -336,7 +351,7 @@ var BaseInputTerminal = Terminal.extend({
|
||||
for (var t in this.datatypes) {
|
||||
var thisDatatype = this.datatypes[t];
|
||||
if (thisDatatype == "input") {
|
||||
return true;
|
||||
return new ConnectionAcceptable(true, null);
|
||||
}
|
||||
var cat_outputs = [];
|
||||
cat_outputs = cat_outputs.concat(other.datatypes);
|
||||
@@ -361,11 +376,14 @@ var BaseInputTerminal = Terminal.extend({
|
||||
other_datatype == "input_collection" ||
|
||||
window.workflow_globals.app.isSubType(cat_outputs[other_datatype_i], thisDatatype)
|
||||
) {
|
||||
return true;
|
||||
return new ConnectionAcceptable(true, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
`Effective output data type(s) [${cat_outputs}] do not appear to match input type(s) [${this.datatypes}].`
|
||||
);
|
||||
},
|
||||
_otherCollectionType: function(other) {
|
||||
var otherCollectionType = NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
@@ -403,16 +421,16 @@ var InputTerminal = BaseInputTerminal.extend({
|
||||
if (otherCollectionType.isCollection) {
|
||||
if (this.multiple) {
|
||||
if (this.connected() && !this._collectionAttached()) {
|
||||
// if single inputs attached, cannot also attach a
|
||||
// collection (yet...)
|
||||
return false;
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Cannot attach collections to data parameters with individual data inputs already attached."
|
||||
);
|
||||
}
|
||||
if (otherCollectionType.rank == 1) {
|
||||
return this._producesAcceptableDatatype(other);
|
||||
} else {
|
||||
// TODO: Allow subcollection mapping over this as if it were
|
||||
// a list collection input.
|
||||
return false;
|
||||
if (otherCollectionType.collectionType.endsWith("paired")) {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Cannot attach paired inputs to multiple data parameters, only lists may be treated this way."
|
||||
);
|
||||
}
|
||||
}
|
||||
if (thisMapOver.isCollection && thisMapOver.canMatch(otherCollectionType)) {
|
||||
@@ -423,13 +441,32 @@ var InputTerminal = BaseInputTerminal.extend({
|
||||
if (mappingConstraints.every(_.bind(otherCollectionType.canMatch, otherCollectionType))) {
|
||||
return this._producesAcceptableDatatype(other);
|
||||
} else {
|
||||
return false;
|
||||
if (thisMapOver.isCollection) {
|
||||
// incompatible collection type attached
|
||||
if (this.node.hasConnectedMappedInputTerminals()) {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Can't map over this input with output collection type - other inputs have an incompatible map over collection type. Disconnect inputs (and potentially outputs) and retry."
|
||||
);
|
||||
} else {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"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."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Can't map over this input with output collection type - an output of this tool is not mapped over constraining this input. Disconnect output(s) and retry."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (thisMapOver.isCollection) {
|
||||
// Attempting to match a non-collection output to an
|
||||
// explicitly collection input.
|
||||
return false;
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Cannot attach non-collection outputs to mapped over inputs, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
);
|
||||
}
|
||||
return this._producesAcceptableDatatype(other);
|
||||
}
|
||||
@@ -443,7 +480,7 @@ var InputParameterTerminal = BaseInputTerminal.extend({
|
||||
BaseInputTerminal.prototype.connect.call(this, connector);
|
||||
},
|
||||
attachable: function(other) {
|
||||
return this.type == other.attributes.type;
|
||||
return new ConnectionAcceptable(this.type == other.attributes.type, "");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -527,20 +564,46 @@ var InputCollectionTerminal = BaseInputTerminal.extend({
|
||||
// Otherwise we need to mapOver
|
||||
} else if (thisMapOver.isCollection) {
|
||||
// In this case, mapOver already set and we didn't match skipping...
|
||||
return false;
|
||||
if (this.node.hasConnectedMappedInputTerminals()) {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Can't map over this input with output collection type - other inputs have an incompatible map over collection type. Disconnect inputs (and potentially outputs) and retry."
|
||||
);
|
||||
} else {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"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."
|
||||
);
|
||||
}
|
||||
} else if (_.some(this.collectionTypes, collectionType => otherCollectionType.canMapOver(collectionType))) {
|
||||
// we're not mapped over - but hey maybe we could be... lets check.
|
||||
var effectiveMapOver = this._effectiveMapOver(other);
|
||||
if (!effectiveMapOver.isCollection) {
|
||||
return false;
|
||||
return new ConnectionAcceptable(false, "Incompatible collection type(s) for attachment.");
|
||||
}
|
||||
// Need to check if this would break constraints...
|
||||
var mappingConstraints = this._mappingConstraints();
|
||||
if (mappingConstraints.every(effectiveMapOver.canMatch)) {
|
||||
if (mappingConstraints.every(d => effectiveMapOver.canMatch(d))) {
|
||||
return this._producesAcceptableDatatype(other);
|
||||
} else {
|
||||
if (this.node.hasConnectedMappedInputTerminals()) {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Can't map over this input with output collection type - other inputs have an incompatible map over collection type. Disconnect inputs (and potentially outputs) and retry."
|
||||
);
|
||||
} else {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"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."
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return new ConnectionAcceptable(false, "Incompatible collection type(s) for attachment.");
|
||||
}
|
||||
} else {
|
||||
return new ConnectionAcceptable(false, "Cannot attach a data output to a collection input.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -77,9 +77,7 @@ var BaseInputTerminalView = TerminalView.extend({
|
||||
const name = input.name;
|
||||
const id = `node-${node.cid}-input-${name}`;
|
||||
const terminal = this.terminalForInput(input);
|
||||
if (!terminal.multiple) {
|
||||
this.setupMappingView(terminal);
|
||||
}
|
||||
this.setupMappingView(terminal);
|
||||
this.el.terminal = terminal;
|
||||
this.$el.attr("input-name", name);
|
||||
this.$el.attr("id", id);
|
||||
@@ -102,21 +100,47 @@ var BaseInputTerminalView = TerminalView.extend({
|
||||
var terminal = this.el.terminal;
|
||||
// Accept a dragable if it is an output terminal and has a
|
||||
// compatible type
|
||||
return $(d.drag).hasClass("output-terminal") && terminal.canAccept(d.drag.terminal);
|
||||
var connectionAcceptable = $(d.drag).hasClass("output-terminal") && terminal.canAccept(d.drag.terminal);
|
||||
if (connectionAcceptable.canAccept) {
|
||||
this.$el.addClass("can-accept");
|
||||
this.$el.removeClass("cannot-accept");
|
||||
this.reason = null;
|
||||
} else {
|
||||
this.$el.addClass("cannot-accept");
|
||||
this.$el.removeClass("can-accept");
|
||||
this.reason = connectionAcceptable.reason;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onDropStart: function(e, d) {
|
||||
if (d.proxy.terminal) {
|
||||
d.proxy.terminal.connectors[0].inner_color = "#BBFFBB";
|
||||
if (this.$el.hasClass("can-accept")) {
|
||||
d.proxy.terminal.connectors[0].inner_color = "#BBFFBB";
|
||||
d.proxy.dropTooltip = "";
|
||||
} else {
|
||||
d.proxy.terminal.connectors[0].inner_color = "#fe7f02";
|
||||
if (this.reason) {
|
||||
d.proxy.dropTooltip = this.reason;
|
||||
$(d.proxy).tooltip("show");
|
||||
} else {
|
||||
d.proxy.dropTooltip = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDropEnd: function(e, d) {
|
||||
d.proxy.dropTooltip = "";
|
||||
if (d.proxy.terminal) {
|
||||
d.proxy.terminal.connectors[0].inner_color = "#FFFFFF";
|
||||
}
|
||||
},
|
||||
onDrop: function(e, d) {
|
||||
var terminal = this.el.terminal;
|
||||
new Connector(d.drag.terminal, terminal).redraw();
|
||||
d.proxy.dropTooltip = "";
|
||||
if (this.$el.hasClass("can-accept")) {
|
||||
const terminal = this.el.terminal;
|
||||
new Connector(d.drag.terminal, terminal).redraw();
|
||||
}
|
||||
},
|
||||
onHover: function() {
|
||||
let element = this.el;
|
||||
@@ -227,14 +251,19 @@ var BaseOutputTerminalView = TerminalView.extend({
|
||||
var h = $("<div class='drag-terminal'/>")
|
||||
.appendTo("#canvas-container")
|
||||
.get(0);
|
||||
|
||||
h.dropTooltip = "";
|
||||
|
||||
// Terminal and connection to display noodle while dragging
|
||||
$(h).tooltip({
|
||||
title: function() {
|
||||
return h.dropTooltip || "";
|
||||
}
|
||||
});
|
||||
h.terminal = new Terminals.OutputTerminal({ element: h });
|
||||
var c = new Connector();
|
||||
c.dragging = true;
|
||||
c.connect(
|
||||
this.el.terminal,
|
||||
h.terminal
|
||||
);
|
||||
c.connect(this.el.terminal, h.terminal);
|
||||
return h;
|
||||
},
|
||||
onDragEnd: function(e, d) {
|
||||
@@ -246,6 +275,7 @@ var BaseOutputTerminalView = TerminalView.extend({
|
||||
if (connector) {
|
||||
connector.destroy();
|
||||
}
|
||||
$(d.proxy).tooltip("dispose");
|
||||
$(d.proxy).remove();
|
||||
$(d.available).removeClass("input-terminal-active");
|
||||
$("#canvas-container")
|
||||
|
||||
@@ -641,8 +641,19 @@ export default Backbone.View.extend({
|
||||
url: `${getAppRoot()}api/workflows/build_module`,
|
||||
data: request_data,
|
||||
success: function(data) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
node.init_field_data(data);
|
||||
node.update_field_data(data);
|
||||
// Post init/update, for new modules we want to default to
|
||||
// nodes being outputs
|
||||
// TODO: Overhaul the handling of all this when we modernize
|
||||
// the editor, replace callout image manipulation with a simple
|
||||
// class toggle, etc.
|
||||
$.each(node.output_terminals, (ot_id, ot) => {
|
||||
node.addWorkflowOutput(ot.name);
|
||||
var callout = $(node.element).find(`.callout.${ot.name.replace(/(?=[()])/g, "\\")}`);
|
||||
callout.find("img").attr("src", `${Galaxy.root}static/images/fugue/asterisk-small.png`);
|
||||
});
|
||||
self.workflow.activate_node(node);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,12 +4,12 @@ import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import * as mod_toastr from "libs/toastr";
|
||||
import TAGS from "mvc/tag";
|
||||
import { Toast } from "ui/toast";
|
||||
import WORKFLOWS from "mvc/workflow/workflow-model";
|
||||
import QueryStringParsing from "utils/query-string-parsing";
|
||||
import _l from "utils/localization";
|
||||
import LoadingIndicator from "ui/loading-indicator";
|
||||
import { mountModelTags } from "components/Tags";
|
||||
|
||||
/** View of the individual workflows */
|
||||
const WorkflowItemView = Backbone.View.extend({
|
||||
@@ -24,7 +24,7 @@ const WorkflowItemView = Backbone.View.extend({
|
||||
"removeWorkflow",
|
||||
"copyWorkflow"
|
||||
); // every function that uses 'this' as the current object should be in here
|
||||
mod_toastr.options.timeOut = 1500;
|
||||
Toast.options.timeOut = 1500;
|
||||
},
|
||||
|
||||
events: {
|
||||
@@ -57,7 +57,7 @@ const WorkflowItemView = Backbone.View.extend({
|
||||
if (window.confirm(`Are you sure you want to delete workflow '${wfName}'?`)) {
|
||||
this.model.destroy({
|
||||
success: function() {
|
||||
mod_toastr.success(`Successfully deleted workflow '${wfName}'`);
|
||||
Toast.success(`Successfully deleted workflow '${wfName}'`);
|
||||
}
|
||||
});
|
||||
this.remove();
|
||||
@@ -72,7 +72,7 @@ const WorkflowItemView = Backbone.View.extend({
|
||||
{ name: newName },
|
||||
{
|
||||
success: function() {
|
||||
mod_toastr.success(`Successfully renamed workflow '${oldName}' to '${newName}'`);
|
||||
Toast.success(`Successfully renamed workflow '${oldName}' to '${newName}'`);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -94,16 +94,16 @@ const WorkflowItemView = Backbone.View.extend({
|
||||
at: 0,
|
||||
wait: true,
|
||||
success: function() {
|
||||
mod_toastr.success(`Successfully copied workflow '${oldName}' to '${newName}'`);
|
||||
Toast.success(`Successfully copied workflow '${oldName}' to '${newName}'`);
|
||||
},
|
||||
error: function(model, resp, options) {
|
||||
// signature seems to have changed over the course of backbone dev
|
||||
// see https://github.com/jashkenas/backbone/issues/2606#issuecomment-19289483
|
||||
mod_toastr.error(options.errorThrown);
|
||||
Toast.error(options.errorThrown);
|
||||
}
|
||||
});
|
||||
}).error((jqXHR, textStatus, errorThrown) => {
|
||||
mod_toastr.error(jqXHR.responseJSON.err_msg);
|
||||
Toast.error(jqXHR.responseJSON.err_msg);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -127,8 +127,7 @@ const WorkflowItemView = Backbone.View.extend({
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="${wfId} tags-display">
|
||||
</div>
|
||||
<div class="${wfId} tags-display"></div>
|
||||
</td>
|
||||
<td>
|
||||
${this.model.get("owner") === Galaxy.user.attributes.username ? "You" : this.model.get("owner")}
|
||||
@@ -139,13 +138,13 @@ const WorkflowItemView = Backbone.View.extend({
|
||||
},
|
||||
|
||||
renderTagEditor: function() {
|
||||
const TagEditor = new TAGS.TagsEditor({
|
||||
let el = $(this.el).find(".tags-display")[0];
|
||||
let propsData = {
|
||||
model: this.model,
|
||||
el: $.find(`.${this.model.id}.tags-display`),
|
||||
workflow_mode: true
|
||||
});
|
||||
TagEditor.toggle(true);
|
||||
TagEditor.render();
|
||||
disabled: false,
|
||||
context: "workflow"
|
||||
};
|
||||
return mountModelTags(propsData, el);
|
||||
},
|
||||
|
||||
/** Template for user actions for workflows */
|
||||
@@ -195,13 +194,14 @@ const WorkflowListView = Backbone.View.extend({
|
||||
},
|
||||
|
||||
events: {
|
||||
dragleave: "unhighlightDropZone",
|
||||
drop: "drop",
|
||||
dragover: function(ev) {
|
||||
$(".hidden_description_layer").addClass("dragover");
|
||||
$(".menubutton").addClass("background-none");
|
||||
ev.preventDefault();
|
||||
}
|
||||
dragover: "highlightDropZone",
|
||||
dragleave: "unhighlightDropZone"
|
||||
},
|
||||
|
||||
highlightDropZone: function(ev) {
|
||||
$(".hidden_description_layer").addClass("dragover");
|
||||
$(".menubutton").addClass("background-none");
|
||||
ev.preventDefault();
|
||||
},
|
||||
|
||||
unhighlightDropZone: function() {
|
||||
@@ -226,7 +226,7 @@ const WorkflowListView = Backbone.View.extend({
|
||||
try {
|
||||
wf_json = JSON.parse(reader.result);
|
||||
} catch (e) {
|
||||
mod_toastr.error(`Could not read file '${f.name}'. Verify it is a valid Galaxy workflow`);
|
||||
Toast.error(`Could not read file '${f.name}'. Verify it is a valid Galaxy workflow`);
|
||||
wf_json = null;
|
||||
}
|
||||
if (wf_json) {
|
||||
@@ -234,10 +234,10 @@ const WorkflowListView = Backbone.View.extend({
|
||||
at: 0,
|
||||
wait: true,
|
||||
success: function() {
|
||||
mod_toastr.success(`Successfully imported workflow '${wf_json.name}'`);
|
||||
Toast.success(`Successfully imported workflow '${wf_json.name}'`);
|
||||
},
|
||||
error: function(model, resp, options) {
|
||||
mod_toastr.error(options.errorThrown);
|
||||
Toast.error(options.errorThrown);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -250,9 +250,9 @@ const WorkflowListView = Backbone.View.extend({
|
||||
const msg_text = QueryStringParsing.get("message");
|
||||
const msg_status = QueryStringParsing.get("status");
|
||||
if (msg_status === "error") {
|
||||
mod_toastr.error(_.escape(msg_text || "Unknown Error, please report this to an administrator."));
|
||||
Toast.error(_.escape(msg_text || "Unknown Error, please report this to an administrator."));
|
||||
} else if (msg_text) {
|
||||
mod_toastr.info(_.escape(msg_text));
|
||||
Toast.info(_.escape(msg_text));
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -272,6 +272,9 @@ const WorkflowListView = Backbone.View.extend({
|
||||
this.searchWorkflow(this.$(".search-wf"), this.$(".workflow-search tr"), minQueryLength);
|
||||
this.adjustActiondropdown();
|
||||
this._showArgErrors();
|
||||
this.$(".hidden_description_layer")
|
||||
.get(0)
|
||||
.addEventListener("drop", _.bind(this.drop, this));
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ QUnit.module("Masthead test", {
|
||||
use_remote_user: "use_remote_user",
|
||||
remote_user_logout_href: "remote_user_logout_href",
|
||||
lims_doc_url: "lims_doc_url",
|
||||
biostar_url: "biostar_url",
|
||||
biostar_url_redirect: "biostar_url_redirect",
|
||||
support_url: "support_url",
|
||||
search_url: "search_url",
|
||||
mailing_lists: "mailing_lists",
|
||||
|
||||
@@ -726,15 +726,18 @@ QUnit.test("select-content", function(assert) {
|
||||
"Contains " + options.totalmultiple + " multiselect fields"
|
||||
);
|
||||
assert.ok(
|
||||
select.$el.children(".ui-options").find(".ui-option").length ===
|
||||
(options.selectfields > 1 ? options.selectfields : 0),
|
||||
"Radio button count"
|
||||
select.$el.find(".ui-options:first .ui-option").length === options.selectfields,
|
||||
"Radio button count, expected " + options.selectfields
|
||||
);
|
||||
assert.ok(select.$(".ui-select:first").css("display") == "block", "Check select visibility");
|
||||
assert.ok(
|
||||
select.$(".ui-select:last").css("display") == (options.selectfields == 1 ? "block" : "none"),
|
||||
"Last select visibility"
|
||||
);
|
||||
/*assert.ok(
|
||||
(select.button_dialog.$el.css("display") != "none") === options.showdialog,
|
||||
"Data dialog button visible"
|
||||
);*/
|
||||
_testSelect("first", options);
|
||||
_testSelect("last", options);
|
||||
};
|
||||
@@ -759,7 +762,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id2",
|
||||
lastlabel: "hid2: name2",
|
||||
lastlength: 3,
|
||||
lastmultiple: false
|
||||
lastmultiple: false,
|
||||
showdialog: true
|
||||
};
|
||||
_test(initial);
|
||||
|
||||
@@ -775,7 +779,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id2",
|
||||
lastlabel: "hid2: name2",
|
||||
lastlength: 3,
|
||||
lastmultiple: true
|
||||
lastmultiple: true,
|
||||
showdialog: true
|
||||
});
|
||||
|
||||
select.model.set("multiple", false);
|
||||
@@ -790,7 +795,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id2",
|
||||
lastlabel: "hid2: name2",
|
||||
lastlength: 3,
|
||||
lastmultiple: false
|
||||
lastmultiple: false,
|
||||
showdialog: false
|
||||
});
|
||||
|
||||
select.model.set("type", "module_data_collection");
|
||||
@@ -804,7 +810,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id2",
|
||||
lastlabel: "hid2: name2",
|
||||
lastlength: 3,
|
||||
lastmultiple: true
|
||||
lastmultiple: true,
|
||||
showdialog: false
|
||||
});
|
||||
|
||||
select.model.set("type", "module_data");
|
||||
@@ -818,7 +825,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id0",
|
||||
lastlabel: "hid0: name0",
|
||||
lastlength: 2,
|
||||
lastmultiple: true
|
||||
lastmultiple: true,
|
||||
showdialog: true
|
||||
});
|
||||
|
||||
select.model.set("type", "data");
|
||||
|
||||
@@ -72,7 +72,7 @@ QUnit.module("Input terminal model test", {
|
||||
return Terminals.NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
};
|
||||
}
|
||||
return this.input_terminal.canAccept(other);
|
||||
return this.input_terminal.canAccept(other).canAccept;
|
||||
},
|
||||
pja_change_datatype_node: function(output_name, newtype) {
|
||||
var pja = {
|
||||
@@ -319,7 +319,7 @@ QUnit.test("Collection output can connect to same collection input type", functi
|
||||
});
|
||||
outputTerminal.node = {};
|
||||
assert.ok(
|
||||
inputTerminal.canAccept(outputTerminal),
|
||||
inputTerminal.canAccept(outputTerminal).canAccept,
|
||||
"Input terminal " + inputTerminal + " can not accept " + outputTerminal
|
||||
);
|
||||
});
|
||||
@@ -332,7 +332,7 @@ QUnit.test("Collection output cannot connect to different collection input type"
|
||||
collection_type: "paired"
|
||||
});
|
||||
outputTerminal.node = {};
|
||||
assert.ok(!inputTerminal.canAccept(outputTerminal));
|
||||
assert.ok(!inputTerminal.canAccept(outputTerminal).canAccept);
|
||||
});
|
||||
|
||||
QUnit.module("Node unit test", {
|
||||
@@ -545,6 +545,8 @@ QUnit.module("Node view ", {
|
||||
input_terminals: {},
|
||||
output_terminals: {},
|
||||
markChanged: function() {},
|
||||
hasConnectedOutputTerminals: function() {},
|
||||
connectedMappedInputTerminals: function() {},
|
||||
terminalMapping: { disableMapOver: function() {} }
|
||||
});
|
||||
},
|
||||
@@ -987,7 +989,7 @@ QUnit.module("terminal mapping logic", {
|
||||
outputTerminal = output;
|
||||
}
|
||||
|
||||
assert.ok(!inputTerminal.attachable(outputTerminal));
|
||||
assert.ok(!inputTerminal.attachable(outputTerminal).canAccept);
|
||||
},
|
||||
verifyAttachable: function(assert, inputTerminal, output) {
|
||||
var outputTerminal;
|
||||
@@ -998,12 +1000,15 @@ QUnit.module("terminal mapping logic", {
|
||||
outputTerminal = output;
|
||||
}
|
||||
|
||||
assert.ok(inputTerminal.attachable(outputTerminal), "Cannot attach " + outputTerminal + " to " + inputTerminal);
|
||||
assert.ok(
|
||||
inputTerminal.attachable(outputTerminal).canAccept,
|
||||
"Cannot attach " + outputTerminal + " to " + inputTerminal
|
||||
);
|
||||
|
||||
// Go further... make sure datatypes are being enforced
|
||||
inputTerminal.datatypes = ["bam"];
|
||||
outputTerminal.datatypes = ["txt"];
|
||||
assert.ok(!inputTerminal.attachable(outputTerminal));
|
||||
assert.ok(!inputTerminal.attachable(outputTerminal).canAccept);
|
||||
},
|
||||
verifyMappedOver: function(assert, terminal) {
|
||||
assert.ok(terminal.terminalMapping.mapOver.isCollection);
|
||||
@@ -1174,7 +1179,14 @@ QUnit.test("multiple input attachable by collections", function(assert) {
|
||||
this.verifyAttachable(assert, this.inputTerminal1, "list");
|
||||
});
|
||||
|
||||
QUnit.test("unconnected multiple inputs cannot be connected to rank > 1 collections (yet...)", function(assert) {
|
||||
QUnit.test("multiple input attachable by nested collections", function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal(null, { multiple: true });
|
||||
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
|
||||
this.addConnectedOutput(connectedInput1);
|
||||
this.verifyAttachable(assert, this.inputTerminal1, "list:list");
|
||||
});
|
||||
|
||||
QUnit.test("Multiple inputs cannot be connected to pairs", function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal(null, { multiple: true });
|
||||
this.verifyNotAttachable(assert, this.inputTerminal1, "list:paired");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import * as d3 from "../libs/d3";
|
||||
import { event as currentEvent } from "../libs/d3";
|
||||
import * as d3 from "d3";
|
||||
import { event as currentEvent } from "d3";
|
||||
|
||||
function date_by_subtracting_days(date, days) {
|
||||
return new Date(
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
describe("Sample Test", () => {
|
||||
it("hey look, tests run", () => {
|
||||
assert.equal(1, 1);
|
||||
});
|
||||
|
||||
it("prove I can use a Proxy", () => {
|
||||
let target = {};
|
||||
let thing = new Proxy(target, {});
|
||||
thing.foo = 232;
|
||||
assert.equal(target.foo, thing.foo);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Vuex store module used for managing search parameters in the grid. Currently
|
||||
* only manages the tags that access this store via the new tagging components,
|
||||
* but presumably the entire grid search filter criteria will live here one day.
|
||||
*/
|
||||
|
||||
export const gridSearchStore = {
|
||||
state: {
|
||||
searchTags: new Set()
|
||||
},
|
||||
mutations: {
|
||||
// TODO: we could write an equivalence comparator here for searchTag
|
||||
// Sets and not register a change if the new set is equivalent
|
||||
setSearchTags(state, tags) {
|
||||
state.searchTags = new Set(tags);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
toggleSearchTag({ state, commit }, { text }) {
|
||||
let tags = new Set(state.searchTags);
|
||||
tags.has(text) ? tags.delete(text) : tags.add(text);
|
||||
commit("setSearchTags", tags);
|
||||
},
|
||||
removeSearchTag({ state, commit }, { text }) {
|
||||
let tags = new Set(state.searchTags);
|
||||
tags.delete(text);
|
||||
commit("setSearchTags", tags);
|
||||
},
|
||||
clearSearchTags({ state, commit }) {
|
||||
commit("setSearchTags", new Set());
|
||||
}
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user