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:
Dave B
2019-04-15 11:45:36 -04:00
786 changed files with 36510 additions and 20549 deletions
+1 -9
View File
@@ -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 "$@"
+1 -9
View File
@@ -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 "$@"
+212
View File
@@ -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
View File
@@ -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
View File
@@ -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
+17 -15
View File
@@ -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}
}
+1
View File
@@ -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>
+6 -7
View File
@@ -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
View File
@@ -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
-12
View File
@@ -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/*"]
}
+1
View File
@@ -0,0 +1 @@
10.13.0
+11 -13
View File
@@ -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
-1
View File
@@ -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",
+13 -7
View File
@@ -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"),
+2 -2
View File
@@ -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() {
+2 -2
View File
@@ -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);
}
+2 -2
View File
@@ -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>' +
+11 -5
View File
@@ -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);
}
+21 -40
View File
@@ -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(() => {
+12 -3
View File
@@ -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();
+12 -1
View File
@@ -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
-307
View File
@@ -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>&times;</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. */
+24 -2
View File
@@ -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 */
+20 -18
View File
@@ -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) {
+1 -1
View File
@@ -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;
+47 -14
View File
@@ -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 -2
View File
@@ -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>
`;
+18 -2
View File
@@ -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>',
"&nbsp;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>',
"&nbsp;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>',
"&nbsp;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>',
"&nbsp;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"/>&nbsp;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() {
+28 -20
View File
@@ -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">&hellip;</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">&hellip;</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>",
+42 -21
View File
@@ -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");
}
});
});
}
});
+20 -16
View File
@@ -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 */
+14 -2
View File
@@ -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");
}
});
+117 -70
View File
@@ -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}&nbsp;<div style="${Utils.generateTagStyle(
tag.slice(5)
)}" class="badge badge-primary badge-tags">${_.escape(tag)}</div>`;
const tagColors = keyedColorScheme(tag.slice(5));
return `${memo}&nbsp;<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">&nbsp;</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 };
+25 -1
View File
@@ -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