mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge branch 'dev' of https://github.com/galaxyproject/galaxy into csv_tsv_converters
Should also rerun (and hopefully pass) CI.
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
# Enable retries on tests to reduce chances of transient failures.
|
||||
: ${GALAXY_TEST_SELENIUM_RETRIES:=1}
|
||||
: ${GALAXY_TEST_CLIENT_BUILD_IMAGE:='node:9.4.0'}
|
||||
|
||||
# If in Jenkins environment, use it for artifacts.
|
||||
if [ -n "$BUILD_NUMBER" ];
|
||||
@@ -20,15 +19,8 @@ mkdir -p "$GALAXY_TEST_SCREENSHOTS_DIRECTORY"
|
||||
mkdir -p ~/.jenkins-yarn-cache
|
||||
YARN_CACHE_FOLDER=~/.jenkins-yarn-cache
|
||||
|
||||
# Set git environment variables to enable Git. https://github.com/galaxyproject/galaxy/issues/5912
|
||||
# Setup volume and environment variable to cache this users yarn build.
|
||||
docker run -e GIT_COMMITTER_NAME=Jenkins -e GIT_COMMITTER_EMAIL=jenkins@galaxyproject.org \
|
||||
-e YARN_CACHE_FOLDER=$YARN_CACHE_FOLDER -v $YARN_CACHE_FOLDER:$YARN_CACHE_FOLDER:rw \
|
||||
-v `pwd`:`pwd`:rw -w `pwd` -u $UID $GALAXY_TEST_CLIENT_BUILD_IMAGE \
|
||||
/bin/bash -c 'make client-production-maps'
|
||||
|
||||
# Start Selenium server in the test Docker container.
|
||||
DOCKER_RUN_EXTRA_ARGS="--shm-size=2g -v $YARN_CACHE_FOLDER:$YARN_CACHE_FOLDER -e YARN_CACHE_FOLDER=$YARN_CACHE_FOLDER -e USE_SELENIUM=1 -e GALAXY_TEST_SELENIUM_RETRIES=${GALAXY_TEST_SELENIUM_RETRIES} -e GALAXY_TEST_ERRORS_DIRECTORY=${GALAXY_TEST_ERRORS_DIRECTORY} -e GALAXY_TEST_SCREENSHOTS_DIRECTORY=${GALAXY_TEST_SCREENSHOTS_DIRECTORY} ${DOCKER_RUN_EXTRA_ARGS}"
|
||||
DOCKER_RUN_EXTRA_ARGS="${DOCKER_RUN_EXTRA_ARGS} --shm-size=2g -v $YARN_CACHE_FOLDER:$YARN_CACHE_FOLDER -e YARN_CACHE_FOLDER=$YARN_CACHE_FOLDER -e USE_SELENIUM=1 -e GALAXY_TEST_SELENIUM_RETRIES=${GALAXY_TEST_SELENIUM_RETRIES} -e GALAXY_TEST_ERRORS_DIRECTORY=${GALAXY_TEST_ERRORS_DIRECTORY} -e GALAXY_TEST_SCREENSHOTS_DIRECTORY=${GALAXY_TEST_SCREENSHOTS_DIRECTORY}"
|
||||
export DOCKER_RUN_EXTRA_ARGS
|
||||
|
||||
./run_tests.sh --dockerize --python3 --db postgres --clean_pyc --skip_flakey_fails --selenium "$@"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
# Enable retries on tests to reduce chances of transient failures.
|
||||
: ${GALAXY_TEST_SELENIUM_RETRIES:=1}
|
||||
: ${GALAXY_TEST_CLIENT_BUILD_IMAGE:='node:9.4.0'}
|
||||
|
||||
# If in Jenkins environment, use it for artifacts.
|
||||
if [ -n "$BUILD_NUMBER" ];
|
||||
@@ -20,15 +19,8 @@ mkdir -p "$GALAXY_TEST_SCREENSHOTS_DIRECTORY"
|
||||
mkdir -p ~/.jenkins-yarn-cache
|
||||
YARN_CACHE_FOLDER=~/.jenkins-yarn-cache
|
||||
|
||||
# Set git environment variables to enable Git. https://github.com/galaxyproject/galaxy/issues/5912
|
||||
# Setup volume and environment variable to cache this users yarn build.
|
||||
docker run -e GIT_COMMITTER_NAME=Jenkins -e GIT_COMMITTER_EMAIL=jenkins@galaxyproject.org \
|
||||
-e YARN_CACHE_FOLDER=$YARN_CACHE_FOLDER -v $YARN_CACHE_FOLDER:$YARN_CACHE_FOLDER:rw \
|
||||
-v `pwd`:`pwd`:rw -w `pwd` -u $UID $GALAXY_TEST_CLIENT_BUILD_IMAGE \
|
||||
/bin/bash -c 'make client-production-maps'
|
||||
|
||||
# Start Selenium server in the test Docker container.
|
||||
DOCKER_RUN_EXTRA_ARGS="--shm-size=2g -v $YARN_CACHE_FOLDER:$YARN_CACHE_FOLDER -e YARN_CACHE_FOLDER=$YARN_CACHE_FOLDER -e USE_SELENIUM=1 -e GALAXY_TEST_SELENIUM_RETRIES=${GALAXY_TEST_SELENIUM_RETRIES} -e GALAXY_TEST_ERRORS_DIRECTORY=${GALAXY_TEST_ERRORS_DIRECTORY} -e GALAXY_TEST_SCREENSHOTS_DIRECTORY=${GALAXY_TEST_SCREENSHOTS_DIRECTORY} ${DOCKER_RUN_EXTRA_ARGS}"
|
||||
DOCKER_RUN_EXTRA_ARGS="${DOCKER_RUN_EXTRA_ARGS} --shm-size=2g -v $YARN_CACHE_FOLDER:$YARN_CACHE_FOLDER -e YARN_CACHE_FOLDER=$YARN_CACHE_FOLDER -e USE_SELENIUM=1 -e GALAXY_TEST_SELENIUM_RETRIES=${GALAXY_TEST_SELENIUM_RETRIES} -e GALAXY_TEST_ERRORS_DIRECTORY=${GALAXY_TEST_ERRORS_DIRECTORY} -e GALAXY_TEST_SCREENSHOTS_DIRECTORY=${GALAXY_TEST_SCREENSHOTS_DIRECTORY}"
|
||||
export DOCKER_RUN_EXTRA_ARGS
|
||||
|
||||
./run_tests.sh --dockerize --db postgres --clean_pyc --skip_flakey_fails --selenium "$@"
|
||||
|
||||
@@ -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
|
||||
+8
-2
@@ -117,6 +117,7 @@ test/unit/**.log
|
||||
|
||||
# Project files
|
||||
*.kpf
|
||||
.idea
|
||||
.vscode
|
||||
client/**/jsconfig.json
|
||||
|
||||
@@ -142,15 +143,20 @@ doc/schema.md
|
||||
doc/source/admin/config_logging_default_yaml.rst
|
||||
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
|
||||
*.rej
|
||||
*~
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
+12
-59
@@ -1,64 +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=py34-lint
|
||||
- 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=py34-first_startup
|
||||
addons:
|
||||
&py3_addons
|
||||
apt:
|
||||
packages:
|
||||
# For psutil, pyyaml, uwsgi...
|
||||
- libpython3.4-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=py34-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
|
||||
@@ -69,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
|
||||
@@ -27,7 +27,6 @@ docs: ## Generate HTML documentation.
|
||||
# $ ./scripts/common_startup.sh
|
||||
# $ . .venv/bin/activate
|
||||
# $ pip install -r lib/galaxy/dependencies/dev-requirements.txt
|
||||
# You also need to install pandoc separately.
|
||||
$(IN_VENV) $(MAKE) -C doc clean
|
||||
$(IN_VENV) $(MAKE) -C doc html
|
||||
|
||||
@@ -141,7 +140,7 @@ 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/*"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
10.13.0
|
||||
+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);
|
||||
@@ -448,7 +448,7 @@
|
||||
<hot-table
|
||||
id="hot-table"
|
||||
ref="hotTable"
|
||||
:data="hotData['data']"
|
||||
:data="hotData.data"
|
||||
:colHeaders="colHeadersDisplay"
|
||||
:readOnly="true"
|
||||
stretchH="all"
|
||||
@@ -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">
|
||||
@@ -1029,7 +990,6 @@ export default {
|
||||
}
|
||||
return {
|
||||
rules: rules,
|
||||
colHeadersPerRule: [],
|
||||
mapping: mapping,
|
||||
state: "build", // 'build', 'error', 'wait',
|
||||
ruleView: "normal", // 'normal' or 'source'
|
||||
@@ -1257,8 +1217,7 @@ export default {
|
||||
return targets;
|
||||
},
|
||||
colHeaders() {
|
||||
const data = this.hotData["data"];
|
||||
const columns = this.hotData["columns"];
|
||||
let { data, columns } = this.hotData;
|
||||
return RuleDefs.colHeadersFor(data, columns);
|
||||
},
|
||||
colHeadersDisplay() {
|
||||
@@ -1374,6 +1333,44 @@ export default {
|
||||
valid = false;
|
||||
}
|
||||
return valid;
|
||||
},
|
||||
hotData() {
|
||||
let data, sources, columns;
|
||||
if (
|
||||
this.elementsType == "datasets" ||
|
||||
this.elementsType == "library_datasets" ||
|
||||
this.elementsType == "ftp"
|
||||
) {
|
||||
sources = this.initialElements.slice();
|
||||
data = sources.map(el => []);
|
||||
columns = [];
|
||||
} else if (this.elementsType == "collection_contents") {
|
||||
const collection = this.initialElements;
|
||||
if (collection) {
|
||||
const obj = this.populateElementsFromCollectionDescription(
|
||||
collection.elements,
|
||||
collection.collection_type
|
||||
);
|
||||
data = obj.data;
|
||||
sources = obj.sources;
|
||||
columns = [];
|
||||
} else {
|
||||
data = [];
|
||||
sources = [];
|
||||
columns = [];
|
||||
}
|
||||
} else {
|
||||
data = this.initialElements.slice();
|
||||
sources = data.map(el => null);
|
||||
columns = [];
|
||||
if (this.initialElements) {
|
||||
this.initialElements[0].forEach(() => columns.push("new"));
|
||||
}
|
||||
}
|
||||
return RuleDefs.applyRules(data, sources, columns, this.rules);
|
||||
},
|
||||
colHeadersPerRule() {
|
||||
return this.hotData.colHeadersPerRule;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -1412,42 +1409,6 @@ export default {
|
||||
this.rules.push(rule);
|
||||
}
|
||||
},
|
||||
hotData() {
|
||||
let data, sources, columns;
|
||||
if (
|
||||
this.elementsType == "datasets" ||
|
||||
this.elementsType == "library_datasets" ||
|
||||
this.elementsType == "ftp"
|
||||
) {
|
||||
data = this.initialElements.map(el => []);
|
||||
sources = this.initialElements.slice();
|
||||
columns = [];
|
||||
} else if (this.elementsType == "collection_contents") {
|
||||
const collection = this.initialElements;
|
||||
if (collection) {
|
||||
const obj = this.populateElementsFromCollectionDescription(
|
||||
collection.elements,
|
||||
collection.collection_type
|
||||
);
|
||||
data = obj.data;
|
||||
sources = obj.sources;
|
||||
columns = [];
|
||||
} else {
|
||||
data = [];
|
||||
sources = [];
|
||||
columns = [];
|
||||
}
|
||||
} else {
|
||||
data = this.initialElements.slice();
|
||||
sources = data.map(el => null);
|
||||
columns = [];
|
||||
if (this.initialElements) {
|
||||
this.initialElements[0].forEach(() => columns.push("new"));
|
||||
}
|
||||
}
|
||||
this.colHeadersPerRule = [];
|
||||
return RuleDefs.applyRules(data, sources, columns, this.rules, this.colHeadersPerRule);
|
||||
},
|
||||
viewSource() {
|
||||
this.resetSource();
|
||||
this.ruleView = "source";
|
||||
@@ -1684,7 +1645,7 @@ export default {
|
||||
return identifierColumns;
|
||||
},
|
||||
buildRequestElements(createDatasetDescription, createSubcollectionDescription, subElementProp) {
|
||||
const data = this.hotData["data"];
|
||||
const data = this.hotData.data;
|
||||
const identifierColumns = this.identifierColumns();
|
||||
if (identifierColumns.length < 1) {
|
||||
console.log("Error but this shouldn't have happened, create button should have been disabled.");
|
||||
@@ -1791,8 +1752,7 @@ export default {
|
||||
return elementsByName;
|
||||
},
|
||||
creationElementsFromDatasets() {
|
||||
const sources = this.hotData["sources"];
|
||||
const data = this.hotData["data"];
|
||||
const { sources, data } = this.hotData;
|
||||
const mappingAsDict = this.mappingAsDict;
|
||||
|
||||
const elementsByCollectionName = this.buildRequestElements(
|
||||
@@ -1811,7 +1771,7 @@ export default {
|
||||
},
|
||||
creationElementsForFetch() {
|
||||
// fetch elements for HDCA
|
||||
const data = this.hotData["data"];
|
||||
const data = this.hotData.data;
|
||||
const mappingAsDict = this.mappingAsDict;
|
||||
|
||||
const elementsByCollectionName = this.buildRequestElements(
|
||||
@@ -1830,7 +1790,7 @@ export default {
|
||||
},
|
||||
creationDatasetsForFetch() {
|
||||
// fetch elements for HDAs if not collection information specified.
|
||||
const data = this.hotData["data"];
|
||||
const data = this.hotData.data;
|
||||
const mappingAsDict = this.mappingAsDict;
|
||||
|
||||
const datasets = [];
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="showLogin">
|
||||
<login :show_welcome_with_login="show_welcome_with_login" :welcome_url="welcome_url" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<register
|
||||
:registration_warning_message="registration_warning_message"
|
||||
:mailing_join_addr="mailing_join_addr"
|
||||
:smtp_server="smtp_server"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Login from "components/login/Login.vue";
|
||||
import Register from "components/login/Register.vue";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
login: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showLogin: function() {
|
||||
return this.login;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
login: Login,
|
||||
register: Register
|
||||
},
|
||||
methods: {
|
||||
toggleLogin: function() {
|
||||
this.login = !this.login;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
show_welcome_with_login: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
welcome_url: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
registration_warning_message: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
mailing_join_addr: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
smtp_server: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -3,21 +3,30 @@
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col col-lg-6">
|
||||
<b-alert :show="messageShow" :variant="messageVariant" v-html="messageText" />
|
||||
<b-form id="login" @submit.prevent="submit()">
|
||||
<b-card header="Welcome to Galaxy, please log in">
|
||||
<b-form-group label="Username or Email Address">
|
||||
<b-form-input name="login" type="text" v-model="login" />
|
||||
</b-form-group>
|
||||
<b-form-group label="Password">
|
||||
<b-form-input name="password" type="password" v-model="password" />
|
||||
<b-form-text
|
||||
>Forgot password? Click here to <a @click="reset" href="#">reset</a> your
|
||||
password.</b-form-text
|
||||
>
|
||||
</b-form-group>
|
||||
<b-button type="submit">Login</b-button>
|
||||
<b-form id="login" @submit.prevent="submitGalaxyLogin()">
|
||||
<b-card no-body header="Welcome to Galaxy, please log in">
|
||||
<b-card-body>
|
||||
<b-form-group label="Username or Email Address">
|
||||
<b-form-input name="login" type="text" v-model="login" />
|
||||
</b-form-group>
|
||||
<b-form-group label="Password">
|
||||
<b-form-input name="password" type="password" v-model="password" />
|
||||
<b-form-text
|
||||
>Forgot password? Click here to <a @click="reset" href="#">reset</a> your
|
||||
password.</b-form-text
|
||||
>
|
||||
</b-form-group>
|
||||
<b-button name="login" type="submit">Login</b-button>
|
||||
</b-card-body>
|
||||
<b-card-footer>
|
||||
Don't have an account?
|
||||
<a id="register-toggle" href="#" @click.prevent="toggleLogin">Register here.</a>
|
||||
</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)">
|
||||
<icon v-bind:class="oidc_idps_icons[idp]" /> Sign in with {{ idp.charAt(0).toUpperCase() + idp.slice(1) }}
|
||||
</b-button>
|
||||
</div>
|
||||
<div v-if="show_welcome_with_login" class="col">
|
||||
<b-embed type="iframe" :src="welcome_url" aspect="1by1" />
|
||||
@@ -46,7 +55,14 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
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,
|
||||
@@ -54,7 +70,12 @@ export default {
|
||||
provider: null,
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
redirect: Galaxy.params.redirect
|
||||
redirect: galaxy.params.redirect,
|
||||
session_csrf_token: galaxy.session_csrf_token,
|
||||
enable_oidc: galaxy.config.enable_oidc,
|
||||
oidc_idps: oidc_idps,
|
||||
oidc_idps_icons: oidc_idps_icons
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -63,11 +84,15 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit: function(method) {
|
||||
toggleLogin: function() {
|
||||
if (this.$root.toggleLogin) {
|
||||
this.$root.toggleLogin();
|
||||
}
|
||||
},
|
||||
submitGalaxyLogin: function(method) {
|
||||
let rootUrl = getAppRoot();
|
||||
let data = { login: this.login, password: this.password, redirect: this.redirect };
|
||||
axios
|
||||
.post(`${rootUrl}user/login`, data)
|
||||
.post(`${rootUrl}user/login`, this.$data)
|
||||
.then(response => {
|
||||
if (response.data.message && response.data.status) {
|
||||
alert(response.data.message);
|
||||
@@ -86,6 +111,22 @@ export default {
|
||||
this.messageText = message || "Login failed for an unknown reason.";
|
||||
});
|
||||
},
|
||||
submitOIDCLogin: function(idp) {
|
||||
let rootUrl = getAppRoot();
|
||||
axios
|
||||
.post(`${rootUrl}authnz/${idp}/login`)
|
||||
.then(response => {
|
||||
if (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?
|
||||
})
|
||||
.catch(error => {
|
||||
this.messageVariant = "danger";
|
||||
let message = error.response.data && error.response.data.err_msg;
|
||||
this.messageText = message || "Login failed for an unknown reason.";
|
||||
});
|
||||
},
|
||||
reset: function(ev) {
|
||||
let rootUrl = getAppRoot();
|
||||
ev.preventDefault();
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col col-lg-6">
|
||||
<b-alert :show="registration_warning_message" variant="danger">
|
||||
{{ registration_warning_message }}
|
||||
</b-alert>
|
||||
<b-alert :show="messageShow" :variant="messageVariant" v-html="messageText" />
|
||||
<b-form id="registration" @submit.prevent="submit()">
|
||||
<b-card no-body header="Create a Galaxy account">
|
||||
<b-card-body>
|
||||
<b-form-group label="Email Address">
|
||||
<b-form-input name="email" type="text" v-model="email" />
|
||||
</b-form-group>
|
||||
<b-form-group label="Password">
|
||||
<b-form-input name="password" type="password" v-model="password" />
|
||||
</b-form-group>
|
||||
<b-form-group label="Confirm password">
|
||||
<b-form-input name="confirm" type="password" v-model="confirm" />
|
||||
</b-form-group>
|
||||
<b-form-group label="Public name">
|
||||
<b-form-input name="username" type="text" v-model="username" />
|
||||
<b-form-text
|
||||
>Your public name is an identifier that will be used to generate addresses for
|
||||
information you share publicly. Public names must be at least three characters in
|
||||
length and contain only lower-case letters, numbers, dots, underscores, and dashes
|
||||
('.', '_', '-').</b-form-text
|
||||
>
|
||||
</b-form-group>
|
||||
<b-form-group v-if="mailing_join_addr && smtp_server" label="Subscribe to mailing list">
|
||||
<input name="subscribe" type="checkbox" v-model="subscribe" />
|
||||
</b-form-group>
|
||||
<b-button name="create" type="submit">Create</b-button>
|
||||
</b-card-body>
|
||||
<b-card-footer>
|
||||
Already have an account?
|
||||
<a id="login-toggle" href="#" @click.prevent="toggleLogin">Log in here.</a>
|
||||
</b-card-footer>
|
||||
</b-card>
|
||||
</b-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
registration_warning_message: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
smtp_server: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
mailing_join_addr: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
redirect: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let galaxy = getGalaxyInstance();
|
||||
return {
|
||||
email: null,
|
||||
password: null,
|
||||
username: null,
|
||||
confirm: null,
|
||||
subscribe: null,
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
session_csrf_token: galaxy.session_csrf_token
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageShow() {
|
||||
return this.messageText != null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleLogin: function() {
|
||||
if (this.$root.toggleLogin) {
|
||||
this.$root.toggleLogin();
|
||||
}
|
||||
},
|
||||
submit: function(method) {
|
||||
let rootUrl = getAppRoot();
|
||||
axios
|
||||
.post(`${rootUrl}user/create`, this.$data)
|
||||
.then(response => {
|
||||
if (response.data.message && response.data.status) {
|
||||
alert(response.data.message);
|
||||
}
|
||||
window.location = this.redirect || rootUrl;
|
||||
})
|
||||
.catch(error => {
|
||||
this.messageVariant = "danger";
|
||||
let message = error.response.data && error.response.data.err_msg;
|
||||
this.messageText = message || "Registration failed for an unknown reason.";
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -9,6 +9,7 @@ import DataTables from "components/admin/DataTables.vue";
|
||||
import DataTypes from "components/admin/DataTypes.vue";
|
||||
import DataManagerView from "components/admin/DataManager/DataManagerView.vue";
|
||||
import DataManagerRouter from "components/admin/DataManager/DataManagerRouter.vue";
|
||||
import Register from "components/login/Register.vue";
|
||||
import ErrorStack from "components/admin/ErrorStack.vue";
|
||||
import DisplayApplications from "components/admin/DisplayApplications.vue";
|
||||
import Vue from "vue";
|
||||
@@ -20,6 +21,7 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
routes: {
|
||||
"(/)admin(/)": "home",
|
||||
"(/)admin(/)users": "show_users",
|
||||
"(/)admin(/)users(/)create": "show_users_create",
|
||||
"(/)admin(/)roles": "show_roles",
|
||||
"(/)admin(/)groups": "show_groups",
|
||||
"(/)admin(/)error_stack": "show_error_stack",
|
||||
@@ -54,6 +56,20 @@ export const getAdminRouter = (Galaxy, options) => {
|
||||
this._show_grid_view("admin/users_list");
|
||||
},
|
||||
|
||||
show_users_create: function() {
|
||||
let instance = Vue.extend(Register);
|
||||
let vm = document.createElement("div");
|
||||
this.page.display(vm);
|
||||
new instance({
|
||||
propsData: {
|
||||
redirect: "/admin/users",
|
||||
registration_warning_message: options.config.registration_warning_message,
|
||||
mailing_join_addr: options.config.mailing_join_addr,
|
||||
smtp_server: options.config.smtp_server
|
||||
}
|
||||
}).$mount(vm);
|
||||
},
|
||||
|
||||
show_roles: function() {
|
||||
this._show_grid_view("admin/roles_list");
|
||||
},
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { standardInit, addInitialization } from "onload";
|
||||
import Page from "layout/page";
|
||||
import Login from "components/login/Login.vue";
|
||||
import Index from "components/login/Index.vue";
|
||||
import Password from "components/login/Password.vue";
|
||||
import Vue from "vue";
|
||||
|
||||
@@ -9,7 +9,7 @@ export function initLoginView(Galaxy, { options }) {
|
||||
Galaxy.page = new Page.View(options);
|
||||
var vm = document.createElement("div");
|
||||
Galaxy.display(vm);
|
||||
var component = Galaxy.params.token || Galaxy.params.expired_user ? Password : Login;
|
||||
var component = Galaxy.params.token || Galaxy.params.expired_user ? Password : Index;
|
||||
var loginInstance = Vue.extend(component);
|
||||
new loginInstance({ propsData: options }).$mount(vm);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Backbone from "backbone";
|
||||
import $ from "jquery";
|
||||
import Tools from "mvc/tool/tools";
|
||||
import Upload from "mvc/upload/upload-view";
|
||||
import _l from "utils/localization";
|
||||
@@ -6,6 +7,7 @@ import _l from "utils/localization";
|
||||
import _ from "libs/underscore";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload";
|
||||
import Buttons from "mvc/ui/ui-buttons";
|
||||
|
||||
var ToolPanel = Backbone.View.extend({
|
||||
initialize: function(page, options) {
|
||||
@@ -42,13 +44,25 @@ var ToolPanel = Backbone.View.extend({
|
||||
default_extension: config.default_extension
|
||||
});
|
||||
|
||||
// add favorite filter button
|
||||
this.favorite_button = new Buttons.ButtonLink({
|
||||
cls: "panel-header-button",
|
||||
title: _l("Show favorites"),
|
||||
icon: "fa fa-star-o",
|
||||
onclick: e => {
|
||||
$("#tool-search-query")
|
||||
.val("#favorites")
|
||||
.trigger("change");
|
||||
}
|
||||
});
|
||||
|
||||
// add uploader button to Galaxy object
|
||||
Galaxy.upload = this.upload_button;
|
||||
|
||||
// components for panel definition
|
||||
this.model = new Backbone.Model({
|
||||
title: _l("Tools"),
|
||||
buttons: [this.upload_button]
|
||||
buttons: [this.upload_button, this.favorite_button]
|
||||
});
|
||||
|
||||
// build body template
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -28,7 +28,7 @@ export var CommunicationServerView = Backbone.View.extend({
|
||||
var $el_chat_modal_header = null;
|
||||
var $el_chat_modal_body = null;
|
||||
|
||||
var iframe_template = `<iframe class="f-iframe fade in communication-iframe" src="${src}"> </iframe>`;
|
||||
var iframe_template = `<iframe class="h-100 w-100" src="${src}"> </iframe>`;
|
||||
|
||||
var header_template =
|
||||
'<i class="fa fa-comment" aria-hidden="true" title="Communicate with other users"></i>' +
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Masthead Collection **/
|
||||
import _ from "underscore";
|
||||
import $ from "jquery";
|
||||
import axios from "axios";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
@@ -9,6 +10,18 @@ import { CommunicationServerView } from "layout/communication-server-view";
|
||||
import Webhooks from "mvc/webhooks";
|
||||
import Utils from "utils/utils";
|
||||
|
||||
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`;
|
||||
});
|
||||
}
|
||||
|
||||
var Collection = Backbone.Collection.extend({
|
||||
model: Backbone.Model.extend({
|
||||
defaults: {
|
||||
@@ -35,8 +48,7 @@ var Collection = Backbone.Collection.extend({
|
||||
id: "analysis",
|
||||
title: _l("Analyze Data"),
|
||||
url: "",
|
||||
tooltip: _l("Analysis home view"),
|
||||
target: "__use_router__"
|
||||
tooltip: _l("Analysis home view")
|
||||
});
|
||||
|
||||
//
|
||||
@@ -208,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"),
|
||||
@@ -241,21 +239,8 @@ var Collection = Backbone.Collection.extend({
|
||||
id: "user",
|
||||
title: _l("Login or Register"),
|
||||
cls: "loggedout-only",
|
||||
tooltip: _l("Account registration or login"),
|
||||
menu: [
|
||||
{
|
||||
title: _l("Login"),
|
||||
url: "login",
|
||||
target: "_top",
|
||||
noscratchbook: true
|
||||
},
|
||||
{
|
||||
title: _l("Register"),
|
||||
url: "user/create",
|
||||
target: "galaxy_main",
|
||||
noscratchbook: true
|
||||
}
|
||||
]
|
||||
url: "login",
|
||||
tooltip: _l("Account registration or login")
|
||||
};
|
||||
} else {
|
||||
userTab = {
|
||||
@@ -290,9 +275,8 @@ var Collection = Backbone.Collection.extend({
|
||||
},
|
||||
{
|
||||
title: _l("Logout"),
|
||||
url: `user/logout?session_csrf_token=${Galaxy.session_csrf_token}`,
|
||||
target: "_top",
|
||||
divider: true
|
||||
divider: true,
|
||||
onclick: logoutClick
|
||||
},
|
||||
{
|
||||
title: _l("Saved Datasets"),
|
||||
@@ -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);
|
||||
}
|
||||
@@ -476,10 +460,7 @@ var Tab = Backbone.View.extend({
|
||||
.popover({
|
||||
html: true,
|
||||
placement: "bottom",
|
||||
content: `Please ${this.buildLink("login", "login")} or ${this.buildLink(
|
||||
"register",
|
||||
"user/create?use_panels=True"
|
||||
)} to use this feature.`
|
||||
content: `Please ${this.buildLink("login or register", "login")} to use this feature.`
|
||||
})
|
||||
.popover("show");
|
||||
window.setTimeout(() => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -48,7 +48,7 @@ var View = Backbone.View.extend({
|
||||
var err_msg = response.responseJSON && response.responseJSON.err_msg;
|
||||
self.message.update({
|
||||
status: "danger",
|
||||
message: err_msg || "Error occured while loading the dataset."
|
||||
message: err_msg || "Error occurred while loading the dataset."
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -73,7 +73,7 @@ var View = Backbone.View.extend({
|
||||
var err_msg = response.responseJSON && response.responseJSON.err_msg;
|
||||
self.message.update({
|
||||
status: "danger",
|
||||
message: err_msg || "Error occured while editing the dataset attributes."
|
||||
message: err_msg || "Error occurred while editing the dataset attributes."
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ var View = Backbone.View.extend({
|
||||
error: response => {
|
||||
var error_response = {
|
||||
status: "error",
|
||||
message: "Error occured while loading the job.",
|
||||
message: "Error occurred while loading the job.",
|
||||
persistent: true,
|
||||
cls: "errormessage"
|
||||
};
|
||||
@@ -47,7 +47,7 @@ var View = Backbone.View.extend({
|
||||
error: response => {
|
||||
var error_response = {
|
||||
status: "error",
|
||||
message: "Error occured while loading the dataset.",
|
||||
message: "Error occurred while loading the dataset.",
|
||||
persistent: true,
|
||||
cls: "errormessage"
|
||||
};
|
||||
@@ -61,7 +61,7 @@ var View = Backbone.View.extend({
|
||||
this.$el.empty().append(`
|
||||
${this._templateHeader()}
|
||||
<h2>Dataset Error</h2>
|
||||
<p>An error occured while running the tool <b>${job.tool_id}</b>.</p>
|
||||
<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>
|
||||
|
||||
@@ -194,7 +194,7 @@ var View = Backbone.View.extend({
|
||||
error: response => {
|
||||
var error_response = {
|
||||
status: "error",
|
||||
message: "Error occured while saving. Please fill all the required fields and try again.",
|
||||
message: "Error occurred while saving. Please fill all the required fields and try again.",
|
||||
persistent: true,
|
||||
cls: "errormessage"
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
@@ -483,8 +527,7 @@ var HistoryViewEdit = _super.extend(
|
||||
this.$list().before([this._renderDropTargetHelp(), $dropTarget]);
|
||||
for (var evName in dropHandlers) {
|
||||
if (dropHandlers.hasOwnProperty(evName)) {
|
||||
//console.debug( evName, dropHandlers[ evName ] );
|
||||
$dropTarget.on(evName, dropHandlers[evName]);
|
||||
$dropTarget.get(0).addEventListener(evName, dropHandlers[evName]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
@@ -551,8 +594,7 @@ var HistoryViewEdit = _super.extend(
|
||||
drop: function(ev) {
|
||||
ev.preventDefault();
|
||||
//ev.stopPropagation();
|
||||
|
||||
var dataTransfer = ev.originalEvent.dataTransfer;
|
||||
var dataTransfer = ev.dataTransfer;
|
||||
var data = dataTransfer.getData("text");
|
||||
|
||||
dataTransfer.dropEffect = "move";
|
||||
|
||||
@@ -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 occured. 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 occured 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 occured! 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 occured! 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 occured 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 occured.");
|
||||
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 occured.");
|
||||
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();
|
||||
@@ -1411,92 +1423,81 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
|
||||
templateToolBar: function() {
|
||||
return _.template(
|
||||
[
|
||||
'<div class="library_style_container">', // container start
|
||||
'<div class="d-flex align-items-center mb-2">',
|
||||
'<span class="mr-1">DATA LIBRARIES</span>',
|
||||
"<div>", // toolbar start
|
||||
'<form class="form-inline">',
|
||||
'<div class="form-check logged-dataset-manipulation mr-1" style="display:none;">', // include deleted checkbox
|
||||
'<input class="form-check-input include-deleted-datasets-chk" id="include_deleted_datasets_chk" type="checkbox">',
|
||||
'<label class="form-check-label" for="include_deleted_datasets_chk">include deleted</label>',
|
||||
"</div>",
|
||||
'<button style="display:none;" title="Create new folder" class="btn btn-secondary toolbtn-create-folder add-library-items add-library-items-folder mr-1" type="button">',
|
||||
'<span class="fa fa-plus"></span> <span class="fa fa-folder"></span> Create Folder ',
|
||||
"</button>",
|
||||
"<% if(multiple_add_dataset_options) { %>", // add datasets button
|
||||
'<div data-toggle="tooltip" data-placement="right" title="Add datasets to current folder" class="dropdown add-library-items add-library-items-datasets mr-1" style="display:none;">',
|
||||
'<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown">',
|
||||
'<span class="fa fa-plus"></span> <span class="fa fa-file"></span> Add Datasets <span class="caret"></span>',
|
||||
"</button>",
|
||||
'<div class="dropdown-menu">',
|
||||
'<a class="dropdown-item" href="#folders/<%= id %>/import/history"> from History</a>',
|
||||
"<% if(Galaxy.config.user_library_import_dir !== null) { %>",
|
||||
'<a class="dropdown-item" href="#folders/<%= id %>/import/userdir"> from User Directory</a>',
|
||||
"<% } %>",
|
||||
"<% if(Galaxy.config.library_import_dir !== null || Galaxy.config.allow_library_path_paste) { %>",
|
||||
'<h5 class="dropdown-header">Admins only</h5>',
|
||||
"<% if(Galaxy.config.library_import_dir !== null) { %>",
|
||||
'<a class="dropdown-item" href="#folders/<%= id %>/import/importdir">from Import Directory</a>',
|
||||
"<% } %>",
|
||||
"<% if(Galaxy.config.allow_library_path_paste) { %>",
|
||||
'<a class="dropdown-item" href="#folders/<%= id %>/import/path">from Path</a>',
|
||||
"<% } %>",
|
||||
"<% } %>",
|
||||
"</div>",
|
||||
"</div>",
|
||||
|
||||
"<% } else { %>",
|
||||
'<a data-placement="top" title="Add Datasets to Current Folder" style="display:none;" class="btn btn-secondary add-library-items add-library-items-datasets" href="#folders/<%= id %>/import/history" role="button">',
|
||||
'<span class="fa fa-plus"></span><span class="fa fa-file"></span>',
|
||||
"</a>",
|
||||
"<% } %>",
|
||||
// import to history button
|
||||
'<div data-toggle="tooltip" data-placement="right" title="Import to history" class="dropdown mr-1">',
|
||||
'<button type="button" class="primary-button dropdown-toggle add-to-history" data-toggle="dropdown">',
|
||||
'<span class="fa fa-book"></span> To History <span class="caret"></span>',
|
||||
"</button>",
|
||||
'<div class="dropdown-menu" role="menu">',
|
||||
'<a href="" class="toolbtn-bulk-import add-to-history-datasets dropdown-item">as Datasets</a>',
|
||||
'<a href="" class="toolbtn-collection-import add-to-history-collection dropdown-item">as a Collection</a>',
|
||||
"</div>",
|
||||
"</div>",
|
||||
// download button
|
||||
'<div data-toggle="tooltip" data-placement="right" title="Download items as archive" class="dropdown dataset-manipulation mr-1" style="display:none; ">',
|
||||
'<button type="button" class="primary-button dropdown-toggle" data-toggle="dropdown">',
|
||||
'<span class="fa fa-save"></span> Download <span class="caret"></span>',
|
||||
"</button>",
|
||||
'<div class="dropdown-menu" role="menu">',
|
||||
'<a class="dropdown-item" href="#/folders/<%= id %>/download/tgz">.tar.gz</a>',
|
||||
'<a class="dropdown-item" href="#/folders/<%= id %>/download/tbz">.tar.bz</a>',
|
||||
'<a class="dropdown-item" href="#/folders/<%= id %>/download/zip">.zip</a>',
|
||||
"</div>",
|
||||
"</div>",
|
||||
// delete button
|
||||
'<button data-toggle="tooltip" data-placement="right" title="Mark items deleted" class="primary-button toolbtn-bulk-delete logged-dataset-manipulation mr-1" style="display:none;" type="button">',
|
||||
'<span class="fa fa-trash"></span> Delete',
|
||||
"</button>",
|
||||
'<span class="mr-1" data-toggle="tooltip" data-placement="right" title="Show location details">', // location button
|
||||
'<button data-id="<%- id %>" class="primary-button toolbtn-show-locinfo" type="button">',
|
||||
'<span class="fa fa-info-circle"></span>',
|
||||
" Details",
|
||||
"</button>",
|
||||
"</span>",
|
||||
'<span data-toggle="tooltip" data-placement="right" title="See this screen annotated">', // help button
|
||||
'<a class="library-help-button" href="https://galaxyproject.org/data-libraries/screen/folder-contents/" target="_blank">',
|
||||
'<button class="primary-button" type="button">',
|
||||
'<span class="fa fa-question-circle"></span>',
|
||||
" Help",
|
||||
"</button>",
|
||||
"</a>",
|
||||
"</span>",
|
||||
"</form>",
|
||||
"</div>", // toolbar end
|
||||
"</div>", // end flex
|
||||
'<div id="folder_items_element" />', // folder contents will append here
|
||||
'<div class="d-flex justify-content-center align-items-center folder-paginator mt-2 mb-2" />', // paginator will append here
|
||||
"</div>" // container end
|
||||
].join("")
|
||||
`<div class="library_style_container">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<a class="mr-1 btn btn-secondary" href="list" data-toggle="tooltip" title="Go to first page">
|
||||
<span class="fa fa-home"/>
|
||||
</a>
|
||||
<a class="mr-1 btn btn-secondary" data-toggle="tooltip" title="See this screen annotated" href="https://galaxyproject.org/data-libraries/screen/folder-contents/" target="_blank">
|
||||
<span class="fa fa-question"/>
|
||||
</a>
|
||||
<div>
|
||||
<form class="form-inline">
|
||||
<div class="form-check logged-dataset-manipulation mr-1" style="display:none;">
|
||||
<input class="form-check-input include-deleted-datasets-chk" id="include_deleted_datasets_chk" type="checkbox">
|
||||
<label class="form-check-label" for="include_deleted_datasets_chk">include deleted</label>
|
||||
</div>
|
||||
<button style="display:none;" title="Create new folder" class="btn btn-secondary toolbtn-create-folder add-library-items add-library-items-folder mr-1" type="button">
|
||||
<span class="fa fa-folder"/> Create Folder</button>
|
||||
<% if(multiple_add_dataset_options) { %>
|
||||
<div title="Add datasets to current folder" class="dropdown add-library-items add-library-items-datasets mr-1" style="display:none;">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="fa fa-file"/> Add Datasets <span class="caret"/>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#folders/<%= id %>/import/history"> from History</a>
|
||||
<% if(Galaxy.config.user_library_import_dir !== null) { %>
|
||||
<a class="dropdown-item" href="#folders/<%= id %>/import/userdir"> from User Directory</a>
|
||||
<% } %>
|
||||
<% if(Galaxy.config.library_import_dir !== null || Galaxy.config.allow_library_path_paste) { %>
|
||||
<h5 class="dropdown-header">Admins only</h5>
|
||||
<% if(Galaxy.config.library_import_dir !== null) { %>
|
||||
<a class="dropdown-item" href="#folders/<%= id %>/import/importdir">from Import Directory</a>
|
||||
<% } %>
|
||||
<% if(Galaxy.config.allow_library_path_paste) { %>
|
||||
<a class="dropdown-item" href="#folders/<%= id %>/import/path">from Path</a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<a title="Add Datasets to Current Folder" style="display:none;" class="btn btn-secondary add-library-items add-library-items-datasets mr-1" href="#folders/<%= id %>/import/history" role="button">
|
||||
<span class="fa fa-file"/> Add Datasets
|
||||
</a>
|
||||
<% } %>
|
||||
<div class="dropdown mr-1">
|
||||
<button type="button" class="primary-button dropdown-toggle add-to-history" data-toggle="dropdown">
|
||||
<span class="fa fa-book"></span> Export to History <span class="caret"/>
|
||||
</button>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<a href="#" class="toolbtn-bulk-import add-to-history-datasets dropdown-item">as Datasets</a>
|
||||
<a href="#" class="toolbtn-collection-import add-to-history-collection dropdown-item">as a Collection</a>
|
||||
</div>
|
||||
</div>
|
||||
<div title="Download items as archive" class="dropdown dataset-manipulation mr-1" style="display:none; ">
|
||||
<button type="button" class="primary-button dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="fa fa-save"/> Download <span class="caret"/>
|
||||
</button>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<a class="dropdown-item" href="#/folders/<%= id %>/download/tgz">.tar.gz</a>
|
||||
<a class="dropdown-item" href="#/folders/<%= id %>/download/tbz">.tar.bz</a>
|
||||
<a class="dropdown-item" href="#/folders/<%= id %>/download/zip">.zip</a>
|
||||
</div>
|
||||
</div>
|
||||
<button data-toggle="tooltip" title="Mark items deleted" class="primary-button toolbtn-bulk-delete logged-dataset-manipulation mr-1" style="display:none;" type="button">
|
||||
<span class="fa fa-trash"/> Delete
|
||||
</button>
|
||||
<span class="mr-1" data-toggle="tooltip" title="Show location details">
|
||||
<button data-id="<%- id %>" class="primary-button toolbtn-show-locinfo" type="button">
|
||||
<span class="fa fa-info-circle"/> Details
|
||||
</button>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="folder_items_element" />
|
||||
<div class="d-flex justify-content-center align-items-center folder-paginator mt-2 mb-2" />
|
||||
</div>`
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -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 occured 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 occured 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 occured 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 occured.");
|
||||
Toast.error("An error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mod_toastr.error("Library's name is missing.");
|
||||
Toast.error("Library's name is missing.");
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -207,32 +207,37 @@ var LibraryToolbarView = Backbone.View.extend({
|
||||
|
||||
templateToolBar: function() {
|
||||
return _.template(
|
||||
[
|
||||
'<div class="library_style_container">',
|
||||
'<div class="d-flex align-items-center mb-2">',
|
||||
'<a class="mr-1" href="#" data-toggle="tooltip" data-placement="top" title="Go to first page">DATA LIBRARIES</a>',
|
||||
'<div class="d-flex align-items-center library-paginator mr-1" />', // paginator will append here
|
||||
'<form class="form-inline mr-1">',
|
||||
'<input type="text" class="form-control library-search-input mr-1" placeholder="Great Library" size="15">',
|
||||
"<% if(admin_user === true) { %>", // only admins
|
||||
'<div class="form-check mr-1">',
|
||||
'<input class="form-check-input" id="include_deleted_chk" type="checkbox">',
|
||||
'<label class="form-check-label" for="include_deleted_chk">include deleted</label>',
|
||||
"</div>",
|
||||
'<div class="form-check mr-1">',
|
||||
'<input class="form-check-input" id="exclude_restricted_chk" type="checkbox">',
|
||||
'<label class="form-check-label" for="exclude_restricted_chk">exclude restricted</label>',
|
||||
"</div>",
|
||||
'<button data-toggle="tooltip" data-placement="top" title="Create new library" id="create_new_library_btn" class="btn btn-secondary" type="button"><span class="fa fa-plus"></span> New Library</button>',
|
||||
"<% } %>", // end admins
|
||||
"</form>",
|
||||
'<a class="library-help-button" data-toggle="tooltip" data-placement="top" title="See this screen annotated" href="https://galaxyproject.org/data-libraries/screen/list-of-libraries/" target="_blank">',
|
||||
'<button class="btn btn-secondary" type="button"><span class="fa fa-question-circle"></span> Help</button>',
|
||||
"</a>",
|
||||
"</div>", // end flex
|
||||
'<div id="libraries_element" />', // table with libraries will append here
|
||||
"</div>" // end library_style_container
|
||||
].join("")
|
||||
`<div class="library_style_container">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<a class="btn btn-secondary mr-1" data-toggle="tooltip" data-placement="top" title="Go to first page" href="#">
|
||||
<span class="fa fa-home"/>
|
||||
</a>
|
||||
<a class="library-help-button btn btn-secondary mr-1" data-toggle="tooltip" title="See this screen annotated" href="https://galaxyproject.org/data-libraries/screen/list-of-libraries/" target="_blank">
|
||||
<span class="fa fa-question"/>
|
||||
</a>
|
||||
<% if(admin_user === true) { %>
|
||||
<button data-toggle="tooltip" data-placement="top" title="Create new library" id="create_new_library_btn" class="mr-1 btn btn-secondary" type="button">
|
||||
<span class="fa fa-plus"/>
|
||||
</button>
|
||||
<% } %>
|
||||
<div class="d-flex align-items-center library-paginator mr-1" />
|
||||
<form class="form-inline mr-1">
|
||||
<input type="text" class="form-control library-search-input mr-1" placeholder="Great Library" size="15">
|
||||
<% if(admin_user === true) { %>
|
||||
<div class="form-check mr-1">
|
||||
<input class="form-check-input" id="include_deleted_chk" type="checkbox"/>
|
||||
<label class="form-check-label" for="include_deleted_chk">include deleted</label>
|
||||
</div>
|
||||
<div class="form-check mr-1">
|
||||
<input class="form-check-input" id="exclude_restricted_chk" type="checkbox"/>
|
||||
<label class="form-check-label" for="exclude_restricted_chk">exclude restricted</label>
|
||||
</div>
|
||||
<% } %>
|
||||
</form>
|
||||
</div>
|
||||
<div id="libraries_element" />
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -861,8 +861,8 @@ const colHeadersFor = function(data, columns) {
|
||||
}
|
||||
};
|
||||
|
||||
const applyRules = function(data, sources, columns, rules, colHeadersPerRule) {
|
||||
colHeadersPerRule = colHeadersPerRule || [];
|
||||
const applyRules = function(data, sources, columns, rules, headersPerRule = []) {
|
||||
let colHeadersPerRule = Array.from(headersPerRule);
|
||||
let hasRuleError = false;
|
||||
for (var ruleIndex in rules) {
|
||||
const ruleHeaders = colHeadersFor(data, columns);
|
||||
@@ -889,7 +889,7 @@ const applyRules = function(data, sources, columns, rules, colHeadersPerRule) {
|
||||
columns = res.columns || columns;
|
||||
}
|
||||
}
|
||||
return { data, sources, columns };
|
||||
return { data, sources, columns, colHeadersPerRule };
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -13,6 +13,7 @@ import FormBase from "mvc/form/form-view";
|
||||
import Webhooks from "mvc/webhooks";
|
||||
import Citations from "components/Citations.vue";
|
||||
import Vue from "vue";
|
||||
import axios from "axios";
|
||||
|
||||
export default FormBase.extend({
|
||||
initialize: function(options) {
|
||||
@@ -113,13 +114,51 @@ export default FormBase.extend({
|
||||
_operations: function() {
|
||||
var self = this;
|
||||
var options = this.model.attributes;
|
||||
let Galaxy = getGalaxyInstance();
|
||||
|
||||
// Buttons for adding and removing favorite.
|
||||
let in_favorites = Galaxy.user.getFavorites().tools.indexOf(options.id) >= 0;
|
||||
var favorite_button = new Ui.Button({
|
||||
icon: "fa-star-o",
|
||||
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 })
|
||||
.then(response => {
|
||||
favorite_button.hide();
|
||||
remove_favorite_button.show();
|
||||
Galaxy.user.updateFavorites("tools", response.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var remove_favorite_button = new Ui.Button({
|
||||
icon: "fa-star",
|
||||
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)}`
|
||||
)
|
||||
.then(response => {
|
||||
remove_favorite_button.hide();
|
||||
favorite_button.show();
|
||||
Galaxy.user.updateFavorites("tools", response.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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"
|
||||
});
|
||||
|
||||
if (!options.sustain_version && options.versions && options.versions.length > 1) {
|
||||
for (var i in options.versions) {
|
||||
var version = options.versions[i];
|
||||
@@ -145,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"),
|
||||
@@ -176,7 +199,6 @@ export default FormBase.extend({
|
||||
});
|
||||
|
||||
// add admin operations
|
||||
let Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.user && Galaxy.user.get("is_admin")) {
|
||||
menu_button.addMenu({
|
||||
icon: "fa-download",
|
||||
@@ -242,7 +264,9 @@ export default FormBase.extend({
|
||||
|
||||
return {
|
||||
menu: menu_button,
|
||||
versions: versions_button
|
||||
versions: versions_button,
|
||||
favorite: favorite_button,
|
||||
remove_favorite: remove_favorite_button
|
||||
};
|
||||
},
|
||||
|
||||
@@ -267,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() {
|
||||
|
||||
@@ -344,6 +344,8 @@ _.extend(ToolSection.prototype, VisibilityMixin);
|
||||
* query.
|
||||
*/
|
||||
var ToolSearch = Backbone.Model.extend({
|
||||
SEARCH_RESERVED_TERMS_FAVORITES: ["#favs", "#favorites", "#favourites"],
|
||||
|
||||
defaults: {
|
||||
search_hint_string: "search tools",
|
||||
min_chars_for_search: 3,
|
||||
@@ -365,6 +367,7 @@ var ToolSearch = Backbone.Model.extend({
|
||||
* Do the search and update the results.
|
||||
*/
|
||||
do_search: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
var query = this.attributes.query;
|
||||
|
||||
// If query is too short, do not search.
|
||||
@@ -379,26 +382,31 @@ var ToolSearch = Backbone.Model.extend({
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
// Start a new ajax-request in X ms
|
||||
$("#search-clear-btn").hide();
|
||||
$("#search-spinner").show();
|
||||
var self = this;
|
||||
this.timer = setTimeout(() => {
|
||||
// log the search to analytics if present
|
||||
if (typeof ga !== "undefined") {
|
||||
ga("send", "pageview", `${getAppRoot()}?q=${q}`);
|
||||
}
|
||||
$.get(
|
||||
self.urlRoot,
|
||||
{ q: q },
|
||||
data => {
|
||||
self.set("results", data);
|
||||
$("#search-spinner").hide();
|
||||
$("#search-clear-btn").show();
|
||||
},
|
||||
"json"
|
||||
);
|
||||
}, 400);
|
||||
// Catch reserved words
|
||||
if (this.SEARCH_RESERVED_TERMS_FAVORITES.indexOf(q) >= 0) {
|
||||
this.set("results", Galaxy.user.getFavorites().tools);
|
||||
} else {
|
||||
// Start a new ajax-request in X ms
|
||||
$("#search-clear-btn").hide();
|
||||
$("#search-spinner").show();
|
||||
var self = this;
|
||||
this.timer = setTimeout(() => {
|
||||
// log the search to analytics if present
|
||||
if (typeof ga !== "undefined") {
|
||||
ga("send", "pageview", `${getAppRoot()}?q=${q}`);
|
||||
}
|
||||
$.get(
|
||||
self.urlRoot,
|
||||
{ q: q },
|
||||
data => {
|
||||
self.set("results", data);
|
||||
$("#search-spinner").hide();
|
||||
$("#search-clear-btn").show();
|
||||
},
|
||||
"json"
|
||||
);
|
||||
}, 400);
|
||||
}
|
||||
},
|
||||
|
||||
clear_search: function() {
|
||||
|
||||
@@ -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>`;
|
||||
},
|
||||
""
|
||||
)}
|
||||
|
||||
@@ -464,8 +464,7 @@ export default Backbone.View.extend({
|
||||
|
||||
/** Template */
|
||||
_template: function() {
|
||||
return (
|
||||
`<div class="upload-view-default">
|
||||
return `<div class="upload-view-default">
|
||||
<div class="upload-top">
|
||||
<div class="upload-top-info"/>
|
||||
</div>
|
||||
@@ -495,7 +494,6 @@ export default Backbone.View.extend({
|
||||
<span class="upload-footer-genome"/>
|
||||
</div>
|
||||
<div class="upload-buttons"/>
|
||||
</div>`
|
||||
);
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -285,8 +285,7 @@ export default Backbone.View.extend({
|
||||
|
||||
/** Template */
|
||||
_template: function() {
|
||||
return (
|
||||
`<tr class="upload-row">
|
||||
return `<tr class="upload-row">
|
||||
<td>
|
||||
<div class="upload-source"/>
|
||||
<div class="upload-text-column">
|
||||
@@ -320,7 +319,6 @@ export default Backbone.View.extend({
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`
|
||||
);
|
||||
</tr>`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -176,8 +176,7 @@ export default Backbone.View.extend({
|
||||
|
||||
/** Load html template */
|
||||
_template: function() {
|
||||
return (
|
||||
`<div class="upload-view-composite">
|
||||
return `<div class="upload-view-composite">
|
||||
<div class="upload-top">
|
||||
<div class="upload-top-info"> </div>
|
||||
</div>
|
||||
@@ -205,7 +204,6 @@ export default Backbone.View.extend({
|
||||
<span class="upload-footer-genome"/>
|
||||
</div>
|
||||
<div class="upload-buttons"/>
|
||||
</div>`
|
||||
);
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ var View = Backbone.View.extend({
|
||||
this.model =
|
||||
(options && options.model) ||
|
||||
new Backbone.Model({
|
||||
icon: "fa-upload",
|
||||
tooltip: _l("Download from URL or upload files from disk"),
|
||||
label: "Load Data",
|
||||
percentage: 0,
|
||||
@@ -46,16 +45,14 @@ var View = Backbone.View.extend({
|
||||
|
||||
/** Template */
|
||||
_template: function() {
|
||||
return (
|
||||
'<div class="upload-button">' +
|
||||
'<div class="progress">' +
|
||||
'<div class="progress-bar"/>' +
|
||||
'<a class="panel-header-button" href="javascript:void(0)" id="tool-panel-upload-button">' +
|
||||
'<span class="fa fa-upload"/>' +
|
||||
"</a>" +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
);
|
||||
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>`;
|
||||
}
|
||||
});
|
||||
export default { View: View };
|
||||
|
||||
@@ -28,7 +28,8 @@ var User = Backbone.Model.extend(baseMVC.LoggableMixin).extend(
|
||||
total_disk_usage: 0,
|
||||
nice_total_disk_usage: "",
|
||||
quota_percent: null,
|
||||
is_admin: false
|
||||
is_admin: false,
|
||||
preferences: {}
|
||||
},
|
||||
|
||||
/** Set up and bind events
|
||||
@@ -53,6 +54,29 @@ var User = Backbone.Model.extend(baseMVC.LoggableMixin).extend(
|
||||
return this.get("is_admin");
|
||||
},
|
||||
|
||||
updatePreferences: function(name, new_value) {
|
||||
let preferences = this.get("preferences");
|
||||
preferences[name] = JSON.stringify(new_value);
|
||||
this.preferences = preferences;
|
||||
},
|
||||
|
||||
getFavorites: function() {
|
||||
let preferences = this.get("preferences");
|
||||
if (preferences && preferences.favorites) {
|
||||
return JSON.parse(preferences.favorites);
|
||||
} else {
|
||||
return {
|
||||
tools: []
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
updateFavorites: function(object_type, new_favorites) {
|
||||
let favorites = this.getFavorites();
|
||||
favorites[object_type] = new_favorites[object_type];
|
||||
this.updatePreferences("favorites", favorites);
|
||||
},
|
||||
|
||||
/** Load a user with the API using an id.
|
||||
* If getting an anonymous user or no access to a user id, pass the User.CURRENT_ID_STR
|
||||
* (e.g. 'current') and the API will return the current transaction's user data.
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user