mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-01 15:37:32 +08:00
Merge branch 'dev' of github.com:galaxyproject/galaxy into cellxgene-gie
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
# Python CircleCI 2.0 configuration file
|
||||
version: 2
|
||||
variables:
|
||||
restore_repo_cache: &restore_repo_cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v1-repo-{{ .Environment.CIRCLE_SHA1 }}
|
||||
restore_yarn_cache: &restore_yarn_cache
|
||||
restore_cache:
|
||||
keys:
|
||||
- v1-repo-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- yarn-packages-{{ checksum "client/yarn.lock" }}
|
||||
save_yarn_cache: &save_yarn_cache
|
||||
save_cache:
|
||||
key: yarn-packages-{{ checksum "client/yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
install_tox: &install_tox
|
||||
run: sudo pip install tox
|
||||
set_workdir: &set_workdir
|
||||
working_directory: ~/repo
|
||||
requires_get_code: &requires_get_code
|
||||
requires:
|
||||
- get_code
|
||||
jobs:
|
||||
get_code:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
# Replace standard code checkout with shallow clone to speed things up.
|
||||
- run:
|
||||
name: Checkout code
|
||||
command: |-
|
||||
# Add github.com to known hosts
|
||||
mkdir -p ~/.ssh
|
||||
echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
|
||||
' >> ~/.ssh/known_hosts
|
||||
|
||||
# Add the user ssh key and set correct perms
|
||||
(umask 077; touch ~/.ssh/id_rsa)
|
||||
chmod 0600 ~/.ssh/id_rsa
|
||||
echo "$CHECKOUT_KEY" > ~/.ssh/id_rsa
|
||||
|
||||
# Use git+ssh instead of https
|
||||
git config --global url."ssh://git@github.com".insteadOf "https://github.com" || true
|
||||
git config --global gc.auto 0 || true
|
||||
|
||||
# Shallow clone
|
||||
git clone --depth=1 "${CIRCLE_REPOSITORY_URL}" .
|
||||
|
||||
if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then
|
||||
# Update PR refs for testing.
|
||||
FETCH_REFS="${FETCH_REFS} +refs/pull/${CIRCLE_PR_NUMBER}/head:pr/${CIRCLE_PR_NUMBER}/head"
|
||||
FETCH_REFS="${FETCH_REFS} +refs/pull/${CIRCLE_PR_NUMBER}/merge:pr/${CIRCLE_PR_NUMBER}/merge"
|
||||
|
||||
# Retrieve the refs
|
||||
git fetch --force origin ${FETCH_REFS}
|
||||
|
||||
# Checkout PR merge ref.
|
||||
git checkout -f "pr/${CIRCLE_PR_NUMBER}/merge"
|
||||
|
||||
# Test for *some* merge conflicts.
|
||||
git branch --merged | grep "pr/${CIRCLE_PR_NUMBER}/head" > /dev/null
|
||||
else
|
||||
if [ -n "$CIRCLE_TAG" ]; then
|
||||
git fetch --depth=1 --force origin "refs/tags/${CIRCLE_TAG}"
|
||||
else
|
||||
git fetch --depth=1 --force origin "$CIRCLE_BRANCH:remotes/origin/$CIRCLE_BRANCH"
|
||||
fi
|
||||
if [ -n "$CIRCLE_TAG" ]; then
|
||||
git reset --hard "$CIRCLE_SHA1"
|
||||
git checkout "$CIRCLE_TAG"
|
||||
elif [ -n "$CIRCLE_BRANCH" ]; then
|
||||
git reset --hard "$CIRCLE_SHA1"
|
||||
git checkout -B "$CIRCLE_BRANCH"
|
||||
fi
|
||||
git reset --hard "${CIRCLE_SHA1}"
|
||||
fi
|
||||
- save_cache:
|
||||
key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
|
||||
paths:
|
||||
- ~/repo
|
||||
py27_lint:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py27-lint
|
||||
py27_unit:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py27-unit
|
||||
py27_docstring:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py27-lint_docstring_include_list
|
||||
py27_first_startup:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- run: sh scripts/common_startup.sh
|
||||
- run: wget -q https://github.com/jmchilton/galaxy-downloads/raw/master/db_gx_rev_0127.sqlite
|
||||
- run: mv db_gx_rev_0127.sqlite database/universe.sqlite
|
||||
- run: sh manage_db.sh -c ./config/galaxy.yml.sample upgrade
|
||||
- *install_tox
|
||||
- run: tox -e py27-first_startup
|
||||
py35_lint:
|
||||
docker:
|
||||
- image: circleci/python:3.5
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py35-lint
|
||||
py35_unit:
|
||||
docker:
|
||||
- image: circleci/python:3.5
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: tox -e py35-unit
|
||||
py35_first_startup:
|
||||
docker:
|
||||
- image: circleci/python:3.5
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- *install_tox
|
||||
- run: sudo apt-get update
|
||||
# For uwsgi
|
||||
- run: sudo apt-get install -y libpython3.5-dev
|
||||
- run: tox -e py35-first_startup
|
||||
validate_test_tools:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- run: sudo apt-get update
|
||||
- run: sudo apt-get install -y libxml2-utils
|
||||
- *install_tox
|
||||
- run: tox -e validate_test_tools
|
||||
check_py3_compatibility:
|
||||
docker:
|
||||
- image: circleci/python:2.7.15
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_repo_cache
|
||||
- run: sudo apt-get update
|
||||
- run: sudo apt-get install -y ack-grep
|
||||
- *install_tox
|
||||
- run: tox -e check_py3_compatibility
|
||||
js_unit:
|
||||
docker:
|
||||
- image: circleci/node:10-browsers
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_yarn_cache
|
||||
- run: cd client && yarn install --frozen-lockfile
|
||||
- *save_yarn_cache
|
||||
- run: cd client && yarn run build
|
||||
- run: cd client && yarn run test
|
||||
js_lint:
|
||||
docker:
|
||||
- image: circleci/node:10-browsers
|
||||
<<: *set_workdir
|
||||
steps:
|
||||
- *restore_yarn_cache
|
||||
- run: cd client && yarn install --frozen-lockfile
|
||||
- *save_yarn_cache
|
||||
- run: cd client && yarn run eslint
|
||||
workflows:
|
||||
version: 2
|
||||
get_code_and_test:
|
||||
jobs:
|
||||
- get_code
|
||||
- py27_lint:
|
||||
<<: *requires_get_code
|
||||
- py27_unit:
|
||||
<<: *requires_get_code
|
||||
- py27_first_startup:
|
||||
<<: *requires_get_code
|
||||
- py27_docstring:
|
||||
<<: *requires_get_code
|
||||
- py35_lint:
|
||||
<<: *requires_get_code
|
||||
- py35_unit:
|
||||
<<: *requires_get_code
|
||||
- py35_first_startup:
|
||||
<<: *requires_get_code
|
||||
- validate_test_tools:
|
||||
<<: *requires_get_code
|
||||
- check_py3_compatibility:
|
||||
<<: *requires_get_code
|
||||
- js_unit:
|
||||
<<: *requires_get_code
|
||||
- js_lint:
|
||||
<<: *requires_get_code
|
||||
-57
@@ -1,69 +1,12 @@
|
||||
language: python
|
||||
python: 2.7
|
||||
os:
|
||||
- linux
|
||||
env:
|
||||
- TOX_ENV=py27-lint
|
||||
- TOX_ENV=py27-unit
|
||||
- TOX_ENV=py27-first_startup
|
||||
- TOX_ENV=py27-lint_docstring_include_list
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- env: TOX_ENV=py35-lint
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- python3.5
|
||||
- env: TOX_ENV=py35-first_startup
|
||||
addons:
|
||||
&py3_addons
|
||||
apt:
|
||||
packages:
|
||||
- python3.5
|
||||
# For psutil, pyyaml, uwsgi...
|
||||
- libpython3.5-dev
|
||||
- env: TOX_ENV=validate_test_tools
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libxml2-utils
|
||||
- env: TOX_ENV=check_py3_compatibility
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- ack-grep
|
||||
- env: TOX_ENV=py35-unit
|
||||
addons: *py3_addons
|
||||
- os: osx
|
||||
# No version of Python is available via virtualenv on OS X workers, see https://github.com/travis-ci/travis-ci/issues/2312
|
||||
language: generic
|
||||
env: TOX_ENV=py27-first_startup
|
||||
- name: js-unit
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
before_install:
|
||||
- cd client
|
||||
install:
|
||||
- yarn
|
||||
- yarn run build
|
||||
script:
|
||||
- yarn run test
|
||||
- name: js-lint
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
before_install:
|
||||
- cd client
|
||||
install:
|
||||
- yarn
|
||||
script:
|
||||
- yarn run eslint
|
||||
|
||||
before_install:
|
||||
# Workaround for https://github.com/travis-ci/travis-ci/issues/7940
|
||||
- sudo rm -f /etc/boto.cfg
|
||||
|
||||
install:
|
||||
- set -e
|
||||
|
||||
@@ -49,6 +49,7 @@ The following individuals have contributed code to Galaxy:
|
||||
* Gianmauro Cuccuru <gmauro@crs4.it>
|
||||
* Frederik Delaere <frederik.delaere@gmail.com>
|
||||
* Matthias Desmet <matthias.desmet@ugent.be>
|
||||
* Matthew Ryan Dillon <matthewrdillon@gmail.com>
|
||||
* Olivia Doppelt <olivia.doppelt@pasteur.fr>
|
||||
* Shane Dowling <shane@shanedowling.com>
|
||||
* John Duddy <jduddy@illumina.com>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Location of virtualenv used for development.
|
||||
VENV?=.venv
|
||||
# Source virtualenv to execute command (flake8, sphinx, twine, etc...)
|
||||
IN_VENV=if [ -f $(VENV)/bin/activate ]; then . $(VENV)/bin/activate; fi;
|
||||
RELEASE_CURR:=16.01
|
||||
RELEASE_CURR_MINOR_NEXT:=$(shell python scripts/bootstrap_history.py --print-next-minor-version)
|
||||
RELEASE_CURR_MINOR_NEXT:=$(shell $(IN_VENV) python scripts/bootstrap_history.py --print-next-minor-version)
|
||||
RELEASE_NEXT:=16.04
|
||||
# TODO: This needs to be updated with create_release_rc
|
||||
#RELEASE_NEXT_BRANCH:=release_$(RELEASE_NEXT)
|
||||
RELEASE_NEXT_BRANCH:=dev
|
||||
RELEASE_UPSTREAM:=upstream
|
||||
MY_UPSTREAM:=origin
|
||||
# Location of virtualenv used for development.
|
||||
VENV?=.venv
|
||||
# Source virtualenv to execute command (flake8, sphinx, twine, etc...)
|
||||
IN_VENV=if [ -f $(VENV)/bin/activate ]; then . $(VENV)/bin/activate; fi;
|
||||
CONFIG_MANAGE=$(IN_VENV) python lib/galaxy/webapps/config_manage.py
|
||||
PROJECT_URL?=https://github.com/galaxyproject/galaxy
|
||||
DOCS_DIR=doc
|
||||
@@ -140,7 +140,7 @@ ifndef YARN
|
||||
@echo "Could not find yarn, which is required to build the Galaxy client.\nTo install yarn, please visit \033[0;34mhttps://yarnpkg.com/en/docs/install\033[0m for instructions, and package information for all platforms.\n"
|
||||
false;
|
||||
else
|
||||
cd client && yarn install --network-timeout 120000 --check-files
|
||||
cd client && yarn install --network-timeout 300000 --check-files
|
||||
endif
|
||||
|
||||
|
||||
|
||||
+2
-4
@@ -1,11 +1,9 @@
|
||||
.. figure:: https://galaxyproject.org/images/galaxy-logos/galaxy_project_logo.jpg
|
||||
:alt: Galaxy Logo
|
||||
|
||||
The latest information about Galaxy is available via `https://galaxyproject.org/ <https://galaxyproject.org/>`__
|
||||
The latest information about Galaxy can be found on the `Galaxy Community Hub <https://galaxyproject.org/>`__.
|
||||
|
||||
.. image:: https://img.shields.io/badge/questions-galaxy%20biostar-blue.svg
|
||||
:target: https://biostar.usegalaxy.org
|
||||
:alt: Ask a question
|
||||
Community support is available at `Galaxy Help <https://help.galaxyproject.org/>`__.
|
||||
|
||||
.. image:: https://img.shields.io/badge/chat-irc.freenode.net%23galaxyproject-blue.svg
|
||||
:target: https://webchat.freenode.net/?channels=galaxyproject
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": ["transform-vue-template", "@babel/plugin-syntax-dynamic-import"],
|
||||
"ignore": ["i18n.js", "utils/localization.js", "nls/*"]
|
||||
}
|
||||
+11
-13
@@ -2,32 +2,30 @@ Client Build System
|
||||
===================
|
||||
|
||||
Installs, stages, and builds the client-side scripts necessary for running the
|
||||
Galaxy webapp. There's no need to use this system unless you are modifying or
|
||||
developing client-side scripts, or are running the development branch of
|
||||
Galaxy. When started through `run.sh` or any other method that utilizes
|
||||
`common_startup.sh`, Galaxy will also (since 18.09) *automatically* build
|
||||
Galaxy webapp. When started through `run.sh` or any other method that utilizes
|
||||
`scripts/common_startup.sh`, Galaxy will (since 18.09) *automatically* build
|
||||
the client as a part of server startup, when it detects changes, unless that
|
||||
functionality is explicitly disabled.
|
||||
|
||||
The base dependencies used are Node.js and Yarn. Galaxy now includes these in
|
||||
the virtual environment, and they can be accessed by activating that with
|
||||
`source .venv/bin/activate` from the Galaxy root directory.
|
||||
`. .venv/bin/activate` from the Galaxy root directory.
|
||||
|
||||
If you'd like to install your own dependencies, on OSX the easiest way to get
|
||||
set up is using homebrew and the command `brew install nodejs yarn`. More
|
||||
information including instructions for other platforms is available at
|
||||
nodejs.org and yarnpkg.com.
|
||||
set up is using `homebrew` and the command `brew install nodejs yarn`. More
|
||||
information, including instructions for other platforms, is available at
|
||||
https://nodejs.org/ and https://yarnpkg.com/ .
|
||||
|
||||
The Galaxy client build has necessarily grown more complex in the past several
|
||||
years, but we're still trying to keep things as simple as possible for
|
||||
developers (everyone, really). If you're having any trouble with building the
|
||||
client after following the instructions below please create an issue on GitHub
|
||||
years, but we are still trying to keep things as simple as possible for
|
||||
everyone. If you're having any trouble with building the
|
||||
client after following the instructions below, please create an issue on GitHub
|
||||
or reach out for help directly on Gitter at
|
||||
https://gitter.im/galaxyproject/Lobby.
|
||||
https://gitter.im/galaxyproject/Lobby .
|
||||
|
||||
|
||||
Complete Client Build
|
||||
================================================
|
||||
=====================
|
||||
|
||||
There are many moving parts to the client build system, but the entry point for
|
||||
most people is the 'client' rule in the Makefile at the root of the Galaxy
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
<b-table
|
||||
small
|
||||
hover
|
||||
:items="items"
|
||||
:items="formatedItems"
|
||||
: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 }}
|
||||
<i v-if="isDataset(data.item)" 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 : "-" }}
|
||||
@@ -30,16 +30,42 @@
|
||||
<template slot="update_time" slot-scope="data">
|
||||
{{ data.value ? data.value.substring(0, 16).replace("T", " ") : "-" }}
|
||||
</template>
|
||||
<template slot="arrow" slot-scope="data">
|
||||
<b-button
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="py-0"
|
||||
v-if="!isDataset(data.item)"
|
||||
@click.stop="load(data.item.url)"
|
||||
>
|
||||
View
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
<div v-if="nItems == 0">No search results found for: {{ this.filter }}.</div>
|
||||
<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>
|
||||
</div>
|
||||
<div v-else><span class="fa fa-spinner fa-spin" /> <span>Please wait...</span></div>
|
||||
</div>
|
||||
<div 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="done" :disabled="values.length === 0">
|
||||
Ok
|
||||
</b-btn>
|
||||
<b-btn size="sm" class="float-right" @click="modalShow = false"> Cancel </b-btn>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
@@ -52,10 +78,20 @@ export default {
|
||||
callback: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: "url"
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: null,
|
||||
errorShow: false,
|
||||
fields: {
|
||||
name: {
|
||||
sortable: true
|
||||
@@ -65,53 +101,124 @@ export default {
|
||||
},
|
||||
update_time: {
|
||||
sortable: true
|
||||
},
|
||||
arrow: {
|
||||
label: "",
|
||||
sortable: false,
|
||||
class: "text-right"
|
||||
}
|
||||
},
|
||||
filter: null,
|
||||
nItems: 0,
|
||||
currentPage: 0,
|
||||
perPage: 10,
|
||||
items: [],
|
||||
errorMessage: null,
|
||||
errorShow: true,
|
||||
historyId: null,
|
||||
items: [],
|
||||
modalShow: true,
|
||||
optionsShow: false
|
||||
nItems: 0,
|
||||
optionsShow: false,
|
||||
undoShow: false,
|
||||
url: null,
|
||||
values: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
formatedItems() {
|
||||
for (let item of this.items) {
|
||||
if (this.isDataset(item)) {
|
||||
let key = item.id;
|
||||
item._rowVariant = this.values[key] ? "success" : "default";
|
||||
} else {
|
||||
item._rowVariant = "active";
|
||||
}
|
||||
}
|
||||
return this.items;
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
isDataset: function(item) {
|
||||
return item.history_content_type == "dataset";
|
||||
},
|
||||
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.";
|
||||
if (this.isDataset(record)) {
|
||||
if (!this.multiple) {
|
||||
this.values = {};
|
||||
}
|
||||
let key = record.id;
|
||||
if (!this.values[key]) {
|
||||
this.values[key] = record;
|
||||
} else {
|
||||
delete this.values[key];
|
||||
}
|
||||
this.values = Object.assign({}, this.values);
|
||||
if (!this.multiple) {
|
||||
this.done();
|
||||
}
|
||||
}
|
||||
},
|
||||
done: function() {
|
||||
let results = [];
|
||||
Object.values(this.values).forEach(v => {
|
||||
let value = v.id;
|
||||
if (this.format == "url") {
|
||||
let host = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
|
||||
value = `${host}/api/histories/${v.history_id}/contents/${value}/display`;
|
||||
}
|
||||
results.push(value);
|
||||
});
|
||||
if (results.length > 0 && !this.multiple) {
|
||||
results = results[0];
|
||||
}
|
||||
this.modalShow = false;
|
||||
this.callback(results);
|
||||
},
|
||||
load: function(url) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
this.optionsShow = false;
|
||||
this.undoShow = false;
|
||||
let hasUrl = !!url;
|
||||
if (!hasUrl) {
|
||||
let historyId = Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model.id;
|
||||
if (historyId) {
|
||||
url = `${Galaxy.root}api/histories/${historyId}/contents`;
|
||||
} else {
|
||||
this.errorMessage = "History not accessible.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
this.items = [];
|
||||
this.stack = [response.data];
|
||||
while (this.stack.length > 0) {
|
||||
let root = this.stack.pop();
|
||||
if (Array.isArray(root)) {
|
||||
root.forEach(element => {
|
||||
this.stack.push(element);
|
||||
});
|
||||
} else if (root.elements) {
|
||||
this.stack.push(root.elements);
|
||||
} else if (root.object) {
|
||||
this.stack.push(root.object);
|
||||
} else if (root.hid) {
|
||||
this.items.push(root);
|
||||
}
|
||||
}
|
||||
this.optionsShow = true;
|
||||
this.undoShow = hasUrl;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.response) {
|
||||
this.errorMessage =
|
||||
e.response.data.err_msg || `${e.response.statusText} (${e.response.status})`;
|
||||
} else {
|
||||
this.errorMessage = "Server unavailable.";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -119,6 +226,7 @@ export default {
|
||||
<style>
|
||||
.data-dialog-modal .modal-body {
|
||||
max-height: 50vh;
|
||||
height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<span class="badge badge-tags" :style="tagStyles" @click.prevent="$emit('tag-click', tag)"> {{ tagLabel }} </span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { keyedColorScheme } from "utils/color";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tag: { type: String, required: true }
|
||||
},
|
||||
computed: {
|
||||
tagLabel() {
|
||||
return this.tag.startsWith("name:") ? this.tag.replace("name:", "") : this.tag;
|
||||
},
|
||||
tagStyles() {
|
||||
let { primary, contrasting, darker } = keyedColorScheme(this.tag);
|
||||
return {
|
||||
"background-color": primary,
|
||||
color: contrasting,
|
||||
"border-color": darker
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.badge {
|
||||
padding: 0.2em 0.6em 0.3em;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 0.15rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="nametags" :title="title"><nametag v-for="tag in nameTags" :key="tag" :tag="tag" /></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Nametag from "./Nametag";
|
||||
import { mapActions } from "vuex";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Nametag
|
||||
},
|
||||
props: {
|
||||
storeKey: { type: String, required: true },
|
||||
tags: { type: Array, required: false, default: () => [] }
|
||||
},
|
||||
computed: {
|
||||
// only display tags that start with name:
|
||||
nameTags() {
|
||||
return this.$store.getters.getTagsById(this.storeKey).filter(tag => tag.startsWith("name:"));
|
||||
},
|
||||
title() {
|
||||
return `${this.nameTags.length} nametags`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(["updateTags", "initializeTags"])
|
||||
},
|
||||
mounted() {
|
||||
this.initializeTags({ key: this.storeKey, tags: this.tags });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.nametags:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nametags .badge {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
import Nametags from "./Nametags";
|
||||
import { mountVueComponent } from "utils/mountVueComponent";
|
||||
|
||||
export { default as Nametag } from "./Nametag.vue";
|
||||
export { default as Nametags } from "./Nametags.vue";
|
||||
export const mountNametags = mountVueComponent(Nametags);
|
||||
@@ -576,52 +576,13 @@ import JobStatesModel from "mvc/history/job-states-model";
|
||||
import RuleDefs from "mvc/rules/rule-definitions";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import Select2 from "components/Select2";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
const RULES = RuleDefs.RULES;
|
||||
const MAPPING_TARGETS = RuleDefs.MAPPING_TARGETS;
|
||||
|
||||
// Local components...
|
||||
|
||||
// Based on https://vuejs.org/v2/examples/select2.html but adapted to handle list values
|
||||
// with "multiple: true" set.
|
||||
const Select2 = {
|
||||
props: ["options", "value", "placeholder"],
|
||||
template: `<select>
|
||||
<slot></slot>
|
||||
</select>`,
|
||||
mounted: function() {
|
||||
var vm = this;
|
||||
$(this.$el)
|
||||
// init select2
|
||||
.select2({ data: this.options, placeholder: this.placeholder, allowClear: this.placeholder })
|
||||
.val(this.value)
|
||||
.trigger("change")
|
||||
// emit event on change.
|
||||
.on("change", function(event) {
|
||||
vm.$emit("input", event.val);
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
value: function(value) {
|
||||
// update value
|
||||
$(this.$el).val(value);
|
||||
},
|
||||
options: function(options) {
|
||||
// update options
|
||||
$(this.$el)
|
||||
.empty()
|
||||
.select2({ data: options });
|
||||
}
|
||||
},
|
||||
destroyed: function() {
|
||||
$(this.$el)
|
||||
.off()
|
||||
.select2("destroy");
|
||||
}
|
||||
};
|
||||
|
||||
const ColumnSelector = {
|
||||
template: `
|
||||
<div class="rule-column-selector" v-if="!multiple || !ordered">
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Based on https://vuejs.org/v2/examples/select2.html but adapted to handle list values
|
||||
// with "multiple: true" set.
|
||||
import $ from "jquery";
|
||||
|
||||
export default {
|
||||
props: ["options", "value", "placeholder"],
|
||||
template: `<select><slot></slot></select>`,
|
||||
mounted: function() {
|
||||
var vm = this;
|
||||
$(this.$el)
|
||||
// init select2
|
||||
.select2({ data: this.options, placeholder: this.placeholder, allowClear: this.placeholder })
|
||||
.val(this.value)
|
||||
.trigger("change")
|
||||
// emit event on change.
|
||||
.on("change", function(event) {
|
||||
vm.$emit("input", event.val);
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
value: function(value) {
|
||||
// update value
|
||||
$(this.$el).val(value);
|
||||
},
|
||||
options: function(options) {
|
||||
// update options
|
||||
$(this.$el)
|
||||
.empty()
|
||||
.select2({ data: options });
|
||||
}
|
||||
},
|
||||
destroyed: function() {
|
||||
$(this.$el)
|
||||
.off()
|
||||
.select2("destroy");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { mount, createLocalVue } from "@vue/test-utils";
|
||||
import StatelessTags from "./StatelessTags";
|
||||
import _l from "utils/localization";
|
||||
|
||||
describe("Tags/StatelessTags.vue", () => {
|
||||
const localVue = createLocalVue();
|
||||
localVue.filter("localize", value => _l(value));
|
||||
|
||||
let testTags = ["abc", "def", "ghi"];
|
||||
let wrapper, emitted;
|
||||
|
||||
beforeEach(function() {
|
||||
wrapper = mount(StatelessTags, { localVue });
|
||||
wrapper.setProps({
|
||||
value: testTags
|
||||
});
|
||||
emitted = wrapper.emitted();
|
||||
});
|
||||
|
||||
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
|
||||
@@ -3,7 +3,7 @@
|
||||
<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-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">
|
||||
@@ -19,10 +19,14 @@
|
||||
<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>
|
||||
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-if="enable_oidc" class="mt-3" @click="submitOIDCLogin()">
|
||||
<icon class="fa fa-google" /> Sign in with Google
|
||||
</b-button>
|
||||
</div>
|
||||
<div v-if="show_welcome_with_login" class="col">
|
||||
<b-embed type="iframe" :src="welcome_url" aspect="1by1" />
|
||||
@@ -51,7 +55,7 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let galaxy = getGalaxyInstance();
|
||||
return {
|
||||
login: null,
|
||||
password: null,
|
||||
@@ -59,7 +63,9 @@ 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
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -73,11 +79,10 @@ export default {
|
||||
this.$root.toggleLogin();
|
||||
}
|
||||
},
|
||||
submit: function(method) {
|
||||
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);
|
||||
@@ -96,6 +101,22 @@ export default {
|
||||
this.messageText = message || "Login failed for an unknown reason.";
|
||||
});
|
||||
},
|
||||
submitOIDCLogin: function(method) {
|
||||
let rootUrl = getAppRoot();
|
||||
axios
|
||||
.post(`${rootUrl}authnz/google/login`)
|
||||
.then(response => {
|
||||
if (response.data.redirect_uri) {
|
||||
window.location = encodeURI(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();
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
<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>
|
||||
Already have an account?
|
||||
<a id="login-toggle" href="#" @click.prevent="toggleLogin">Log in here.</a>
|
||||
</b-card-footer>
|
||||
</b-card>
|
||||
</b-form>
|
||||
@@ -45,6 +46,7 @@
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
@@ -69,6 +71,7 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let galaxy = getGalaxyInstance();
|
||||
return {
|
||||
email: null,
|
||||
password: null,
|
||||
@@ -76,7 +79,8 @@ export default {
|
||||
confirm: null,
|
||||
subscribe: null,
|
||||
messageText: null,
|
||||
messageVariant: null
|
||||
messageVariant: null,
|
||||
session_csrf_token: galaxy.session_csrf_token
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -36,6 +36,15 @@ var HistoryPanel = Backbone.View.extend({
|
||||
self.historyView.loadCurrentHistory();
|
||||
}
|
||||
});
|
||||
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();
|
||||
}
|
||||
});
|
||||
this.buttonOptions = new Ui.ButtonLink({
|
||||
id: "history-options-button",
|
||||
title: _l("History options"),
|
||||
@@ -52,11 +61,11 @@ var HistoryPanel = Backbone.View.extend({
|
||||
href: `${this.root}history/view_multiple`
|
||||
});
|
||||
|
||||
// define components
|
||||
this.model = new Backbone.Model({
|
||||
// define components
|
||||
cls: "history-right-panel",
|
||||
title: _l("History"),
|
||||
buttons: [this.buttonRefresh, this.buttonOptions, this.buttonViewMulti]
|
||||
buttons: [this.buttonRefresh, this.buttonNew, this.buttonOptions, this.buttonViewMulti]
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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>' +
|
||||
|
||||
@@ -9,14 +9,13 @@ export default class Data {
|
||||
* Opens a modal dialog for data selection
|
||||
* @param {function} callback - Result function called with selection
|
||||
*/
|
||||
dialog(callback) {
|
||||
dialog(callback, options = {}) {
|
||||
options.callback = callback;
|
||||
var instance = Vue.extend(DataDialog);
|
||||
var vm = document.createElement("div");
|
||||
$("body").append(vm);
|
||||
new instance({
|
||||
propsData: {
|
||||
callback: callback
|
||||
}
|
||||
propsData: options
|
||||
}).$mount(vm);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Masthead Collection **/
|
||||
import _ from "underscore";
|
||||
import $ from "jquery";
|
||||
import axios from "axios";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
@@ -11,12 +12,14 @@ import Utils from "utils/utils";
|
||||
|
||||
function logoutClick() {
|
||||
let galaxy = getGalaxyInstance();
|
||||
let token = galaxy.session_csrf_token || "";
|
||||
if (galaxy.user) {
|
||||
galaxy.user.clearSessionStorage();
|
||||
}
|
||||
let url = `${galaxy.root}user/logout?session_csrf_token=${token}`;
|
||||
window.top.location.href = url;
|
||||
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({
|
||||
@@ -45,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")
|
||||
});
|
||||
|
||||
//
|
||||
@@ -218,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"),
|
||||
@@ -417,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);
|
||||
@@ -456,7 +444,7 @@ var Tab = Backbone.View.extend({
|
||||
} else {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
if (model.attributes.target == "__use_router__" && typeof Galaxy.page != "undefined") {
|
||||
Galaxy.page.router.push(model.attributes.url);
|
||||
Galaxy.page.router.executeUseRouter(model.attributes.url);
|
||||
} else {
|
||||
Galaxy.frame.add(model.attributes);
|
||||
}
|
||||
|
||||
@@ -231,9 +231,18 @@ var CenterPanel = Backbone.View.extend({
|
||||
|
||||
/** Display a view in the center panel, hide iframe */
|
||||
display: function(view) {
|
||||
var contentWindow = this.$frame[0].contentWindow || {};
|
||||
var message = contentWindow.onbeforeunload && contentWindow.onbeforeunload();
|
||||
var Galaxy = getGalaxyInstance();
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let contentWindow = this.$frame[0].contentWindow || {};
|
||||
let message;
|
||||
try {
|
||||
message = contentWindow.onbeforeunload && contentWindow.onbeforeunload();
|
||||
} catch (err) {
|
||||
// This can happen when external content is displayed in this iframe // CORS violation
|
||||
contentWindow = {};
|
||||
console.warn(
|
||||
"Iframe unload exception. This can happen when external content is displayed in the page iframe and causes a CORS violation -- likely harmless."
|
||||
);
|
||||
}
|
||||
if (!message || confirm(message)) {
|
||||
contentWindow.onbeforeunload = undefined;
|
||||
this.$frame.attr("src", "about:blank").hide();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import QUERY_STRING from "utils/query-string-parsing";
|
||||
import Ui from "mvc/ui/ui-misc";
|
||||
@@ -12,6 +13,14 @@ var Router = Backbone.Router.extend({
|
||||
this.options = options;
|
||||
},
|
||||
|
||||
executeUseRouter: function(url) {
|
||||
let prefix = getAppRoot();
|
||||
if (url.startsWith(prefix)) {
|
||||
url = url.replace(prefix, "/");
|
||||
}
|
||||
return this.push(url);
|
||||
},
|
||||
|
||||
/** helper to push a new navigation state */
|
||||
push: function(url, data) {
|
||||
data = data || {};
|
||||
@@ -19,10 +28,12 @@ var Router = Backbone.Router.extend({
|
||||
.toString(36)
|
||||
.substr(2);
|
||||
url += url.indexOf("?") == -1 ? "?" : "&";
|
||||
url += $.param(data, true);
|
||||
let bustParam = $.param(data, true);
|
||||
url += bustParam;
|
||||
let Galaxy = getGalaxyInstance();
|
||||
Galaxy.params = data;
|
||||
this.navigate(url, { trigger: true });
|
||||
window.history.replaceState(window.history.state, "", window.location.pathname.replace(bustParam, ""));
|
||||
},
|
||||
|
||||
/** override to parse query string into obj and send to each route */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import STATES from "mvc/dataset/states";
|
||||
import DATASET_LI from "mvc/dataset/dataset-li";
|
||||
import TAGS from "mvc/tag";
|
||||
import { mountModelTags } from "components/Tags";
|
||||
import ANNOTATIONS from "mvc/annotation";
|
||||
import faIconButton from "ui/fa-icon-button";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
@@ -297,35 +297,44 @@ var DatasetListItemEdit = _super.extend(
|
||||
}
|
||||
},
|
||||
|
||||
//TODO: if possible move these to readonly view - but display the owner's tags/annotation (no edit)
|
||||
/** Render the tags list/control */
|
||||
_renderTags: function($where) {
|
||||
if (!this.hasUser) {
|
||||
return;
|
||||
}
|
||||
var view = this;
|
||||
this.tagsEditor = new TAGS.TagsEditor({
|
||||
|
||||
let el = $where.find(".tags-display")[0];
|
||||
|
||||
let propsData = {
|
||||
model: this.model,
|
||||
el: $where.find(".tags-display"),
|
||||
onshowFirstTime: function() {
|
||||
this.render();
|
||||
},
|
||||
// persist state on the hda view (and not the editor) since these are currently re-created each time
|
||||
onshow: function() {
|
||||
view.tagsEditorShown = true;
|
||||
},
|
||||
onhide: function() {
|
||||
view.tagsEditorShown = false;
|
||||
},
|
||||
$activator: faIconButton({
|
||||
title: _l("Edit dataset tags"),
|
||||
classes: "tag-btn",
|
||||
faIcon: "fa-tags"
|
||||
}).appendTo($where.find(".actions .right"))
|
||||
});
|
||||
disabled: false,
|
||||
context: "dataset-li-edit"
|
||||
};
|
||||
|
||||
let vm = mountModelTags(propsData, el);
|
||||
|
||||
// tag icon button open/closes
|
||||
let activator = faIconButton({
|
||||
title: _l("Edit dataset tags"),
|
||||
classes: "tag-btn",
|
||||
faIcon: "fa-tags"
|
||||
}).appendTo($where.find(".actions .right"));
|
||||
|
||||
let toggleEditor = () => {
|
||||
$(vm.$el).toggleClass("active");
|
||||
this.tagsEditorShown = $(vm.$el).hasClass("active");
|
||||
};
|
||||
|
||||
activator.on("click", toggleEditor);
|
||||
|
||||
if (this.tagsEditorShown) {
|
||||
this.tagsEditor.toggle(true);
|
||||
let editorIsOpen = $(vm.$el).hasClass("active");
|
||||
if (!editorIsOpen) {
|
||||
toggleEditor();
|
||||
}
|
||||
}
|
||||
|
||||
return vm;
|
||||
},
|
||||
|
||||
/** Render the annotation display/control */
|
||||
|
||||
@@ -7,6 +7,7 @@ import STATES from "mvc/dataset/states";
|
||||
import faIconButton from "ui/fa-icon-button";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import _l from "utils/localization";
|
||||
import { mountNametags } from "components/Nametags";
|
||||
|
||||
var logNamespace = "dataset";
|
||||
/*==============================================================================
|
||||
@@ -42,6 +43,20 @@ export var DatasetListItemView = _super.extend(
|
||||
this.linkTarget = attributes.linkTarget || "_blank";
|
||||
},
|
||||
|
||||
// mount new vue component for tags
|
||||
render: function() {
|
||||
let result = _super.prototype.render.apply(this, arguments);
|
||||
this._mountVueNametags();
|
||||
return result;
|
||||
},
|
||||
|
||||
_mountVueNametags: function() {
|
||||
let container = this.$(".nametags")[0];
|
||||
let { id, model_class, tags } = this.model.attributes;
|
||||
let storeKey = `${model_class}-${id}`;
|
||||
mountNametags({ storeKey, tags }, container);
|
||||
},
|
||||
|
||||
/** event listeners */
|
||||
_setUpListeners: function() {
|
||||
_super.prototype._setUpListeners.call(this);
|
||||
@@ -64,11 +79,11 @@ export var DatasetListItemView = _super.extend(
|
||||
self.render();
|
||||
});
|
||||
} else {
|
||||
if (_.has(model.changed, "tags") && _.keys(model.changed).length === 1) {
|
||||
// If only the tags have changed, rerender specifically
|
||||
// the titlebar region. Otherwise default to the full
|
||||
// render.
|
||||
self.$(".nametags").html(self._renderNametags());
|
||||
if (_.has(self.model.changed, "tags") && _.keys(self.model.changed).length === 2) {
|
||||
// If only the tags and update time have changed,
|
||||
// rerender specifically the titlebar region.
|
||||
// Otherwise default to the full render.
|
||||
self._mountVueNametags();
|
||||
} else {
|
||||
self.render();
|
||||
}
|
||||
@@ -327,19 +342,6 @@ export var DatasetListItemView = _super.extend(
|
||||
</div>`);
|
||||
},
|
||||
|
||||
_renderNametags: function() {
|
||||
var tpl = _.template(
|
||||
[
|
||||
"<% _.each(_.sortBy(_.uniq(tags), function(x) { return x }), function(tag){ %>",
|
||||
'<% if (tag.indexOf("name:") == 0){ %>',
|
||||
'<span class="badge badge-primary badge-tags"><%- tag.slice(5) %></span>',
|
||||
"<% } %>",
|
||||
"<% }); %>"
|
||||
].join("")
|
||||
);
|
||||
return tpl({ tags: this.model.get("tags") });
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
events: _.extend(_.clone(_super.prototype.events), {
|
||||
"click .display-btn": function(ev) {
|
||||
|
||||
@@ -203,7 +203,7 @@ export default Backbone.View.extend({
|
||||
.append(
|
||||
$("<div/>")
|
||||
.addClass("ui-form-field")
|
||||
.append($("<span/>").addClass("ui-form-info form-text text-muted"))
|
||||
.append($("<span/>").addClass("ui-form-info form-text text-muted mt-2"))
|
||||
.append($("<div/>").addClass("ui-form-backdrop"))
|
||||
)
|
||||
.append($("<div/>").addClass("ui-form-preview"));
|
||||
|
||||
@@ -29,6 +29,7 @@ export default Backbone.Model.extend({
|
||||
boolean: "_fieldBoolean",
|
||||
drill_down: "_fieldDrilldown",
|
||||
color: "_fieldColor",
|
||||
group_tag: "_fieldSelect",
|
||||
hidden: "_fieldHidden",
|
||||
hidden_data: "_fieldHidden",
|
||||
baseurl: "_fieldHidden",
|
||||
@@ -125,7 +126,7 @@ export default Backbone.Model.extend({
|
||||
/** Text input field */
|
||||
_fieldText: function(input_def) {
|
||||
// field replaces e.g. a select field
|
||||
if (input_def.options && input_def.data) {
|
||||
if (input_def.data_ref || input_def.options && input_def.data) {
|
||||
input_def.area = input_def.multiple;
|
||||
if (Utils.isEmpty(input_def.value)) {
|
||||
input_def.value = null;
|
||||
|
||||
@@ -8,6 +8,8 @@ import Templates from "mvc/grid/grid-template";
|
||||
import PopupMenu from "mvc/ui/popup-menu";
|
||||
import LoadingIndicator from "ui/loading-indicator";
|
||||
import { init_refresh_on_change } from "onload/globalInits/init_refresh_on_change";
|
||||
import store from "../../store";
|
||||
import slugify from "slugify";
|
||||
|
||||
// This is necessary so that, when nested arrays are used in ajax/post/get methods, square brackets ('[]') are
|
||||
// not appended to the identifier of a nested array.
|
||||
@@ -25,17 +27,21 @@ export default Backbone.View.extend({
|
||||
this.active_tab = grid_config.active_tab;
|
||||
var self = this;
|
||||
|
||||
// Why is this a global?
|
||||
window.add_tag_to_grid_filter = (tag_name, tag_value) => {
|
||||
// Put tag name and value together.
|
||||
var tag = tag_name + (tag_value !== undefined && tag_value !== "" ? `:${tag_value}` : "");
|
||||
var advanced_search = $("#advanced-search").is(":visible");
|
||||
if (!advanced_search) {
|
||||
$("#standard-search").slideToggle("fast");
|
||||
$("#advanced-search").slideToggle("fast");
|
||||
// Subscribe to changes in the store, currently just storing
|
||||
// tag changes from the tagging components, but that will change
|
||||
// when we rework the grid. This subscription ties this older grid
|
||||
// code to the new vue components
|
||||
store.watch(
|
||||
state => state.gridSearch.searchTags,
|
||||
newTags => {
|
||||
let tagArray = Array.from(newTags);
|
||||
self.grid.add_filter("tags", tagArray, false);
|
||||
self.openAdvancedSearch();
|
||||
self.render_filter_button("tags", tagArray);
|
||||
self.go_page_one();
|
||||
self.execute();
|
||||
}
|
||||
self.add_filter_condition("tags", tag);
|
||||
};
|
||||
);
|
||||
|
||||
if (grid_config.url_base && !grid_config.items) {
|
||||
LoadingIndicator.markViewAsLoading(this);
|
||||
@@ -66,6 +72,14 @@ export default Backbone.View.extend({
|
||||
}
|
||||
},
|
||||
|
||||
openAdvancedSearch: function() {
|
||||
var isOpen = $("#advanced-search").is(":visible");
|
||||
if (!isOpen) {
|
||||
$("#standard-search").slideToggle("fast");
|
||||
$("#advanced-search").slideToggle("fast");
|
||||
}
|
||||
},
|
||||
|
||||
// refresh frames
|
||||
handle_refresh: function(refresh_frames) {
|
||||
if (refresh_frames) {
|
||||
@@ -99,6 +113,9 @@ export default Backbone.View.extend({
|
||||
// append main template
|
||||
this.$el.html(Templates.grid(options));
|
||||
|
||||
// add a class identifier for styling purposes
|
||||
this.$el.addClass(this.getRootClassName(grid_config));
|
||||
|
||||
// update div contents
|
||||
this.$el.find("#grid-table-header").html(Templates.header(options));
|
||||
this.$el.find("#grid-table-body").html(Templates.body(options));
|
||||
@@ -310,6 +327,14 @@ export default Backbone.View.extend({
|
||||
// Add condition to grid.
|
||||
this.grid.add_filter(name, value, true);
|
||||
|
||||
this.render_filter_button(name, value);
|
||||
|
||||
// execute
|
||||
this.go_page_one();
|
||||
this.execute();
|
||||
},
|
||||
|
||||
render_filter_button: function(name, value) {
|
||||
// Add button that displays filter and provides a button to delete it.
|
||||
var t = $(Templates.filter_element(name, value));
|
||||
var self = this;
|
||||
@@ -324,10 +349,6 @@ export default Backbone.View.extend({
|
||||
// append to container
|
||||
var container = this.$el.find(`#${name}-filtering-criteria`);
|
||||
container.append(t);
|
||||
|
||||
// execute
|
||||
this.go_page_one();
|
||||
this.execute();
|
||||
},
|
||||
|
||||
// Remove a condition to the grid filter; this adds the condition and refreshes the grid.
|
||||
@@ -335,6 +356,11 @@ export default Backbone.View.extend({
|
||||
// Remove filter condition.
|
||||
this.grid.remove_filter(name, value);
|
||||
|
||||
// update vuex if the one criteria we're currently tracking changes
|
||||
if (name == "tags") {
|
||||
store.dispatch("removeSearchTag", { text: value });
|
||||
}
|
||||
|
||||
// Execute
|
||||
this.go_page_one();
|
||||
this.execute();
|
||||
@@ -665,5 +691,12 @@ export default Backbone.View.extend({
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Generates a class name at the root of the view that we can
|
||||
// use for conditional styling in the various kinds of grids
|
||||
// instead of acres of if/then statements in javascript
|
||||
getRootClassName({ title = "grid" }) {
|
||||
return slugify(title).toLowerCase();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import _ from "underscore";
|
||||
import DATASET_LI from "mvc/dataset/dataset-li";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import HISTORY_ITEM_LI from "mvc/history/history-item-li";
|
||||
import _l from "utils/localization";
|
||||
|
||||
//==============================================================================
|
||||
@@ -39,7 +38,7 @@ HDAListItemView.prototype.templates = (() => {
|
||||
<span class="name">${_.escape(dataset.name)}</span>
|
||||
</div>
|
||||
</br>
|
||||
${HISTORY_ITEM_LI.nametagTemplate(dataset)}
|
||||
<div class="nametags"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import _ from "underscore";
|
||||
import STATES from "mvc/dataset/states";
|
||||
import DC_LI from "mvc/collection/collection-li";
|
||||
import DC_VIEW from "mvc/collection/collection-view";
|
||||
import HISTORY_ITEM_LI from "mvc/history/history-item-li";
|
||||
import _l from "utils/localization";
|
||||
import { mountNametags } from "components/Nametags";
|
||||
|
||||
//==============================================================================
|
||||
var _super = DC_LI.DCListItemView;
|
||||
@@ -13,12 +13,19 @@ var HDCAListItemView = _super.extend(
|
||||
/** @lends HDCAListItemView.prototype */ {
|
||||
className: `${_super.prototype.className} history-content`,
|
||||
|
||||
render: function() {
|
||||
let result = _super.prototype.render.apply(this, arguments);
|
||||
this._mountNametags("initialize");
|
||||
return result;
|
||||
},
|
||||
|
||||
/** event listeners */
|
||||
_setUpListeners: function() {
|
||||
_super.prototype._setUpListeners.call(this);
|
||||
var renderListen = (model, options) => {
|
||||
// We want this to swap immediately without extra animations.
|
||||
this.render(0);
|
||||
this._mountNametags("listener");
|
||||
};
|
||||
if (this.model.jobStatesSummary) {
|
||||
this.listenTo(this.model.jobStatesSummary, "change", renderListen);
|
||||
@@ -28,6 +35,15 @@ var HDCAListItemView = _super.extend(
|
||||
});
|
||||
},
|
||||
|
||||
_mountNametags(context) {
|
||||
let container = this.$el.find(".nametags")[0];
|
||||
if (container) {
|
||||
let { id, model_class, tags } = this.model.attributes;
|
||||
let storeKey = `${model_class}-${id}`;
|
||||
mountNametags({ storeKey, tags }, container);
|
||||
}
|
||||
},
|
||||
|
||||
/** Override to provide the proper collections panels as the foldout */
|
||||
_getFoldoutPanelClass: function() {
|
||||
return DC_VIEW.CollectionView;
|
||||
@@ -137,7 +153,7 @@ HDCAListItemView.prototype.templates = (() => {
|
||||
</div>
|
||||
<div class="state-description">
|
||||
</div>
|
||||
${HISTORY_ITEM_LI.nametagTemplate(collection)}
|
||||
<div class="nametags"><!-- Nametags mount here (hdca-li) --></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import _ from "underscore";
|
||||
import Utils from "utils/utils";
|
||||
|
||||
function _templateNametag(tag) {
|
||||
return `<span style="${Utils.generateTagStyle(tag.slice(5))}" class="badge badge-primary badge-tags">${_.escape(
|
||||
tag.slice(5)
|
||||
)}</span>`;
|
||||
}
|
||||
|
||||
function nametagTemplate(historyItem) {
|
||||
let uniqueNametags = _.filter(_.uniq(historyItem.tags), t => t.indexOf("name:") === 0);
|
||||
let nametagsDisplay = _.sortBy(uniqueNametags).map(_templateNametag);
|
||||
return `
|
||||
<div class="nametags" title="${uniqueNametags.length} nametags">
|
||||
${nametagsDisplay.join("")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export default {
|
||||
nametagTemplate: nametagTemplate
|
||||
};
|
||||
@@ -224,16 +224,7 @@ var CurrentHistoryView = _super.extend(
|
||||
|
||||
/** In this override, get and set current panel preferences when editor is used */
|
||||
_renderTags: function($where) {
|
||||
var panel = this;
|
||||
// render tags and show/hide based on preferences
|
||||
_super.prototype._renderTags.call(panel, $where);
|
||||
if (panel.preferences.get("tagsEditorShown")) {
|
||||
panel.tagsEditor.toggle(true);
|
||||
}
|
||||
// store preference when shown or hidden
|
||||
panel.listenTo(panel.tagsEditor, "hiddenUntilActivated:shown hiddenUntilActivated:hidden", tagsEditor => {
|
||||
panel.preferences.set("tagsEditorShown", tagsEditor.hidden);
|
||||
});
|
||||
return _super.prototype._renderTags.call(this, $where);
|
||||
},
|
||||
|
||||
/** In this override, get and set current panel preferences when editor is used */
|
||||
|
||||
@@ -5,7 +5,7 @@ import HISTORY_VIEW from "mvc/history/history-view";
|
||||
import HDA_MODEL from "mvc/history/hda-model";
|
||||
import HDA_LI_EDIT from "mvc/history/hda-li-edit";
|
||||
import HDCA_LI_EDIT from "mvc/history/hdca-li-edit";
|
||||
import TAGS from "mvc/tag";
|
||||
import { mountModelTags } from "components/Tags";
|
||||
import ANNOTATIONS from "mvc/annotation";
|
||||
import LIST_COLLECTION_CREATOR from "mvc/collection/list-collection-creator";
|
||||
import PAIR_COLLECTION_CREATOR from "mvc/collection/pair-collection-creator";
|
||||
@@ -155,28 +155,31 @@ var HistoryViewEdit = _super.extend(
|
||||
|
||||
/** render the tags sub-view controller */
|
||||
_renderTags: function($where) {
|
||||
var panel = this;
|
||||
this.tagsEditor = new TAGS.TagsEditor({
|
||||
let el = $where.find(".controls .tags-display")[0];
|
||||
|
||||
let propsData = {
|
||||
model: this.model,
|
||||
el: $where.find(".controls .tags-display"),
|
||||
onshowFirstTime: function() {
|
||||
this.render();
|
||||
},
|
||||
// show hide sub-view tag editors when this is shown/hidden
|
||||
onshow: function() {
|
||||
panel.toggleHDATagEditors(true, panel.fxSpeed);
|
||||
},
|
||||
onhide: function() {
|
||||
panel.toggleHDATagEditors(false, panel.fxSpeed);
|
||||
},
|
||||
$activator: faIconButton({
|
||||
title: _l("Edit history tags"),
|
||||
classes: "history-tag-btn",
|
||||
faIcon: "fa-tags",
|
||||
tooltipConfig: { placement: "top" }
|
||||
}).appendTo($where.find(".controls .actions"))
|
||||
disabled: false,
|
||||
context: "history-view-edit"
|
||||
};
|
||||
|
||||
let vm = mountModelTags(propsData, el);
|
||||
|
||||
// tag icon button open/closes
|
||||
let activator = faIconButton({
|
||||
title: _l("Edit history tags"),
|
||||
classes: "history-tag-btn",
|
||||
faIcon: "fa-tags",
|
||||
tooltipConfig: { placement: "top" }
|
||||
}).appendTo($where.find(".controls .actions"));
|
||||
|
||||
activator.on("click", () => {
|
||||
$(vm.$el).toggleClass("active");
|
||||
});
|
||||
|
||||
return vm;
|
||||
},
|
||||
|
||||
/** render the annotation sub-view controller */
|
||||
_renderAnnotation: function($where) {
|
||||
var panel = this;
|
||||
@@ -418,13 +421,13 @@ var HistoryViewEdit = _super.extend(
|
||||
},
|
||||
|
||||
/** toggle the visibility of each content's tagsEditor applying all the args sent to this function */
|
||||
toggleHDATagEditors: function(showOrHide, speed) {
|
||||
_.each(this.views, view => {
|
||||
if (view.tagsEditor) {
|
||||
view.tagsEditor.toggle(showOrHide, speed);
|
||||
}
|
||||
});
|
||||
},
|
||||
// toggleHDATagEditors: function(showOrHide, speed) {
|
||||
// _.each(this.views, view => {
|
||||
// if (view.tagsEditor) {
|
||||
// view.tagsEditor.toggle(showOrHide, speed);
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
|
||||
/** toggle the visibility of each content's annotationEditor applying all the args sent to this function */
|
||||
toggleHDAAnnotationEditors: function(showOrHide, speed) {
|
||||
|
||||
@@ -28,15 +28,6 @@ var menu = [
|
||||
header: true,
|
||||
anon: true
|
||||
},
|
||||
{
|
||||
html: _l("Create New"),
|
||||
func: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
if (Galaxy && Galaxy.currHistoryPanel) {
|
||||
Galaxy.currHistoryPanel.createNewHistory();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
html: _l("Copy History"),
|
||||
func: function() {
|
||||
|
||||
@@ -29,7 +29,9 @@ 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
|
||||
@@ -398,6 +400,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.
|
||||
*/
|
||||
@@ -434,12 +442,19 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
ld.set("file_ext", new_ext);
|
||||
is_changed = true;
|
||||
}
|
||||
var dataset_view = this;
|
||||
if (is_changed) {
|
||||
ld.save(null, {
|
||||
this._submitModification(ld);
|
||||
} else {
|
||||
this.render();
|
||||
mod_toastr.info("Nothing has changed.");
|
||||
}
|
||||
},
|
||||
|
||||
_submitModification(library_dataset){
|
||||
library_dataset.save(null, {
|
||||
patch: true,
|
||||
success: function(ld) {
|
||||
dataset_view.render();
|
||||
success: library_dataset => {
|
||||
this.render();
|
||||
mod_toastr.success("Changes to library dataset saved.");
|
||||
},
|
||||
error: function(model, response) {
|
||||
@@ -450,10 +465,6 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dataset_view.render();
|
||||
mod_toastr.info("Nothing has changed.");
|
||||
}
|
||||
},
|
||||
|
||||
copyToClipboard: function(e) {
|
||||
@@ -618,6 +629,10 @@ var LibraryDatasetView = Backbone.View.extend({
|
||||
'<span class="fa fa-pencil"></span>',
|
||||
" Modify",
|
||||
"</button>",
|
||||
'<button data-toggle="tooltip" data-placement="top" title="Attempt to detect the format of dataset" class="btn btn-secondary toolbtn_detect_datatype toolbar-item mr-1" type="button">',
|
||||
'<span class="fa fa-undo"></span>',
|
||||
" Auto-detect datatype",
|
||||
"</button>",
|
||||
"<% } %>",
|
||||
'<% if (item.get("can_user_manage")) { %>',
|
||||
'<a href="#folders/<%- item.get("folder_id") %>/datasets/<%- item.id %>/permissions">',
|
||||
|
||||
@@ -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("")
|
||||
);
|
||||
|
||||
@@ -879,7 +879,7 @@ 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");
|
||||
@@ -889,17 +889,19 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
}
|
||||
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,7 +929,7 @@ 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.");
|
||||
@@ -938,17 +940,20 @@ var FolderToolbarView = Backbone.View.extend({
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -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{ %>",
|
||||
|
||||
@@ -13,6 +13,7 @@ import FormBase from "mvc/form/form-view";
|
||||
import Webhooks from "mvc/webhooks";
|
||||
import Citations from "components/Citations.vue";
|
||||
import Vue from "vue";
|
||||
import axios from "axios";
|
||||
|
||||
export default FormBase.extend({
|
||||
initialize: function(options) {
|
||||
@@ -113,13 +114,51 @@ export default FormBase.extend({
|
||||
_operations: function() {
|
||||
var self = this;
|
||||
var options = this.model.attributes;
|
||||
let Galaxy = getGalaxyInstance();
|
||||
|
||||
// Buttons for adding and removing favorite.
|
||||
let in_favorites = Galaxy.user.getFavorites().tools.indexOf(options.id) >= 0;
|
||||
var favorite_button = new Ui.Button({
|
||||
icon: "fa-star-o",
|
||||
title: options.narrow ? null : "Favorite",
|
||||
tooltip: "Add to favorites",
|
||||
visible: !Galaxy.user.isAnonymous() && !in_favorites,
|
||||
onclick: () => {
|
||||
axios
|
||||
.put(`${Galaxy.root}api/users/${Galaxy.user.id}/favorites/tools`, { object_id: options.id })
|
||||
.then(response => {
|
||||
favorite_button.hide();
|
||||
remove_favorite_button.show();
|
||||
Galaxy.user.updateFavorites("tools", response.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var remove_favorite_button = new Ui.Button({
|
||||
icon: "fa-star",
|
||||
title: options.narrow ? null : "Added",
|
||||
tooltip: "Remove from favorites",
|
||||
visible: !Galaxy.user.isAnonymous() && in_favorites,
|
||||
onclick: () => {
|
||||
axios
|
||||
.delete(
|
||||
`${Galaxy.root}api/users/${Galaxy.user.id}/favorites/tools/${encodeURIComponent(options.id)}`
|
||||
)
|
||||
.then(response => {
|
||||
remove_favorite_button.hide();
|
||||
favorite_button.show();
|
||||
Galaxy.user.updateFavorites("tools", response.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// button for version selection
|
||||
var versions_button = new Ui.ButtonMenu({
|
||||
icon: "fa-cubes",
|
||||
title: (!options.narrow && "Versions") || null,
|
||||
title: options.narrow ? null : "Versions",
|
||||
tooltip: "Select another tool version"
|
||||
});
|
||||
|
||||
if (!options.sustain_version && options.versions && options.versions.length > 1) {
|
||||
for (var i in options.versions) {
|
||||
var version = options.versions[i];
|
||||
@@ -145,25 +184,9 @@ export default FormBase.extend({
|
||||
var menu_button = new Ui.ButtonMenu({
|
||||
id: "options",
|
||||
icon: "fa-caret-down",
|
||||
title: (!options.narrow && "Options") || null,
|
||||
title: options.narrow ? null : "Options",
|
||||
tooltip: "View available options"
|
||||
});
|
||||
if (options.biostar_url) {
|
||||
menu_button.addMenu({
|
||||
icon: "fa-question-circle",
|
||||
title: "Question?",
|
||||
onclick: function() {
|
||||
window.open(`${options.biostar_url}/p/new/post/`);
|
||||
}
|
||||
});
|
||||
menu_button.addMenu({
|
||||
icon: "fa-search",
|
||||
title: _l("Search"),
|
||||
onclick: function() {
|
||||
window.open(`${options.biostar_url}/local/search/page/?q=${options.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
menu_button.addMenu({
|
||||
icon: "fa-share",
|
||||
title: _l("Share"),
|
||||
@@ -176,7 +199,6 @@ export default FormBase.extend({
|
||||
});
|
||||
|
||||
// add admin operations
|
||||
let Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.user && Galaxy.user.get("is_admin")) {
|
||||
menu_button.addMenu({
|
||||
icon: "fa-download",
|
||||
@@ -242,7 +264,9 @@ export default FormBase.extend({
|
||||
|
||||
return {
|
||||
menu: menu_button,
|
||||
versions: versions_button
|
||||
versions: versions_button,
|
||||
favorite: favorite_button,
|
||||
remove_favorite: remove_favorite_button
|
||||
};
|
||||
},
|
||||
|
||||
@@ -267,8 +291,7 @@ export default FormBase.extend({
|
||||
/** Templates */
|
||||
_templateHelp: function(options) {
|
||||
var $tmpl = $("<div/>")
|
||||
.addClass("form-help")
|
||||
.addClass("form-text")
|
||||
.addClass("form-help form-text mt-4")
|
||||
.append(options.help);
|
||||
$tmpl.find("a").attr("target", "_blank");
|
||||
$tmpl.find("img").each(function() {
|
||||
|
||||
@@ -344,6 +344,8 @@ _.extend(ToolSection.prototype, VisibilityMixin);
|
||||
* query.
|
||||
*/
|
||||
var ToolSearch = Backbone.Model.extend({
|
||||
SEARCH_RESERVED_TERMS_FAVORITES: ["#favs", "#favorites", "#favourites"],
|
||||
|
||||
defaults: {
|
||||
search_hint_string: "search tools",
|
||||
min_chars_for_search: 3,
|
||||
@@ -365,6 +367,7 @@ var ToolSearch = Backbone.Model.extend({
|
||||
* Do the search and update the results.
|
||||
*/
|
||||
do_search: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
var query = this.attributes.query;
|
||||
|
||||
// If query is too short, do not search.
|
||||
@@ -379,26 +382,31 @@ var ToolSearch = Backbone.Model.extend({
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
// Start a new ajax-request in X ms
|
||||
$("#search-clear-btn").hide();
|
||||
$("#search-spinner").show();
|
||||
var self = this;
|
||||
this.timer = setTimeout(() => {
|
||||
// log the search to analytics if present
|
||||
if (typeof ga !== "undefined") {
|
||||
ga("send", "pageview", `${getAppRoot()}?q=${q}`);
|
||||
}
|
||||
$.get(
|
||||
self.urlRoot,
|
||||
{ q: q },
|
||||
data => {
|
||||
self.set("results", data);
|
||||
$("#search-spinner").hide();
|
||||
$("#search-clear-btn").show();
|
||||
},
|
||||
"json"
|
||||
);
|
||||
}, 400);
|
||||
// Catch reserved words
|
||||
if (this.SEARCH_RESERVED_TERMS_FAVORITES.indexOf(q) >= 0) {
|
||||
this.set("results", Galaxy.user.getFavorites().tools);
|
||||
} else {
|
||||
// Start a new ajax-request in X ms
|
||||
$("#search-clear-btn").hide();
|
||||
$("#search-spinner").show();
|
||||
var self = this;
|
||||
this.timer = setTimeout(() => {
|
||||
// log the search to analytics if present
|
||||
if (typeof ga !== "undefined") {
|
||||
ga("send", "pageview", `${getAppRoot()}?q=${q}`);
|
||||
}
|
||||
$.get(
|
||||
self.urlRoot,
|
||||
{ q: q },
|
||||
data => {
|
||||
self.set("results", data);
|
||||
$("#search-spinner").hide();
|
||||
$("#search-clear-btn").show();
|
||||
},
|
||||
"json"
|
||||
);
|
||||
}, 400);
|
||||
}
|
||||
},
|
||||
|
||||
clear_search: function() {
|
||||
|
||||
@@ -24,23 +24,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 +50,7 @@ const TOURPAGE_TEMPLATE = `
|
||||
<% }); %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>`;
|
||||
</div>`;
|
||||
|
||||
var tour_opts = {
|
||||
storage: window.sessionStorage,
|
||||
@@ -139,22 +134,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 +171,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");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -9,7 +9,6 @@ var View = Backbone.View.extend({
|
||||
this.model =
|
||||
(options && options.model) ||
|
||||
new Backbone.Model({
|
||||
icon: "fa-upload",
|
||||
tooltip: _l("Download from URL or upload files from disk"),
|
||||
label: "Load Data",
|
||||
percentage: 0,
|
||||
@@ -46,16 +45,14 @@ var View = Backbone.View.extend({
|
||||
|
||||
/** Template */
|
||||
_template: function() {
|
||||
return (
|
||||
'<div class="upload-button">' +
|
||||
'<div class="progress">' +
|
||||
'<div class="progress-bar"/>' +
|
||||
'<a class="panel-header-button" href="javascript:void(0)" id="tool-panel-upload-button">' +
|
||||
'<span class="fa fa-upload"/>' +
|
||||
"</a>" +
|
||||
"</div>" +
|
||||
"</div>"
|
||||
);
|
||||
return `<div class="upload-button">
|
||||
<div class="progress">
|
||||
<div class="progress-bar"/>
|
||||
<a class="upload-button-link" href="javascript:void(0)" id="tool-panel-upload-button">
|
||||
<span class="fa fa-upload"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
export default { View: View };
|
||||
|
||||
@@ -28,7 +28,8 @@ var User = Backbone.Model.extend(baseMVC.LoggableMixin).extend(
|
||||
total_disk_usage: 0,
|
||||
nice_total_disk_usage: "",
|
||||
quota_percent: null,
|
||||
is_admin: false
|
||||
is_admin: false,
|
||||
preferences: {}
|
||||
},
|
||||
|
||||
/** Set up and bind events
|
||||
@@ -53,6 +54,29 @@ var User = Backbone.Model.extend(baseMVC.LoggableMixin).extend(
|
||||
return this.get("is_admin");
|
||||
},
|
||||
|
||||
updatePreferences: function(name, new_value) {
|
||||
let preferences = this.get("preferences");
|
||||
preferences[name] = JSON.stringify(new_value);
|
||||
this.preferences = preferences;
|
||||
},
|
||||
|
||||
getFavorites: function() {
|
||||
let preferences = this.get("preferences");
|
||||
if (preferences && preferences.favorites) {
|
||||
return JSON.parse(preferences.favorites);
|
||||
} else {
|
||||
return {
|
||||
tools: []
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
updateFavorites: function(object_type, new_favorites) {
|
||||
let favorites = this.getFavorites();
|
||||
favorites[object_type] = new_favorites[object_type];
|
||||
this.updatePreferences("favorites", favorites);
|
||||
},
|
||||
|
||||
/** Load a user with the API using an id.
|
||||
* If getting an anonymous user or no access to a user id, pass the User.CURRENT_ID_STR
|
||||
* (e.g. 'current') and the API will return the current transaction's user data.
|
||||
|
||||
@@ -90,7 +90,7 @@ class Workflow {
|
||||
var using_workflow_outputs = false;
|
||||
var has_existing_pjas = false;
|
||||
$.each(this.nodes, (k, node) => {
|
||||
if (node.workflow_outputs && node.workflow_outputs.length > 0) {
|
||||
if (node.type === "tool" && node.workflow_outputs && node.workflow_outputs.length > 0) {
|
||||
using_workflow_outputs = true;
|
||||
}
|
||||
$.each(node.post_job_actions, (pja_id, pja) => {
|
||||
@@ -102,43 +102,41 @@ class Workflow {
|
||||
if (using_workflow_outputs !== false || has_existing_pjas !== false) {
|
||||
// Using workflow outputs, or has existing pjas. Remove all PJAs and recreate based on outputs.
|
||||
$.each(this.nodes, (k, node) => {
|
||||
if (node.type === "tool") {
|
||||
var node_changed = false;
|
||||
if (node.post_job_actions === null) {
|
||||
node.post_job_actions = {};
|
||||
node_changed = true;
|
||||
var node_changed = false;
|
||||
if (node.post_job_actions === null) {
|
||||
node.post_job_actions = {};
|
||||
node_changed = true;
|
||||
}
|
||||
var pjas_to_rem = [];
|
||||
$.each(node.post_job_actions, (pja_id, pja) => {
|
||||
if (pja.action_type == "HideDatasetAction") {
|
||||
pjas_to_rem.push(pja_id);
|
||||
}
|
||||
var pjas_to_rem = [];
|
||||
$.each(node.post_job_actions, (pja_id, pja) => {
|
||||
if (pja.action_type == "HideDatasetAction") {
|
||||
pjas_to_rem.push(pja_id);
|
||||
});
|
||||
if (pjas_to_rem.length > 0) {
|
||||
$.each(pjas_to_rem, (i, pja_name) => {
|
||||
node_changed = true;
|
||||
delete node.post_job_actions[pja_name];
|
||||
});
|
||||
}
|
||||
if (using_workflow_outputs) {
|
||||
$.each(node.output_terminals, (ot_id, ot) => {
|
||||
var create_pja = !node.isWorkflowOutput(ot.name);
|
||||
if (create_pja === true) {
|
||||
node_changed = true;
|
||||
var pja = {
|
||||
action_type: "HideDatasetAction",
|
||||
output_name: ot.name,
|
||||
action_arguments: {}
|
||||
};
|
||||
node.post_job_actions[`HideDatasetAction${ot.name}`] = null;
|
||||
node.post_job_actions[`HideDatasetAction${ot.name}`] = pja;
|
||||
}
|
||||
});
|
||||
if (pjas_to_rem.length > 0) {
|
||||
$.each(pjas_to_rem, (i, pja_name) => {
|
||||
node_changed = true;
|
||||
delete node.post_job_actions[pja_name];
|
||||
});
|
||||
}
|
||||
if (using_workflow_outputs) {
|
||||
$.each(node.output_terminals, (ot_id, ot) => {
|
||||
var create_pja = !node.isWorkflowOutput(ot.name);
|
||||
if (create_pja === true) {
|
||||
node_changed = true;
|
||||
var pja = {
|
||||
action_type: "HideDatasetAction",
|
||||
output_name: ot.name,
|
||||
action_arguments: {}
|
||||
};
|
||||
node.post_job_actions[`HideDatasetAction${ot.name}`] = null;
|
||||
node.post_job_actions[`HideDatasetAction${ot.name}`] = pja;
|
||||
}
|
||||
});
|
||||
}
|
||||
// lastly, if this is the active node, and we made changes, reload the display at right.
|
||||
if (this.active_node == node && node_changed === true) {
|
||||
this.reload_active_node();
|
||||
}
|
||||
}
|
||||
// lastly, if this is the active node, and we made changes, reload the display at right.
|
||||
if (this.active_node == node && node_changed === true) {
|
||||
this.reload_active_node();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,14 +157,21 @@ var Terminal = Backbone.Model.extend({
|
||||
});
|
||||
},
|
||||
setMapOver: function(val) {
|
||||
let output_val = val;
|
||||
if (this.multiple) {
|
||||
return; // Cannot set this to be multirun...
|
||||
// emulate list input
|
||||
let description = new CollectionTypeDescription("list");
|
||||
if (val.collectionType === description.collectionType) {
|
||||
// No mapping over necessary
|
||||
return;
|
||||
}
|
||||
output_val = val.effectiveMapOver ? val.effectiveMapOver(description) : val;
|
||||
}
|
||||
|
||||
if (!this.mapOver().equal(val)) {
|
||||
this.terminalMapping.setMapOver(val);
|
||||
_.each(this.node.output_terminals, outputTerminal => {
|
||||
outputTerminal.setMapOver(val);
|
||||
outputTerminal.setMapOver(output_val);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -407,11 +414,8 @@ var InputTerminal = BaseInputTerminal.extend({
|
||||
// collection (yet...)
|
||||
return false;
|
||||
}
|
||||
if (otherCollectionType.rank == 1) {
|
||||
return this._producesAcceptableDatatype(other);
|
||||
} else {
|
||||
// TODO: Allow subcollection mapping over this as if it were
|
||||
// a list collection input.
|
||||
if (otherCollectionType.collectionType.endsWith("paired")) {
|
||||
// shouldn't process pairs in multiple="true" input
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +77,7 @@ var BaseInputTerminalView = TerminalView.extend({
|
||||
const name = input.name;
|
||||
const id = `node-${node.cid}-input-${name}`;
|
||||
const terminal = this.terminalForInput(input);
|
||||
if (!terminal.multiple) {
|
||||
this.setupMappingView(terminal);
|
||||
}
|
||||
this.setupMappingView(terminal);
|
||||
this.el.terminal = terminal;
|
||||
this.$el.attr("input-name", name);
|
||||
this.$el.attr("id", id);
|
||||
|
||||
@@ -641,8 +641,19 @@ export default Backbone.View.extend({
|
||||
url: `${getAppRoot()}api/workflows/build_module`,
|
||||
data: request_data,
|
||||
success: function(data) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
node.init_field_data(data);
|
||||
node.update_field_data(data);
|
||||
// Post init/update, for new modules we want to default to
|
||||
// nodes being outputs
|
||||
// TODO: Overhaul the handling of all this when we modernize
|
||||
// the editor, replace callout image manipulation with a simple
|
||||
// class toggle, etc.
|
||||
$.each(node.output_terminals, (ot_id, ot) => {
|
||||
node.addWorkflowOutput(ot.name);
|
||||
var callout = $(node.element).find(`.callout.${ot.name.replace(/(?=[()])/g, "\\")}`);
|
||||
callout.find("img").attr("src", `${Galaxy.root}static/images/fugue/asterisk-small.png`);
|
||||
});
|
||||
self.workflow.activate_node(node);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,11 +5,11 @@ import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import * as mod_toastr from "libs/toastr";
|
||||
import TAGS from "mvc/tag";
|
||||
import WORKFLOWS from "mvc/workflow/workflow-model";
|
||||
import QueryStringParsing from "utils/query-string-parsing";
|
||||
import _l from "utils/localization";
|
||||
import LoadingIndicator from "ui/loading-indicator";
|
||||
import { mountModelTags } from "components/Tags";
|
||||
|
||||
/** View of the individual workflows */
|
||||
const WorkflowItemView = Backbone.View.extend({
|
||||
@@ -127,8 +127,7 @@ const WorkflowItemView = Backbone.View.extend({
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="${wfId} tags-display">
|
||||
</div>
|
||||
<div class="${wfId} tags-display"></div>
|
||||
</td>
|
||||
<td>
|
||||
${this.model.get("owner") === Galaxy.user.attributes.username ? "You" : this.model.get("owner")}
|
||||
@@ -139,13 +138,13 @@ const WorkflowItemView = Backbone.View.extend({
|
||||
},
|
||||
|
||||
renderTagEditor: function() {
|
||||
const TagEditor = new TAGS.TagsEditor({
|
||||
let el = $(this.el).find(".tags-display")[0];
|
||||
let propsData = {
|
||||
model: this.model,
|
||||
el: $.find(`.${this.model.id}.tags-display`),
|
||||
workflow_mode: true
|
||||
});
|
||||
TagEditor.toggle(true);
|
||||
TagEditor.render();
|
||||
disabled: false,
|
||||
context: "workflow"
|
||||
};
|
||||
return mountModelTags(propsData, el);
|
||||
},
|
||||
|
||||
/** Template for user actions for workflows */
|
||||
@@ -195,13 +194,14 @@ const WorkflowListView = Backbone.View.extend({
|
||||
},
|
||||
|
||||
events: {
|
||||
dragleave: "unhighlightDropZone",
|
||||
drop: "drop",
|
||||
dragover: function(ev) {
|
||||
$(".hidden_description_layer").addClass("dragover");
|
||||
$(".menubutton").addClass("background-none");
|
||||
ev.preventDefault();
|
||||
}
|
||||
dragover: "highlightDropZone",
|
||||
dragleave: "unhighlightDropZone"
|
||||
},
|
||||
|
||||
highlightDropZone: function(ev) {
|
||||
$(".hidden_description_layer").addClass("dragover");
|
||||
$(".menubutton").addClass("background-none");
|
||||
ev.preventDefault();
|
||||
},
|
||||
|
||||
unhighlightDropZone: function() {
|
||||
@@ -272,6 +272,9 @@ const WorkflowListView = Backbone.View.extend({
|
||||
this.searchWorkflow(this.$(".search-wf"), this.$(".workflow-search tr"), minQueryLength);
|
||||
this.adjustActiondropdown();
|
||||
this._showArgErrors();
|
||||
this.$(".hidden_description_layer")
|
||||
.get(0)
|
||||
.addEventListener("drop", _.bind(this.drop, this));
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ QUnit.module("Masthead test", {
|
||||
use_remote_user: "use_remote_user",
|
||||
remote_user_logout_href: "remote_user_logout_href",
|
||||
lims_doc_url: "lims_doc_url",
|
||||
biostar_url: "biostar_url",
|
||||
biostar_url_redirect: "biostar_url_redirect",
|
||||
support_url: "support_url",
|
||||
search_url: "search_url",
|
||||
mailing_lists: "mailing_lists",
|
||||
|
||||
@@ -545,6 +545,8 @@ QUnit.module("Node view ", {
|
||||
input_terminals: {},
|
||||
output_terminals: {},
|
||||
markChanged: function() {},
|
||||
hasConnectedOutputTerminals: function() {},
|
||||
connectedMappedInputTerminals: function() {},
|
||||
terminalMapping: { disableMapOver: function() {} }
|
||||
});
|
||||
},
|
||||
@@ -1174,7 +1176,14 @@ QUnit.test("multiple input attachable by collections", function(assert) {
|
||||
this.verifyAttachable(assert, this.inputTerminal1, "list");
|
||||
});
|
||||
|
||||
QUnit.test("unconnected multiple inputs cannot be connected to rank > 1 collections (yet...)", function(assert) {
|
||||
QUnit.test("multiple input attachable by nested collections", function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal(null, { multiple: true });
|
||||
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
|
||||
this.addConnectedOutput(connectedInput1);
|
||||
this.verifyAttachable(assert, this.inputTerminal1, "list:list");
|
||||
});
|
||||
|
||||
QUnit.test("Multiple inputs cannot be connected to pairs", function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal(null, { multiple: true });
|
||||
this.verifyNotAttachable(assert, this.inputTerminal1, "list:paired");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import * as d3 from "../libs/d3";
|
||||
import { event as currentEvent } from "../libs/d3";
|
||||
import * as d3 from "d3";
|
||||
import { event as currentEvent } from "d3";
|
||||
|
||||
function date_by_subtracting_days(date, days) {
|
||||
return new Date(
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
describe("Sample Test", () => {
|
||||
it("hey look, tests run", () => {
|
||||
assert.equal(1, 1);
|
||||
});
|
||||
|
||||
it("prove I can use a Proxy", () => {
|
||||
let target = {};
|
||||
let thing = new Proxy(target, {});
|
||||
thing.foo = 232;
|
||||
assert.equal(target.foo, thing.foo);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Vuex store module used for managing search parameters in the grid. Currently
|
||||
* only manages the tags that access this store via the new tagging components,
|
||||
* but presumably the entire grid search filter criteria will live here one day.
|
||||
*/
|
||||
|
||||
export const gridSearchStore = {
|
||||
state: {
|
||||
searchTags: new Set()
|
||||
},
|
||||
mutations: {
|
||||
// TODO: we could write an equivalence comparator here for searchTag
|
||||
// Sets and not register a change if the new set is equivalent
|
||||
setSearchTags(state, tags) {
|
||||
state.searchTags = new Set(tags);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
toggleSearchTag({ state, commit }, { text }) {
|
||||
let tags = new Set(state.searchTags);
|
||||
tags.has(text) ? tags.delete(text) : tags.add(text);
|
||||
commit("setSearchTags", tags);
|
||||
},
|
||||
removeSearchTag({ state, commit }, { text }) {
|
||||
let tags = new Set(state.searchTags);
|
||||
tags.delete(text);
|
||||
commit("setSearchTags", tags);
|
||||
},
|
||||
clearSearchTags({ state, commit }) {
|
||||
commit("setSearchTags", new Set());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import store from "./index";
|
||||
|
||||
describe("store/gridSearchStore.js", () => {
|
||||
it("the searchTags in the store should be a Set object", () => {
|
||||
let searchTags = store.state.gridSearch.searchTags; // this is a Set()
|
||||
assert(searchTags instanceof Set, "searchTags wrong variable type, should be Set()");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Central Vuex store
|
||||
*/
|
||||
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import { gridSearchStore } from "./gridSearchStore";
|
||||
import { tagStore } from "./tagStore";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
gridSearch: gridSearchStore,
|
||||
tags: tagStore
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
export const state = {
|
||||
userTagList: [], // List of recent user tags
|
||||
modelTagCache: new Map() // Maps model id to list of tags
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getTagsById: state => key => {
|
||||
if (state.modelTagCache.has(key)) {
|
||||
let tagSet = state.modelTagCache.get(key); //.sort();
|
||||
return Array.from(tagSet);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
updateTags({ commit }, { key, tags }) {
|
||||
commit("setTags", { key, tags });
|
||||
},
|
||||
initializeTags({ dispatch, state }, { key, tags }) {
|
||||
if (!state.modelTagCache.has(key)) {
|
||||
dispatch("updateTags", { key, tags });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
setTags(state, { key, tags }) {
|
||||
state.modelTagCache = new Map(state.modelTagCache);
|
||||
state.modelTagCache.set(key, new Set(tags));
|
||||
},
|
||||
reset(state) {
|
||||
state.userTagList = [];
|
||||
state.modelTagCache = new Map();
|
||||
}
|
||||
};
|
||||
|
||||
export const tagStore = {
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { tagStore } from "./tagStore";
|
||||
|
||||
describe("store/tagStore.js", () => {
|
||||
const state = tagStore.state;
|
||||
const { reset } = tagStore.mutations;
|
||||
|
||||
afterEach(() => {
|
||||
reset(state);
|
||||
});
|
||||
|
||||
describe("mutations/setTags", () => {
|
||||
const { setTags } = tagStore.mutations;
|
||||
const testKey = "foo";
|
||||
const testTags = ["a", "b", "c", "b"];
|
||||
|
||||
let stateTags;
|
||||
|
||||
beforeEach(() => {
|
||||
setTags(state, { key: testKey, tags: testTags });
|
||||
stateTags = state.modelTagCache.get(testKey);
|
||||
});
|
||||
|
||||
it("should update the state Map and store a Set", () => {
|
||||
assert(stateTags instanceof Set, "Stored list should be a Set");
|
||||
testTags.forEach(t => {
|
||||
assert(stateTags.has(t), `Missing tag: ${t}`);
|
||||
});
|
||||
});
|
||||
|
||||
it("that set should contain all the passed tags", () => {
|
||||
testTags.forEach(t => {
|
||||
assert(stateTags.has(t), `Missing tag: ${t}`);
|
||||
});
|
||||
});
|
||||
|
||||
it("should store a list of unique values", () => {
|
||||
assert(stateTags.size == 3, "Stored list should only consist of unique items");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getters/getTagsById", () => {
|
||||
const { getTagsById } = tagStore.getters;
|
||||
const { setTags } = tagStore.mutations;
|
||||
const testKey = "foo";
|
||||
const testTags = ["a", "b", "c", "b"];
|
||||
|
||||
let thisGetter;
|
||||
|
||||
beforeEach(() => {
|
||||
setTags(state, { key: testKey, tags: testTags });
|
||||
// getter functions are compound functions, need to build the getter first
|
||||
thisGetter = getTagsById(state);
|
||||
});
|
||||
|
||||
it("should update the state Map and store a Set", () => {
|
||||
let tags = thisGetter(testKey);
|
||||
assert(tags instanceof Array, "returned result should be a simple array");
|
||||
assert((tags.length = 3));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,369 +0,0 @@
|
||||
import $ from "jquery";
|
||||
import _ from "underscore";
|
||||
|
||||
// ============================================================================
|
||||
/**
|
||||
* JQuery extension for tagging with autocomplete.
|
||||
* @author: Jeremy Goecks
|
||||
* @require: jquery.autocomplete plugin
|
||||
*/
|
||||
//
|
||||
// Initialize "tag click functions" for tags.
|
||||
//
|
||||
export function init_tag_click_function(tag_elt, click_func) {
|
||||
$(tag_elt)
|
||||
.find(".tag-name")
|
||||
.each(function() {
|
||||
$(this).click(function() {
|
||||
var tag_str = $(this).text();
|
||||
var tag_name_and_value = tag_str.split(":");
|
||||
click_func(tag_name_and_value[0], tag_name_and_value[1]);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$.fn.autocomplete_tagging = function(options) {
|
||||
var defaults = {
|
||||
get_toggle_link_text_fn: function(tags) {
|
||||
var text = "";
|
||||
var num_tags = _.size(tags);
|
||||
if (num_tags > 0) {
|
||||
text = num_tags + (num_tags > 1 ? " Tags" : " Tag");
|
||||
} else {
|
||||
text = "Add tags";
|
||||
}
|
||||
return text;
|
||||
},
|
||||
tag_click_fn: function(name, value) {},
|
||||
editable: true,
|
||||
input_size: 20,
|
||||
in_form: false,
|
||||
tags: {},
|
||||
use_toggle_link: true,
|
||||
item_id: "",
|
||||
add_tag_img: "",
|
||||
add_tag_img_rollover: "",
|
||||
delete_tag_img: "",
|
||||
ajax_autocomplete_tag_url: "",
|
||||
ajax_retag_url: "",
|
||||
ajax_delete_tag_url: "",
|
||||
ajax_add_tag_url: ""
|
||||
};
|
||||
|
||||
var settings = $.extend(defaults, options);
|
||||
|
||||
//
|
||||
// Initalize object's elements.
|
||||
//
|
||||
|
||||
// Get elements for this object. For this_obj, assume the last element with the id is the "this"; this is somewhat of a hack to address the problem
|
||||
// that there may be two tagging elements for a single item if there are both community and individual tags for an element.
|
||||
var this_obj = $(this);
|
||||
var tag_area = this_obj.find(".tag-area");
|
||||
var toggle_link = this_obj.find(".toggle-link");
|
||||
var tag_input_field = this_obj.find(".tag-input");
|
||||
var add_tag_button = this_obj.find(".add-tag-button");
|
||||
|
||||
// Initialize toggle link.
|
||||
toggle_link.click(function() {
|
||||
// Take special actions depending on whether toggle is showing or hiding link.
|
||||
var after_toggle_fn;
|
||||
if (tag_area.is(":hidden")) {
|
||||
after_toggle_fn = function() {
|
||||
// If there are no tags, go right to editing mode by generating a click on the area.
|
||||
var num_tags = $(this).find(".tag-button").length;
|
||||
if (num_tags === 0) {
|
||||
tag_area.click();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
after_toggle_fn = () => {
|
||||
tag_area.blur();
|
||||
};
|
||||
}
|
||||
tag_area.slideToggle("fast", after_toggle_fn);
|
||||
return $(this);
|
||||
});
|
||||
|
||||
// Initialize tag input field.
|
||||
if (settings.editable) {
|
||||
tag_input_field.hide();
|
||||
}
|
||||
tag_input_field.keyup(function(e) {
|
||||
if (e.keyCode === 27) {
|
||||
// Escape key
|
||||
$(this).trigger("blur");
|
||||
} else if (
|
||||
e.keyCode === 13 || // Return Key
|
||||
e.keyCode === 188 || // Comma
|
||||
e.keyCode === 32 // Space
|
||||
) {
|
||||
//
|
||||
// Check input.
|
||||
//
|
||||
|
||||
var new_value = this.value;
|
||||
|
||||
// Suppress space after a ":"
|
||||
if (new_value.indexOf(": ", new_value.length - 2) !== -1) {
|
||||
this.value = new_value.substring(0, new_value.length - 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove trigger keys from input.
|
||||
if (e.keyCode === 188 || e.keyCode === 32) {
|
||||
new_value = new_value.substring(0, new_value.length - 1);
|
||||
}
|
||||
|
||||
// Trim whitespace.
|
||||
new_value = $.trim(new_value);
|
||||
|
||||
// Too short?
|
||||
if (new_value.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// New tag OK - apply it.
|
||||
//
|
||||
|
||||
this.value = ""; // Reset text field now that tag is being added
|
||||
|
||||
// Add button for tag after all other tag buttons.
|
||||
var new_tag_button = build_tag_button(new_value);
|
||||
var tag_buttons = tag_area.children(".tag-button");
|
||||
if (tag_buttons.length !== 0) {
|
||||
var last_tag_button = tag_buttons.slice(tag_buttons.length - 1);
|
||||
last_tag_button.after(new_tag_button);
|
||||
} else {
|
||||
tag_area.prepend(new_tag_button);
|
||||
}
|
||||
|
||||
// Add tag to internal list.
|
||||
var tag_name_and_value = new_value.split(":");
|
||||
settings.tags[tag_name_and_value[0]] = tag_name_and_value[1];
|
||||
|
||||
// Update toggle link text.
|
||||
var new_text = settings.get_toggle_link_text_fn(settings.tags);
|
||||
toggle_link.text(new_text);
|
||||
|
||||
// Commit tag to server.
|
||||
var zz = $(this);
|
||||
$.ajax({
|
||||
url: settings.ajax_add_tag_url,
|
||||
data: { new_tag: new_value },
|
||||
error: function() {
|
||||
// Failed. Roll back changes and show alert.
|
||||
new_tag_button.remove();
|
||||
delete settings.tags[tag_name_and_value[0]];
|
||||
var new_text = settings.get_toggle_link_text_fn(settings.tags);
|
||||
toggle_link.text(new_text);
|
||||
alert("Add tag failed");
|
||||
},
|
||||
success: function() {
|
||||
// Flush autocomplete cache because it's not out of date.
|
||||
// TODO: in the future, we could remove the particular item
|
||||
// that was chosen from the cache rather than flush it.
|
||||
zz.data("autocompleter").cacheFlush();
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Add autocomplete to input.
|
||||
var format_item_func = (key, row_position, num_rows, value, search_term) => {
|
||||
var tag_name_and_value = value.split(":");
|
||||
return tag_name_and_value.length === 1 ? tag_name_and_value[0] : tag_name_and_value[1];
|
||||
};
|
||||
var autocomplete_options = {
|
||||
selectFirst: false,
|
||||
formatItem: format_item_func,
|
||||
autoFill: false,
|
||||
highlight: false
|
||||
};
|
||||
tag_input_field.autocomplete_verheul(settings.ajax_autocomplete_tag_url, autocomplete_options);
|
||||
|
||||
// Initialize delete tag images for current tags.
|
||||
this_obj.find(".delete-tag-img").each(function() {
|
||||
init_delete_tag_image($(this));
|
||||
});
|
||||
|
||||
// Initialize tag click function.
|
||||
init_tag_click_function($(this), settings.tag_click_fn);
|
||||
|
||||
// Initialize "add tag" button.
|
||||
add_tag_button.click(function() {
|
||||
$(this).hide();
|
||||
|
||||
// Clicking on button is the same as clicking on the tag area.
|
||||
tag_area.click();
|
||||
return false;
|
||||
});
|
||||
|
||||
//
|
||||
// Set up tag area interactions; these are needed only if tags are editable.
|
||||
//
|
||||
if (settings.editable) {
|
||||
// When the tag area blurs, go to "view tag" mode.
|
||||
tag_area.bind("blur", e => {
|
||||
if (_.size(settings.tags) > 0) {
|
||||
add_tag_button.show();
|
||||
tag_input_field.hide();
|
||||
tag_area.removeClass("active-tag-area");
|
||||
// tag_area.addClass("tooltip");
|
||||
} else {
|
||||
// No tags, so do nothing to ensure that input is still visible.
|
||||
}
|
||||
});
|
||||
|
||||
// On click, enable user to add tags.
|
||||
tag_area.click(function(e) {
|
||||
var is_active = $(this).hasClass("active-tag-area");
|
||||
|
||||
// If a "delete image" object was pressed and area is inactive, do nothing.
|
||||
if ($(e.target).hasClass("delete-tag-img") && !is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a "tag name" object was pressed and area is inactive, do nothing.
|
||||
if ($(e.target).hasClass("tag-name") && !is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove tooltip.
|
||||
// $(this).removeClass("tooltip");
|
||||
|
||||
// Hide add tag button, show tag_input field. Change background to show
|
||||
// area is active.
|
||||
$(this).addClass("active-tag-area");
|
||||
add_tag_button.hide();
|
||||
tag_input_field.show();
|
||||
tag_input_field.focus();
|
||||
|
||||
// Add handler to document that will call blur when the tag area is blurred;
|
||||
// a tag area is blurred when a user clicks on an element outside the area.
|
||||
var handle_document_click = e => {
|
||||
var check_click = function(tag_area, target) {
|
||||
// Blur the tag area if the element clicked on is not in the tag area.
|
||||
if (target !== tag_area) {
|
||||
tag_area.blur();
|
||||
$(window).unbind("click.tagging_blur");
|
||||
$(this).addClass("tooltip");
|
||||
}
|
||||
};
|
||||
check_click(tag_area, $(e.target));
|
||||
};
|
||||
// TODO: we should attach the click handler to all frames in order to capture
|
||||
// clicks outside the frame that this element is in.
|
||||
//window.parent.document.onclick = handle_document_click;
|
||||
//var temp = $(window.parent.document.body).contents().find("iframe").html();
|
||||
//alert(temp);
|
||||
//$(document).parent().click(handle_document_click);
|
||||
$(window).bind("click.tagging_blur", handle_document_click);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// If using toggle link, hide the tag area. Otherwise, show the tag area.
|
||||
if (settings.use_toggle_link) {
|
||||
tag_area.hide();
|
||||
}
|
||||
|
||||
//
|
||||
// Helper functions.
|
||||
//
|
||||
|
||||
// Initialize a "delete tag image": when click, delete tag from UI and send delete request to server.
|
||||
function init_delete_tag_image(delete_img) {
|
||||
$(delete_img).mouseenter(function() {
|
||||
$(this).attr("src", settings.delete_tag_img_rollover);
|
||||
});
|
||||
$(delete_img).mouseleave(function() {
|
||||
$(this).attr("src", settings.delete_tag_img);
|
||||
});
|
||||
$(delete_img).click(function() {
|
||||
// Tag button is image's parent.
|
||||
var tag_button = $(this).parent();
|
||||
|
||||
// Get tag name, value.
|
||||
var tag_name_elt = tag_button.find(".tag-name").eq(0);
|
||||
var tag_str = tag_name_elt.text();
|
||||
var tag_name_and_value = tag_str.split(":");
|
||||
var tag_name = tag_name_and_value[0];
|
||||
var tag_value = tag_name_and_value[1];
|
||||
|
||||
var prev_button = tag_button.prev();
|
||||
tag_button.remove();
|
||||
|
||||
// Remove tag from local list for consistency.
|
||||
delete settings.tags[tag_name];
|
||||
|
||||
// Update toggle link text.
|
||||
var new_text = settings.get_toggle_link_text_fn(settings.tags);
|
||||
toggle_link.text(new_text);
|
||||
|
||||
// Delete tag.
|
||||
$.ajax({
|
||||
url: settings.ajax_delete_tag_url,
|
||||
data: { tag_name: tag_name },
|
||||
error: function() {
|
||||
// Failed. Roll back changes and show alert.
|
||||
settings.tags[tag_name] = tag_value;
|
||||
if (prev_button.hasClass("tag-button")) {
|
||||
prev_button.after(tag_button);
|
||||
} else {
|
||||
tag_area.prepend(tag_button);
|
||||
}
|
||||
alert("Remove tag failed");
|
||||
|
||||
toggle_link.text(settings.get_toggle_link_text_fn(settings.tags));
|
||||
|
||||
// TODO: no idea why it's necessary to set this up again.
|
||||
delete_img.mouseenter(function() {
|
||||
$(this).attr("src", settings.delete_tag_img_rollover);
|
||||
});
|
||||
delete_img.mouseleave(function() {
|
||||
$(this).attr("src", settings.delete_tag_img);
|
||||
});
|
||||
},
|
||||
success: function() {}
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Function that builds a tag button.
|
||||
//
|
||||
function build_tag_button(tag_str) {
|
||||
// Build "delete tag" image.
|
||||
var delete_img = $("<img/>")
|
||||
.attr("src", settings.delete_tag_img)
|
||||
.addClass("delete-tag-img");
|
||||
init_delete_tag_image(delete_img);
|
||||
|
||||
// Build tag button.
|
||||
var tag_name_elt = $("<span>")
|
||||
.text(tag_str)
|
||||
.addClass("tag-name");
|
||||
tag_name_elt.click(() => {
|
||||
var tag_name_and_value = tag_str.split(":");
|
||||
settings.tag_click_fn(tag_name_and_value[0], tag_name_and_value[1]);
|
||||
return true;
|
||||
});
|
||||
|
||||
var tag_button = $("<span></span>").addClass("tag-button");
|
||||
tag_button.append(tag_name_elt);
|
||||
// Allow delete only if element is editable.
|
||||
if (settings.editable) {
|
||||
tag_button.append(delete_img);
|
||||
}
|
||||
|
||||
return tag_button;
|
||||
}
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* A combination of all the mocha test files so that we can run them as a unit,
|
||||
* which is much faster during deployment.
|
||||
*
|
||||
* Note, this is non-intuitive, but the parameters of require.context must be
|
||||
* literals, can't even be stored in local variables or webpack can't do static
|
||||
* dependency analysis.
|
||||
*
|
||||
* https://webpack.js.org/guides/dependency-management/
|
||||
*/
|
||||
// eslint-disable-next-line no-undef
|
||||
let testContext = require.context("./", true, /\.test\.js$/);
|
||||
testContext.keys().forEach(testContext);
|
||||
@@ -0,0 +1,71 @@
|
||||
import { hashFnv32a } from "utils/utils";
|
||||
|
||||
/**
|
||||
* Implement W3C contrasting color algorithm
|
||||
* http://www.w3.org/TR/AERT#color-contrast
|
||||
*
|
||||
* @param {number} r Red
|
||||
* @param {number} g Green
|
||||
* @param {number} b Blue
|
||||
* @return {string} Either 'white' or 'black'
|
||||
*
|
||||
* Assumes r, g, b are in the set [0, 1]
|
||||
*/
|
||||
export function contrastingColor(r, g, b) {
|
||||
var o = (r * 255 * 299 + g * 255 * 587 + b * 255 * 114) / 1000;
|
||||
return o > 125 ? "black" : "white";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HSL color value to RGB. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes h, s, and l are contained in the set [0, 1] and
|
||||
* returns r, g, and b in the set [0, 1].
|
||||
*
|
||||
* @param {number} h The hue
|
||||
* @param {number} s The saturation
|
||||
* @param {number} l The lightness
|
||||
* @return {Array} The RGB representation
|
||||
*/
|
||||
export function hslToRgb(h, s, l) {
|
||||
var r, g, b;
|
||||
|
||||
if (s == 0) {
|
||||
r = g = b = l; // achromatic
|
||||
} else {
|
||||
var hue2rgb = function hue2rgb(p, q, t) {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
var p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple 3-color keyed color scheme generated
|
||||
* from a string key
|
||||
*/
|
||||
export function keyedColorScheme(strKey) {
|
||||
let hash = hashFnv32a(strKey);
|
||||
let hue = Math.abs((hash >> 4) % 360);
|
||||
let lightnessOffset = 75;
|
||||
let lightness = lightnessOffset + (hash & 0xf);
|
||||
|
||||
let primary = `hsl(${hue}, 100%, ${lightness}%)`;
|
||||
let darker = `hsl(${hue}, 100%, ${lightness - 40}%)`;
|
||||
let [r, g, b] = hslToRgb(hue, 1.0, lightness / 100);
|
||||
let contrasting = contrastingColor(r, g, b);
|
||||
|
||||
return { primary, darker, contrasting };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// console.log api but does nothing
|
||||
// Applies noop to all methods for mock creation
|
||||
|
||||
const doNothing = () => null;
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Generic Vue component mount for use in transitional
|
||||
// mount functions
|
||||
|
||||
import Vue from "vue";
|
||||
import store from "../store";
|
||||
import _l from "utils/localization";
|
||||
|
||||
// make localization filter available to all components
|
||||
Vue.filter("localize", value => _l(value));
|
||||
|
||||
export const mountVueComponent = ComponentDefinition => (propsData, el) => {
|
||||
let component = Vue.extend(ComponentDefinition);
|
||||
return new component({ store, propsData, el });
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
// This file exists purely to make unit testing easier
|
||||
|
||||
export function redirectToUrl(url) {
|
||||
window.location = url;
|
||||
}
|
||||
@@ -356,7 +356,7 @@ export function setWindowTitle(title) {
|
||||
* @param {string} str the input value
|
||||
* @returns {integer}
|
||||
*/
|
||||
function hashFnv32a(str) {
|
||||
export function hashFnv32a(str) {
|
||||
var i,
|
||||
l,
|
||||
hval = 0x811c9dc5;
|
||||
@@ -368,72 +368,6 @@ function hashFnv32a(str) {
|
||||
return hval >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement W3C contrasting color algorithm
|
||||
* http://www.w3.org/TR/AERT#color-contrast
|
||||
*
|
||||
* @param {number} r Red
|
||||
* @param {number} g Green
|
||||
* @param {number} b Blue
|
||||
* @return {string} Either 'white' or 'black'
|
||||
*
|
||||
* Assumes r, g, b are in the set [0, 1]
|
||||
*/
|
||||
function contrastingColor(r, g, b) {
|
||||
var o = (r * 255 * 299 + g * 255 * 587 + b * 255 * 114) / 1000;
|
||||
return o > 125 ? "black" : "white";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HSL color value to RGB. Conversion formula
|
||||
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
|
||||
* Assumes h, s, and l are contained in the set [0, 1] and
|
||||
* returns r, g, and b in the set [0, 1].
|
||||
*
|
||||
* @param {number} h The hue
|
||||
* @param {number} s The saturation
|
||||
* @param {number} l The lightness
|
||||
* @return {Array} The RGB representation
|
||||
*/
|
||||
function hslToRgb(h, s, l) {
|
||||
var r, g, b;
|
||||
|
||||
if (s == 0) {
|
||||
r = g = b = l; // achromatic
|
||||
} else {
|
||||
var hue2rgb = function hue2rgb(p, q, t) {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||
if (t < 1 / 2) return q;
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
var p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
export function generateTagStyle(tag) {
|
||||
var hash = hashFnv32a(tag);
|
||||
var hue = Math.abs((hash >> 4) % 360);
|
||||
var lightnessOffset = 75;
|
||||
var lightness = lightnessOffset + (hash & 0xf);
|
||||
var bgColor = `hsl(${hue}, 100%, ${lightness}%)`;
|
||||
var brColor = `hsl(${hue}, 100%, ${lightness - 40}%)`;
|
||||
|
||||
var [r, g, b] = hslToRgb(hue, 1.0, lightness / 100);
|
||||
var fgColor = contrastingColor(r, g, b);
|
||||
|
||||
return `background-color: ${bgColor}; color: ${fgColor}; border: 1px solid ${brColor}`;
|
||||
}
|
||||
|
||||
export default {
|
||||
cssLoadFile: cssLoadFile,
|
||||
cssGetAttribute: cssGetAttribute,
|
||||
@@ -453,6 +387,5 @@ export default {
|
||||
linkify: linkify,
|
||||
appendScriptStyle: appendScriptStyle,
|
||||
getQueryString: getQueryString,
|
||||
setWindowTitle: setWindowTitle,
|
||||
generateTagStyle: generateTagStyle
|
||||
setWindowTitle: setWindowTitle
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import _l from "utils/localization";
|
||||
import * as d3 from "libs/d3";
|
||||
import * as d3 from "d3";
|
||||
import visualization_mod from "viz/visualization";
|
||||
import { Dataset } from "mvc/dataset/data";
|
||||
import mod_icon_btn from "mvc/ui/icon-button";
|
||||
|
||||
@@ -8,7 +8,7 @@ import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import _l from "utils/localization";
|
||||
import * as d3 from "libs/d3";
|
||||
import * as d3 from "d3";
|
||||
import visualization from "viz/visualization";
|
||||
import tracks from "viz/trackster/tracks";
|
||||
import tools from "mvc/tool/tools";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Bootstrap variables and core framework
|
||||
@import "../../../node_modules/bootstrap/scss/_functions.scss";
|
||||
@import "~bootstrap/scss/_functions.scss";
|
||||
@import "theme/blue.scss";
|
||||
@import "../../../node_modules/bootstrap/scss/bootstrap.scss";
|
||||
@import "~bootstrap/scss/bootstrap.scss";
|
||||
|
||||
// Bootstrap-related style overrides
|
||||
@import "overrides.scss";
|
||||
@@ -30,7 +30,6 @@ $fa-font-path: "../../../node_modules/font-awesome/fonts/";
|
||||
@import "ui.scss";
|
||||
@import "library.scss";
|
||||
@import "trackster.scss";
|
||||
@import "autocomplete_tagging.scss";
|
||||
@import "toastr.scss";
|
||||
@import "jstree.scss";
|
||||
@import "tour.scss";
|
||||
@@ -233,28 +232,14 @@ body {
|
||||
.panel-header-buttons {
|
||||
order: 9999;
|
||||
float: right;
|
||||
}
|
||||
.panel-header-button {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
width: 1.2rem;
|
||||
text-align: center;
|
||||
padding: 0px;
|
||||
&:not(:last-child) {
|
||||
margin-right: 2px;
|
||||
}
|
||||
&:hover {
|
||||
color: $brand-info;
|
||||
}
|
||||
// Bootstrap style span caret needs positioning
|
||||
.caret {
|
||||
margin-top: 7px;
|
||||
}
|
||||
// Another way to get a caret
|
||||
&.popup {
|
||||
padding-right: 1.75em;
|
||||
background: url(../../images/dropdownarrow.png) no-repeat right 7px;
|
||||
.panel-header-button {
|
||||
text-align: center;
|
||||
&:not(:last-child) {
|
||||
@extend .mr-2;
|
||||
}
|
||||
&:hover {
|
||||
color: $brand-info;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -807,9 +792,9 @@ button {
|
||||
.warningmessagelarge,
|
||||
.donemessagelarge,
|
||||
.infomessagelarge,
|
||||
.ui-form-help .error,
|
||||
.ui-form-help .warning,
|
||||
.ui-form-help .note {
|
||||
.form-help .error,
|
||||
.form-help .warning,
|
||||
.form-help .note {
|
||||
@extend .alert;
|
||||
min-height: 36px;
|
||||
padding-left: 52px;
|
||||
@@ -851,9 +836,9 @@ button {
|
||||
.warningmessagesmall,
|
||||
.donemessagesmall,
|
||||
.infomessagesmall,
|
||||
.ui-form-help .error,
|
||||
.ui-form-help .warning,
|
||||
.ui-form-help .note {
|
||||
.form-help .error,
|
||||
.form-help .warning,
|
||||
.form-help .note {
|
||||
@extend .alert;
|
||||
padding: 5px;
|
||||
padding-left: 25px;
|
||||
@@ -867,13 +852,13 @@ button {
|
||||
|
||||
.errormessage,
|
||||
.errormessagesmall,
|
||||
.ui-form-help .error {
|
||||
.form-help .error {
|
||||
@extend .alert-danger;
|
||||
}
|
||||
|
||||
.warningmessage,
|
||||
.warningmessagesmall,
|
||||
.ui-form-help .warning {
|
||||
.form-help .warning {
|
||||
@extend .alert-warning;
|
||||
background-image: url(../../images/warn_small.png);
|
||||
}
|
||||
@@ -886,7 +871,7 @@ button {
|
||||
|
||||
.infomessage,
|
||||
.infomessagesmall,
|
||||
.ui-form-help .note {
|
||||
.form-help .note {
|
||||
@extend .alert-info;
|
||||
background-image: url(../../images/info_small.png);
|
||||
}
|
||||
@@ -1559,8 +1544,6 @@ div.toolPanelLabel {
|
||||
}
|
||||
|
||||
div.toolTitle {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
display: block;
|
||||
@@ -1573,10 +1556,16 @@ div.toolTitle {
|
||||
div a.tool-link {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
|
||||
span.tool-old-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: darken($panel-bg-color, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
div.toolSectionBody div.toolPanelLabel {
|
||||
@@ -1780,11 +1769,6 @@ div.toolTitleNoSection {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.communication-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
@@ -1860,19 +1844,6 @@ div.toolTitleNoSection {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Temporary tag editor display; will be moved to a scoped Vue SFC upon refactoring */
|
||||
.tags-display {
|
||||
@extend %vertical-spacing;
|
||||
.select2-container {
|
||||
max-height: 80px;
|
||||
overflow: auto;
|
||||
min-width: 0px;
|
||||
.select2-choices {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* TEMPORARY, REFACTOR THIS -- This is only image-related styles from the
|
||||
* workflow editor; everything should be moved out of the mako and into its own
|
||||
* file */
|
||||
|
||||
@@ -304,10 +304,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tags-display {
|
||||
// Hidden by default; the interface toggles this.
|
||||
display: none;
|
||||
}
|
||||
.annotation-display {
|
||||
display: none;
|
||||
@extend %vertical-spacing;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import "scss/mixins";
|
||||
|
||||
// all histories
|
||||
.history-panel {
|
||||
@extend .flex-vertical-container;
|
||||
@@ -34,7 +36,6 @@
|
||||
}
|
||||
|
||||
//TODO: move these out
|
||||
.tags-display,
|
||||
.annotation-display {
|
||||
display: none;
|
||||
margin-bottom: 8px;
|
||||
@@ -194,15 +195,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
div.nametags {
|
||||
max-height: 26px;
|
||||
overflow-y: auto;
|
||||
span.badge {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +210,6 @@
|
||||
.subtitle,
|
||||
.history-size,
|
||||
//TODO: move these out
|
||||
.tags-display .prompt,
|
||||
.annotation-display .prompt {
|
||||
display: none;
|
||||
}
|
||||
@@ -629,3 +620,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// History panel tags editor
|
||||
.history-panel .tags-display {
|
||||
padding: 1px;
|
||||
@include shutterFade(250px);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Utility mixin expands to container edges
|
||||
@mixin fill() {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// animates max-height and visibility hidden by default, add .active to show
|
||||
// boxHeight must be larger than the expected maximum height of the element
|
||||
// or it will be clipped
|
||||
|
||||
@mixin shutterFade($boxHeight: 100px) {
|
||||
max-height: 0px;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition-duration: 0.35s;
|
||||
transition-property: visibility, opacity, max-height;
|
||||
transition-timing-function: ease;
|
||||
|
||||
&.active {
|
||||
max-height: $boxHeight;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -46,13 +46,6 @@ input[type="radio"] {
|
||||
float: left;
|
||||
}
|
||||
|
||||
// Override badge's few default CSS
|
||||
.badge-tags {
|
||||
background-color: #3189a3;
|
||||
padding: 0.2em 0.6em 0.3em;
|
||||
border-radius: 0.15rem;
|
||||
}
|
||||
|
||||
// Modal -- wider by default, scroll like Trello
|
||||
|
||||
.modal-dialog {
|
||||
|
||||
@@ -94,7 +94,7 @@ $text-color: $gray-900;
|
||||
$masthead-height: 3rem;
|
||||
|
||||
// Side panels
|
||||
$panel-bg-color: $gray-200;
|
||||
$panel-bg-color: $gray-100;
|
||||
$panel-text-color: $text-color;
|
||||
$panel-header-text-color: $text-color;
|
||||
$panel-footer-bg-color: $panel-bg-color;
|
||||
|
||||
@@ -195,7 +195,6 @@ $ui-margin-horizontal-large: $margin-v * 2;
|
||||
.ui-portlet {
|
||||
@extend .card;
|
||||
border: none;
|
||||
height: 100%;
|
||||
.portlet-header:first-child {
|
||||
@extend .card-header;
|
||||
@extend .rounded;
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
.upload-button {
|
||||
position: relative;
|
||||
width: 31px;
|
||||
height: 21px;
|
||||
@extend .ml-2;
|
||||
float: right;
|
||||
width: 32px;
|
||||
height: 24px;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
-moz-border-radius: $border-radius-base;
|
||||
border-radius: $border-radius-base;
|
||||
.upload-button-link {
|
||||
@extend .panel-header-button;
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
}
|
||||
.progress {
|
||||
height: 21px;
|
||||
margin: 0px;
|
||||
font-size: 0.75rem;
|
||||
position: absolute;
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
font-size: inherit;
|
||||
background-color: $panel-bg-color;
|
||||
}
|
||||
.progress-bar-danger {
|
||||
@@ -27,13 +35,6 @@
|
||||
-o-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
.panel-header-button {
|
||||
position: absolute;
|
||||
width: 31px !important;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-input {
|
||||
|
||||
+41
-37
@@ -1,13 +1,11 @@
|
||||
var path = require("path");
|
||||
var fs = require("fs");
|
||||
var del = require("del");
|
||||
var _ = require("underscore");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const del = require("del");
|
||||
const { src, dest, series, parallel } = require("gulp");
|
||||
const uglify = require("gulp-uglify-es").default;
|
||||
const babel = require("gulp-babel");
|
||||
|
||||
var gulp = require("gulp");
|
||||
var uglify = require("gulp-uglify");
|
||||
var babel = require("gulp-babel");
|
||||
|
||||
var paths = {
|
||||
const paths = {
|
||||
node_modules: "./node_modules",
|
||||
scripts: [
|
||||
"galaxy/scripts/**/*.js",
|
||||
@@ -15,14 +13,16 @@ var paths = {
|
||||
"!galaxy/scripts/entry/**/*",
|
||||
"!galaxy/scripts/libs/**/*"
|
||||
],
|
||||
plugin_dirs: ["../config/plugins/**/static/**/*", "!../config/plugins/**/node_modules{,/**}"],
|
||||
plugin_dirs: [
|
||||
"../config/plugins/{visualizations,interactive_environments}/*/static/**/*",
|
||||
"../config/plugins/{visualizations,interactive_environments}/*/*/static/**/*"
|
||||
],
|
||||
lib_locs: {
|
||||
// This is a stepping stone towards having all this staged
|
||||
// automatically. Eventually, this dictionary and staging step will
|
||||
// not be necessary.
|
||||
backbone: ["backbone.js", "backbone.js"],
|
||||
"bootstrap-tour": ["build/js/bootstrap-tour.js", "bootstrap-tour.js"],
|
||||
d3: ["d3.js", "d3.js"],
|
||||
"bibtex-parse-js": ["bibtexParse.js", "bibtexParse.js"],
|
||||
jquery: ["dist/jquery.js", "jquery/jquery.js"],
|
||||
"jquery.complexify": ["jquery.complexify.js", "jquery/jquery.complexify.js"],
|
||||
@@ -36,8 +36,8 @@ var paths = {
|
||||
libs: ["galaxy/scripts/libs/**/*.js"]
|
||||
};
|
||||
|
||||
gulp.task("stage-libs", function(callback) {
|
||||
_.each(_.keys(paths.lib_locs), function(lib) {
|
||||
function stageLibs(callback) {
|
||||
Object.keys(paths.lib_locs).forEach(lib => {
|
||||
var p1 = path.resolve(path.join(paths.node_modules, lib, paths.lib_locs[lib][0]));
|
||||
var p2 = path.resolve(path.join("galaxy", "scripts", "libs", paths.lib_locs[lib][1]));
|
||||
if (fs.existsSync(p1)) {
|
||||
@@ -50,44 +50,48 @@ gulp.task("stage-libs", function(callback) {
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return callback();
|
||||
}
|
||||
|
||||
gulp.task("fonts", function() {
|
||||
return gulp
|
||||
.src(path.resolve(path.join(paths.node_modules, "font-awesome/fonts/**/*")))
|
||||
.pipe(gulp.dest("../static/images/fonts"));
|
||||
});
|
||||
function fonts() {
|
||||
return src(path.resolve(path.join(paths.node_modules, "font-awesome/fonts/**/*"))).pipe(
|
||||
dest("../static/images/fonts")
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Remove script and lib tasks (for 19.05) once we are sure there are no
|
||||
// external accessors (via require or explicit inclusion in templates)
|
||||
gulp.task("scripts", function() {
|
||||
return gulp
|
||||
.src(paths.scripts)
|
||||
function scripts() {
|
||||
return src(paths.scripts)
|
||||
.pipe(
|
||||
babel({
|
||||
plugins: ["transform-es2015-modules-amd"]
|
||||
})
|
||||
)
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest("../static/scripts/"));
|
||||
});
|
||||
.pipe(dest("../static/scripts/"));
|
||||
}
|
||||
|
||||
gulp.task("libs", function() {
|
||||
return gulp
|
||||
.src(paths.libs)
|
||||
function libs() {
|
||||
return src(paths.libs)
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest("../static/scripts/libs/"));
|
||||
});
|
||||
.pipe(dest("../static/scripts/libs/"));
|
||||
}
|
||||
|
||||
gulp.task("plugins", function() {
|
||||
return gulp.src(paths.plugin_dirs).pipe(gulp.dest("../static/plugins/"));
|
||||
});
|
||||
function plugins() {
|
||||
return src(paths.plugin_dirs).pipe(dest("../static/plugins/"));
|
||||
}
|
||||
|
||||
gulp.task("clean", function() {
|
||||
function clean() {
|
||||
//Wipe out all scripts that aren't handled by webpack
|
||||
return del(["../static/scripts/**/*.js", "!../static/scripts/bundled/**.*.js"], { force: true });
|
||||
});
|
||||
}
|
||||
|
||||
gulp.task("staging", ["stage-libs", "fonts"]);
|
||||
|
||||
gulp.task("default", ["libs", "scripts"]);
|
||||
module.exports.fonts = fonts;
|
||||
module.exports.libs = libs;
|
||||
module.exports.scripts = scripts;
|
||||
module.exports.clean = clean;
|
||||
module.exports.stageLibs = stageLibs;
|
||||
module.exports.plugins = plugins;
|
||||
module.exports.staging = parallel(stageLibs, fonts, plugins);
|
||||
module.exports.default = series(libs, scripts);
|
||||
|
||||
@@ -1,41 +1,69 @@
|
||||
/**
|
||||
* Runs mocha tests
|
||||
*
|
||||
* Individual files can be run by passing in a comma-delimited list
|
||||
* of globs for the karma config like this:
|
||||
*
|
||||
* npm run test-watch watch-only="Tags.test.js,something.js,doodads.js"
|
||||
*/
|
||||
|
||||
const baseKarmaConfig = require("./karma.config.base");
|
||||
|
||||
const single_pack = (process.env.GALAXY_TEST_AS_SINGLE_PACK == "true");
|
||||
|
||||
const testBundles = [
|
||||
"**/unitTestBundle.js",
|
||||
"**/mocha/test.js"
|
||||
];
|
||||
|
||||
const separateTests = [
|
||||
// Complete list of unit tests
|
||||
const defaultFiles = [
|
||||
// component/module tests
|
||||
"**/*.test.js",
|
||||
// pre-existing rules definition tests
|
||||
"**/mocha/tests/*_tests.js"
|
||||
];
|
||||
|
||||
function getTestFiles() {
|
||||
// check for user-supplied list
|
||||
let userPatterns = getUserTestGlobs();
|
||||
let patterns = userPatterns.length ? userPatterns : defaultFiles;
|
||||
return patterns.map(pattern => ({ pattern, watched: true}));
|
||||
}
|
||||
|
||||
// command line arg "watch-only" can be a list of file globs
|
||||
// for karma to watch
|
||||
function getUserTestGlobs() {
|
||||
let userGlobs = process.argv.find(s => s.startsWith("watch-only"));
|
||||
return userGlobs ? processUserGlobs(userGlobs) : [];
|
||||
}
|
||||
|
||||
// split command line arg into an array
|
||||
function processUserGlobs(val) {
|
||||
let result = [];
|
||||
let fileListString = val.split("=")[1];
|
||||
if (fileListString) {
|
||||
result = fileListString.split(",")
|
||||
.map(s => s.trim())
|
||||
.map(checkGlobPrefix);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// prefixes user-supplied glob with directory wildcard
|
||||
function checkGlobPrefix(glob) {
|
||||
return glob.startsWith("**/") ? glob : `**/${glob}`;
|
||||
}
|
||||
|
||||
|
||||
module.exports = function (config) {
|
||||
|
||||
console.log("single_pack?", single_pack);
|
||||
|
||||
// pick all separate tests or the dynamic test-bundles
|
||||
let files = single_pack
|
||||
? testBundles
|
||||
: separateTests;
|
||||
|
||||
let preprocessors = files.reduce((result, path) => {
|
||||
result[path] = ["webpack"];
|
||||
return result;
|
||||
}, {});
|
||||
let files = [
|
||||
"../../node_modules/@babel/polyfill/dist/polyfill.js",
|
||||
...getTestFiles()
|
||||
];
|
||||
|
||||
let settings = Object.assign({}, baseKarmaConfig, {
|
||||
files: files,
|
||||
files,
|
||||
preprocessors: {
|
||||
"**/*.js": ["webpack"]
|
||||
},
|
||||
exclude: ["**/qunit/*"],
|
||||
preprocessors: preprocessors,
|
||||
reporters: ["mocha"],
|
||||
frameworks: ["polyfill", "mocha", "chai"]
|
||||
frameworks: ["mocha", "chai"]
|
||||
});
|
||||
|
||||
config.set(settings);
|
||||
|
||||
@@ -29,7 +29,8 @@ module.exports = function (config) {
|
||||
let settings = Object.assign({}, baseKarmaConfig, {
|
||||
files: testFiles.concat(assets),
|
||||
preprocessors: preprocessors,
|
||||
frameworks: ["polyfill", "qunit"]
|
||||
frameworks: ["polyfill", "qunit"],
|
||||
singleRun: true
|
||||
});
|
||||
|
||||
config.set(settings);
|
||||
|
||||
@@ -3,28 +3,45 @@
|
||||
* the ignore-loader for speedier testing.
|
||||
*/
|
||||
|
||||
let merge = require("webpack-merge");
|
||||
let wpConfig = require("../webpack.config");
|
||||
const merge = require("webpack-merge");
|
||||
const wpConfig = require("../webpack.config");
|
||||
|
||||
wpConfig.mode = "development";
|
||||
wpConfig.entry = () => ({});
|
||||
|
||||
// Don't need assets for unit testing, override those rules
|
||||
module.exports = merge.smart(wpConfig, {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?.*$|$)/,
|
||||
loader: "ignore-loader"
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: "ignore-loader"
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
loader: "ignore-loader"
|
||||
}
|
||||
]
|
||||
|
||||
// Don't need any assets for unit testing
|
||||
|
||||
let ignoreAssetLoaders = {
|
||||
rules:[
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?.*$|$)/,
|
||||
loader: "ignore-loader"
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: "ignore-loader"
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
loader: "ignore-loader"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
wpConfig.module = merge.smart(wpConfig.module, ignoreAssetLoaders);
|
||||
|
||||
|
||||
// Using babel-plugin-rewire to handle dependency mocking since webpack 4
|
||||
// exports immutable bindings for ES modules but we still need a way to
|
||||
// overwrite dependencies during unit-testing.
|
||||
|
||||
wpConfig.module.rules = wpConfig.module.rules.map(rule => {
|
||||
if (rule.loader == "babel-loader") {
|
||||
rule.options.plugins.push("rewire");
|
||||
}
|
||||
return rule;
|
||||
});
|
||||
|
||||
|
||||
module.exports = wpConfig;
|
||||
|
||||
+16
-11
@@ -16,6 +16,8 @@
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@handsontable/vue": "^2.0.0-beta1",
|
||||
"@johmun/vue-tags-input": "^2.0.0",
|
||||
"@vue/test-utils": "^1.0.0-beta.28",
|
||||
"amdi18n-loader": "^0.8.0",
|
||||
"axios": "^0.18.0",
|
||||
"backbone": "1.3",
|
||||
@@ -26,8 +28,6 @@
|
||||
"d3": "3",
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-uglify": "^3.0.1",
|
||||
"handsontable": "^2.0.0",
|
||||
"imports-loader": "^0.8.0",
|
||||
"jquery": "2",
|
||||
@@ -44,9 +44,12 @@
|
||||
"raven-js": "^3.27.0",
|
||||
"requirejs": "2.3.6",
|
||||
"rxjs": "^6.3.3",
|
||||
"slugify": "^1.3.4",
|
||||
"underscore": "^1.9.1",
|
||||
"vue": "2.5.17",
|
||||
"vue-router": "3.0.1"
|
||||
"vue": "^2.5.22",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue-rx": "^6.1.0",
|
||||
"vuex": "^3.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "gulp staging && gulp clean && gulp && yarn run save-build-hash && yarn run webpack-watch",
|
||||
@@ -69,13 +72,14 @@
|
||||
"styleguide:build": "vue-styleguidist build",
|
||||
"test": "npm run test-mocha && npm run test-qunit",
|
||||
"test-watch": "npm run test-mocha -- --no-single-run",
|
||||
"test-qunit": "GALAXY_TEST_AS_SINGLE_PACK=true karma start karma/karma.config.qunit.js",
|
||||
"test-mocha": "GALAXY_TEST_AS_SINGLE_PACK=true karma start karma/karma.config.mocha.js",
|
||||
"test-qunit": "karma start karma/karma.config.qunit.js",
|
||||
"test-mocha": "karma start karma/karma.config.mocha.js",
|
||||
"jshint": "jshint --exclude='galaxy/scripts/libs/**' galaxy/scripts/**/*.js",
|
||||
"eslint": "eslint -c .eslintrc.js galaxy/scripts --ext .js,.vue"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-loader": "^8.0.4",
|
||||
"babel-plugin-rewire": "^1.2.0",
|
||||
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
|
||||
"babel-plugin-transform-vue-template": "^0.4.2",
|
||||
"chai": "^4.2.0",
|
||||
@@ -92,8 +96,9 @@
|
||||
"eslint-plugin-vue": "^5.1.0",
|
||||
"expose-loader": "^0.7.5",
|
||||
"file-loader": "^2.0.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-uglify-es": "^1.0.4",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"jshint": "^2.9.6",
|
||||
"karma": "^1.7.1",
|
||||
@@ -112,14 +117,14 @@
|
||||
"phantomjs-prebuilt": "^2.1.7",
|
||||
"prettier": "^1.15.3",
|
||||
"qunitjs": "^2.4.1",
|
||||
"raw-loader": "^1.0.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sinon": "^4.1.2",
|
||||
"store": "^2.0.12",
|
||||
"style-loader": "^0.23.1",
|
||||
"vue-loader": "^15.4.2",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-loader": "^15.6.2",
|
||||
"vue-styleguidist": "^1.8.9",
|
||||
"vue-template-compiler": "2.5.17",
|
||||
"vue-template-compiler": "^2.5.22",
|
||||
"webpack": "^4.23.0",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-merge": "^4.1.4",
|
||||
|
||||
@@ -29,7 +29,7 @@ let buildconfig = {
|
||||
alias: {
|
||||
jquery$: `${libsBase}/jquery.custom.js`,
|
||||
jqueryVendor$: `${libsBase}/jquery/jquery.js`,
|
||||
store$: "store/dist/store.modern.js"
|
||||
storemodern$: "store/dist/store.modern.js"
|
||||
}
|
||||
},
|
||||
optimization: {
|
||||
@@ -64,7 +64,22 @@ let buildconfig = {
|
||||
libsBase
|
||||
],
|
||||
loader: "babel-loader",
|
||||
options: { babelrc: true }
|
||||
options: {
|
||||
cacheDirectory: true,
|
||||
cacheCompression: false,
|
||||
presets: [
|
||||
["@babel/preset-env", { modules: false }]
|
||||
],
|
||||
plugins: [
|
||||
"transform-vue-template",
|
||||
"@babel/plugin-syntax-dynamic-import"
|
||||
],
|
||||
ignore: [
|
||||
"i18n.js",
|
||||
"utils/localization.js",
|
||||
"nls/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
test: `${libsBase}/jquery.custom.js`,
|
||||
@@ -98,7 +113,7 @@ let buildconfig = {
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
outputPath: "assets",
|
||||
publicPath: "/static/scripts/bundled/assets/"
|
||||
publicPath: "../scripts/bundled/assets/"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -144,12 +159,15 @@ let buildconfig = {
|
||||
},
|
||||
{
|
||||
loader: "sass-loader",
|
||||
options: { sourceMap: true }
|
||||
options: {
|
||||
sourceMap: true,
|
||||
includePaths: ["galaxy/style/scss"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.tmpl$/,
|
||||
test: /\.(txt|tmpl)$/,
|
||||
loader: "raw-loader"
|
||||
}
|
||||
]
|
||||
|
||||
+689
-598
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,19 @@
|
||||
<registration converters_path="lib/galaxy/datatypes/converters" display_path="display_applications">
|
||||
<datatype extension="ab1" type="galaxy.datatypes.binary:Ab1" mimetype="application/octet-stream" display_in_upload="true" description="A binary sequence file in 'ab1' format with a '.ab1' file extension. You must manually select this 'File Format' when uploading the file." description_url="https://wiki.galaxyproject.org/Learn/Datatypes#Ab1"/>
|
||||
<datatype extension="afg" type="galaxy.datatypes.assembly:Amos" display_in_upload="false"/>
|
||||
<datatype extension="anvio_cog_profile" type="galaxy.datatypes.anvio:AnvioComposite" display_in_upload="false" subclass="true" />
|
||||
<datatype extension="anvio_composite" type="galaxy.datatypes.anvio:AnvioComposite" display_in_upload="false" />
|
||||
<datatype extension="anvio_classifier" type="galaxy.datatypes.data:Data" display_in_upload="false" subclass="true" />
|
||||
<datatype extension="anvio_contigs_db" type="galaxy.datatypes.anvio:AnvioContigsDB" display_in_upload="false" />
|
||||
<datatype extension="anvio_db" type="galaxy.datatypes.anvio:AnvioDB" display_in_upload="false" />
|
||||
<datatype extension="anvio_genomes_db" type="galaxy.datatypes.anvio:AnvioGenomesDB" display_in_upload="false" />
|
||||
<datatype extension="anvio_pan_db" type="galaxy.datatypes.anvio:AnvioPanDB" display_in_upload="false" />
|
||||
<datatype extension="anvio_pfam_profile" type="galaxy.datatypes.anvio:AnvioComposite" display_in_upload="false" subclass="true" />
|
||||
<datatype extension="anvio_profile_db" type="galaxy.datatypes.anvio:AnvioProfileDB" display_in_upload="false" />
|
||||
<datatype extension="anvio_samples_db" type="galaxy.datatypes.anvio:AnvioSamplesDB" display_in_upload="false" />
|
||||
<datatype extension="anvio_state" type="galaxy.datatypes.text:Json" mimetype="application/json" subclass="true" display_in_upload="false" />
|
||||
<datatype extension="anvio_structure_db" type="galaxy.datatypes.anvio:AnvioStructureDB" display_in_upload="false" />
|
||||
<datatype extension="anvio_variability" type="galaxy.datatypes.tabular:TSV" display_in_upload="false" subclass="true" />
|
||||
<datatype extension="arff" type="galaxy.datatypes.text:Arff" mimetype="text/plain" display_in_upload="true"/>
|
||||
<datatype extension="asn1" type="galaxy.datatypes.data:GenericAsn1" mimetype="text/plain" display_in_upload="true"/>
|
||||
<datatype extension="asn1-binary" type="galaxy.datatypes.binary:GenericAsn1Binary" mimetype="application/octet-stream" display_in_upload="true"/>
|
||||
@@ -402,6 +415,7 @@
|
||||
<!-- End RGenetics Datatypes -->
|
||||
<datatype extension="ipynb" type="galaxy.datatypes.text:Ipynb" display_in_upload="true"/>
|
||||
<datatype extension="json" type="galaxy.datatypes.text:Json" display_in_upload="true"/>
|
||||
<datatype extension="expression.json" type="galaxy.datatypes.text:ExpressionJson" display_in_upload="true"/>
|
||||
<!-- graph datatypes -->
|
||||
<datatype extension="xgmml" type="galaxy.datatypes.graph:Xgmml" display_in_upload="true"/>
|
||||
<datatype extension="sif" type="galaxy.datatypes.graph:Sif" display_in_upload="true"/>
|
||||
@@ -523,6 +537,8 @@
|
||||
<datatype extension="stockholm" type="galaxy.datatypes.msa:Stockholm_1_0" display_in_upload="true"/>
|
||||
<datatype extension="xmfa" type="galaxy.datatypes.msa:MauveXmfa" display_in_upload="true"/>
|
||||
<datatype extension="cel" type="galaxy.datatypes.binary:Cel" display_in_upload="true"/>
|
||||
<datatype extension="gpr" type="galaxy.datatypes.microarrays:Gpr" display_in_upload="true"/>
|
||||
<datatype extension="gal" type="galaxy.datatypes.microarrays:Gal" display_in_upload="true"/>
|
||||
<datatype extension="rdata" type="galaxy.datatypes.binary:RData" display_in_upload="true" description="Stored data from an R session"/>
|
||||
<datatype extension="rdata.sce" type="galaxy.datatypes.binary:RData" description="Stored RDS from a SingleCellObject" subclass="true" display_in_upload="true"/>
|
||||
<datatype extension="oxlicg" type="galaxy.datatypes.binary:OxliCountGraph" mimetype="application/octet-stream" display_in_upload="true"/>
|
||||
@@ -855,6 +871,8 @@
|
||||
<sniffer type="galaxy.datatypes.msa:InfernalCM"/>
|
||||
<sniffer type="galaxy.datatypes.annotation:SnapHmm"/>
|
||||
<sniffer type="galaxy.datatypes.binary:Cel"/>
|
||||
<sniffer type="galaxy.datatypes.microarrays:Gpr"/>
|
||||
<sniffer type="galaxy.datatypes.microarrays:Gal"/>
|
||||
<sniffer type="galaxy.datatypes.binary:RData"/>
|
||||
<sniffer type="galaxy.datatypes.images:Jpg"/>
|
||||
<sniffer type="galaxy.datatypes.images:Png"/>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# This is an example mapping file for the module (http://modules.sourceforge.net/) Dependency resolver (in YAML format)
|
||||
#
|
||||
# The goal of this file is to map tool's requirements to existing modules available on your system
|
||||
# Of course, if the name of a requirement and the name of a module match perfectly, there is no need to map them together through this mapping file.
|
||||
#
|
||||
# This is a sample file so the first thing to do to activate the mapping system is to create a copy of this file called "environment_modules_mapping.yml".
|
||||
# The module dependency resolver is programmed to search and use this YAML file automatically if it exists in the "config" folder of your Galaxy instance.
|
||||
# Alternatively, you can also use the "mapping_files" attribute of the <modules /> resolver in the dependency_resolvers_conf.xml file to specify a custom mapping file
|
||||
#
|
||||
# Example 1:
|
||||
#
|
||||
# Let's say that one of the wrapper installed on your Galaxy instance has the following requirement:
|
||||
#
|
||||
# <requirements>
|
||||
# <requirement type="package" version="1.5.0">PIPITS</requirement>
|
||||
# </requirements>
|
||||
#
|
||||
# But unfortunately, the name of the corresponding module file on your system is "pipits_pipeline/1.5.0"
|
||||
#
|
||||
# Then, to make Galaxy load/unload the appropriate module, you just have to add the following lines (without the #) to the "environment_modules_mapping.yml" file:
|
||||
#
|
||||
#- from:
|
||||
# name: PIPITS
|
||||
# version: 1.5.0
|
||||
# to:
|
||||
# name: pipits_pipeline
|
||||
# version: 1.5.0.6
|
||||
#
|
||||
#
|
||||
# Example 2:
|
||||
#
|
||||
# The requirements section specify a requirement on the PIPITS tool but do not ask for a specific version of it:
|
||||
#
|
||||
# <requirements>
|
||||
# <requirement type="package">PIPITS</requirement>
|
||||
# </requirements>
|
||||
#
|
||||
# Although, there is no version required you may want to force the loading of a version that is known to run well on your system.
|
||||
#
|
||||
# In that case you can add the following lines to the "environment_modules_mapping.yml" file:
|
||||
#
|
||||
#- from:
|
||||
# name: PIPITS
|
||||
# unversioned: true
|
||||
# to:
|
||||
# name: pipits_pipeline
|
||||
# version: 1.4.0
|
||||
@@ -34,14 +34,6 @@
|
||||
- type: sentry
|
||||
user_submission: false
|
||||
|
||||
# Allow users to submit error reports to biostars. This requires that the
|
||||
# biostars integration is configured. This *only* makes sense when
|
||||
# user_submission is true, as it only generates the link for the user to click
|
||||
# on and submit the bug report, it does not actually submit the bug report on
|
||||
# their behalf.
|
||||
# - type: biostars
|
||||
# user_submission: true
|
||||
|
||||
# InfluxDB error reporting backend. You will need to `pip install
|
||||
# influxdb` in the galaxy virtualenv yourself. This sends well tagged
|
||||
# errors InfluxDB allowing you to notice relationships between tool errors and
|
||||
@@ -58,9 +50,34 @@
|
||||
# comment on existing, open issues. The issues are labelled based on tool ID /
|
||||
# version and include all of the information the normal emailed bug reports
|
||||
# include. If you use a private Github Enterprise deployment, you can set
|
||||
# github_base_url='https://...'
|
||||
# github_base_url='https://...' and github_api_url='https://api.....' as shown below.
|
||||
# The 'github_default_repo_only' flag restores the previous behaviour. When this is set
|
||||
# to true it will automatically only submit to the default git repository.
|
||||
# - type: github
|
||||
# verbose: false
|
||||
# user_submission: true
|
||||
# github_oauth_token: 00000000000
|
||||
# github_repo_owner: galaxyproject
|
||||
# github_repo_name: galaxy
|
||||
# github_base_url: https://github.com
|
||||
# github_api_url: https://api.github.com
|
||||
# github_default_repo_owner: galaxyproject
|
||||
# github_default_repo_name: galaxy
|
||||
# github_default_repo_only: true
|
||||
|
||||
# GitLab error reporting backend. You will need to `pip install python-gitlab`
|
||||
# in the galaxy virtualenv. This will create a new issue if none exists, and
|
||||
# comment on existing, open issues. The issues are labelled based on tool ID /
|
||||
# version and include all of the information the normal emailed bug reports
|
||||
# include. If you use a private GitLab deployment, you can set
|
||||
# gitlab_base_url='https://...'. It supports creating an issue on the git
|
||||
# repository of the tool by querying the ToolShed where the tool comes from
|
||||
# (if applicable). Set verbose to true if you want the message to be displayed
|
||||
# to the user. The 'gitlab_default_repo_only' flags ensures all errors are
|
||||
# submitted to the default repository only.
|
||||
# - type: gitlab
|
||||
# verbose: false
|
||||
# user_submission: true
|
||||
# gitlab_base_url: https://gitlab.com
|
||||
# gitlab_private_token: 00000000000
|
||||
# gitlab_default_repo_owner: galaxyproject
|
||||
# gitlab_default_repo_name: galaxy
|
||||
# gitlab_default_repo_only: true
|
||||
+22
-40
@@ -66,6 +66,14 @@ uwsgi:
|
||||
# Reports, etc.) that you are loading.
|
||||
module: galaxy.webapps.galaxy.buildapp:uwsgi_app()
|
||||
|
||||
# Mount the web application (e.g. Galaxy, Reports, etc.) at the given
|
||||
# URL prefix. Cannot be used together with 'module:' above.
|
||||
#mount: /galaxy=galaxy.webapps.galaxy.buildapp:uwsgi_app()
|
||||
|
||||
# Make uWSGI rewrite PATH_INFO and SCRIPT_NAME according to mount-
|
||||
# points. Set this to true if a URL prefix is used.
|
||||
manage-script-name: false
|
||||
|
||||
# It is usually a good idea to set this to ``true`` if processes is
|
||||
# greater than 1.
|
||||
thunder-lock: false
|
||||
@@ -90,16 +98,11 @@ uwsgi:
|
||||
|
||||
galaxy:
|
||||
|
||||
# If running behind a proxy server and Galaxy is served from a
|
||||
# subdirectory, enable the proxy-prefix filter and set the prefix in
|
||||
# the [filter:proxy-prefix] section above.
|
||||
#filter-with: proxy-prefix
|
||||
|
||||
# If proxy-prefix is enabled and you're running more than one Galaxy
|
||||
# instance behind one hostname, you will want to set this to the same
|
||||
# path as the prefix in the filter above. This value becomes the
|
||||
# "path" attribute set in the cookie so the cookies from each instance
|
||||
# will not clobber each other.
|
||||
# When running multiple Galaxy instances under separate URL prefixes
|
||||
# on a single hostname, you will want to set this to the same path as
|
||||
# the prefix set in the uWSGI "mount" configuration option above. This
|
||||
# value becomes the "path" attribute set in the cookie so the cookies
|
||||
# from one instance will not clobber those from another.
|
||||
#cookie_path: ''
|
||||
|
||||
# By default, Galaxy uses a SQLite database at
|
||||
@@ -167,10 +170,13 @@ galaxy:
|
||||
# not recommended for production use.
|
||||
#database_auto_migrate: false
|
||||
|
||||
# Dataset files are stored in this directory.
|
||||
# Where dataset files are stored. It must accessible at the same path
|
||||
# on any cluster nodes that will run Galaxy jobs, unless using Pulsar.
|
||||
#file_path: database/files
|
||||
|
||||
# Temporary files are stored in this directory.
|
||||
# Where temporary files are stored. It must accessible at the same
|
||||
# path on any cluster nodes that will run Galaxy jobs, unless using
|
||||
# Pulsar.
|
||||
#new_file_path: database/tmp
|
||||
|
||||
# Tool config files, defines what tools are available in Galaxy. Tools
|
||||
@@ -668,6 +674,10 @@ galaxy:
|
||||
# format string as specified by ISO 8601 international standard).
|
||||
#pretty_datetime_format: $locale (UTC)
|
||||
|
||||
# Location of the configuration file containing extra user
|
||||
# preferences.
|
||||
#user_preferences_extra_conf_path: config/user_preferences_extra_conf.yml
|
||||
|
||||
# Default localization for Galaxy UI. Allowed values are listed at the
|
||||
# end of client/galaxy/scripts/nls/locale.js. With the default value
|
||||
# (auto), the locale will be automatically adjusted to the user's
|
||||
@@ -711,21 +721,6 @@ galaxy:
|
||||
# The URL linked by the "Support" link in the "Help" menu.
|
||||
#support_url: 'https://galaxyproject.org/support/'
|
||||
|
||||
# Enable integration with a custom Biostar instance.
|
||||
#biostar_url: ''
|
||||
|
||||
# Enable integration with a custom Biostar instance.
|
||||
#biostar_key_name: ''
|
||||
|
||||
# Enable integration with a custom Biostar instance.
|
||||
#biostar_key: ''
|
||||
|
||||
# Enable integration with a custom Biostar instance.
|
||||
#biostar_enable_bug_reports: true
|
||||
|
||||
# Enable integration with a custom Biostar instance.
|
||||
#biostar_never_authenticate: false
|
||||
|
||||
# The URL linked by the "How to Cite Galaxy" link in the "Help" menu.
|
||||
#citation_url: 'https://galaxyproject.org/citing-galaxy'
|
||||
|
||||
@@ -1351,19 +1346,6 @@ galaxy:
|
||||
# Enable the new container interface for Interactive Environments
|
||||
#enable_beta_containers_interface: false
|
||||
|
||||
# Set the following to a number of threads greater than 1 to spawn a
|
||||
# Python task queue for dealing with large tool submissions (either
|
||||
# through the tool form or as part of an individual workflow step
|
||||
# across large collection). This affects workflow scheduling and web
|
||||
# processes, not job handlers. This is a beta option and should not be
|
||||
# used in production.
|
||||
#tool_submission_burst_threads: 1
|
||||
|
||||
# If tool_submission_burst_threads is set to a number greater than 1,
|
||||
# this is the number of jobs to schedule at which the task queue will
|
||||
# be created.
|
||||
#tool_submission_burst_at: 10
|
||||
|
||||
# Enable beta workflow modules that should not yet be considered part
|
||||
# of Galaxy's stable API.
|
||||
#enable_beta_workflow_modules: false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user