Merge branch 'dev' of github.com:galaxyproject/galaxy into cellxgene-gie

This commit is contained in:
James Taylor
2019-03-22 17:09:19 -04:00
385 changed files with 13734 additions and 6524 deletions
+212
View File
@@ -0,0 +1,212 @@
# Python CircleCI 2.0 configuration file
version: 2
variables:
restore_repo_cache: &restore_repo_cache
restore_cache:
keys:
- v1-repo-{{ .Environment.CIRCLE_SHA1 }}
restore_yarn_cache: &restore_yarn_cache
restore_cache:
keys:
- v1-repo-{{ .Environment.CIRCLE_SHA1 }}
- yarn-packages-{{ checksum "client/yarn.lock" }}
save_yarn_cache: &save_yarn_cache
save_cache:
key: yarn-packages-{{ checksum "client/yarn.lock" }}
paths:
- ~/.cache/yarn
install_tox: &install_tox
run: sudo pip install tox
set_workdir: &set_workdir
working_directory: ~/repo
requires_get_code: &requires_get_code
requires:
- get_code
jobs:
get_code:
docker:
- image: circleci/python:2.7.15
<<: *set_workdir
steps:
# Replace standard code checkout with shallow clone to speed things up.
- run:
name: Checkout code
command: |-
# Add github.com to known hosts
mkdir -p ~/.ssh
echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
' >> ~/.ssh/known_hosts
# Add the user ssh key and set correct perms
(umask 077; touch ~/.ssh/id_rsa)
chmod 0600 ~/.ssh/id_rsa
echo "$CHECKOUT_KEY" > ~/.ssh/id_rsa
# Use git+ssh instead of https
git config --global url."ssh://git@github.com".insteadOf "https://github.com" || true
git config --global gc.auto 0 || true
# Shallow clone
git clone --depth=1 "${CIRCLE_REPOSITORY_URL}" .
if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then
# Update PR refs for testing.
FETCH_REFS="${FETCH_REFS} +refs/pull/${CIRCLE_PR_NUMBER}/head:pr/${CIRCLE_PR_NUMBER}/head"
FETCH_REFS="${FETCH_REFS} +refs/pull/${CIRCLE_PR_NUMBER}/merge:pr/${CIRCLE_PR_NUMBER}/merge"
# Retrieve the refs
git fetch --force origin ${FETCH_REFS}
# Checkout PR merge ref.
git checkout -f "pr/${CIRCLE_PR_NUMBER}/merge"
# Test for *some* merge conflicts.
git branch --merged | grep "pr/${CIRCLE_PR_NUMBER}/head" > /dev/null
else
if [ -n "$CIRCLE_TAG" ]; then
git fetch --depth=1 --force origin "refs/tags/${CIRCLE_TAG}"
else
git fetch --depth=1 --force origin "$CIRCLE_BRANCH:remotes/origin/$CIRCLE_BRANCH"
fi
if [ -n "$CIRCLE_TAG" ]; then
git reset --hard "$CIRCLE_SHA1"
git checkout "$CIRCLE_TAG"
elif [ -n "$CIRCLE_BRANCH" ]; then
git reset --hard "$CIRCLE_SHA1"
git checkout -B "$CIRCLE_BRANCH"
fi
git reset --hard "${CIRCLE_SHA1}"
fi
- save_cache:
key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
paths:
- ~/repo
py27_lint:
docker:
- image: circleci/python:2.7.15
<<: *set_workdir
steps:
- *restore_repo_cache
- *install_tox
- run: tox -e py27-lint
py27_unit:
docker:
- image: circleci/python:2.7.15
<<: *set_workdir
steps:
- *restore_repo_cache
- *install_tox
- run: tox -e py27-unit
py27_docstring:
docker:
- image: circleci/python:2.7.15
<<: *set_workdir
steps:
- *restore_repo_cache
- *install_tox
- run: tox -e py27-lint_docstring_include_list
py27_first_startup:
docker:
- image: circleci/python:2.7.15
<<: *set_workdir
steps:
- *restore_repo_cache
- run: sh scripts/common_startup.sh
- run: wget -q https://github.com/jmchilton/galaxy-downloads/raw/master/db_gx_rev_0127.sqlite
- run: mv db_gx_rev_0127.sqlite database/universe.sqlite
- run: sh manage_db.sh -c ./config/galaxy.yml.sample upgrade
- *install_tox
- run: tox -e py27-first_startup
py35_lint:
docker:
- image: circleci/python:3.5
<<: *set_workdir
steps:
- *restore_repo_cache
- *install_tox
- run: tox -e py35-lint
py35_unit:
docker:
- image: circleci/python:3.5
<<: *set_workdir
steps:
- *restore_repo_cache
- *install_tox
- run: tox -e py35-unit
py35_first_startup:
docker:
- image: circleci/python:3.5
<<: *set_workdir
steps:
- *restore_repo_cache
- *install_tox
- run: sudo apt-get update
# For uwsgi
- run: sudo apt-get install -y libpython3.5-dev
- run: tox -e py35-first_startup
validate_test_tools:
docker:
- image: circleci/python:2.7.15
<<: *set_workdir
steps:
- *restore_repo_cache
- run: sudo apt-get update
- run: sudo apt-get install -y libxml2-utils
- *install_tox
- run: tox -e validate_test_tools
check_py3_compatibility:
docker:
- image: circleci/python:2.7.15
<<: *set_workdir
steps:
- *restore_repo_cache
- run: sudo apt-get update
- run: sudo apt-get install -y ack-grep
- *install_tox
- run: tox -e check_py3_compatibility
js_unit:
docker:
- image: circleci/node:10-browsers
<<: *set_workdir
steps:
- *restore_yarn_cache
- run: cd client && yarn install --frozen-lockfile
- *save_yarn_cache
- run: cd client && yarn run build
- run: cd client && yarn run test
js_lint:
docker:
- image: circleci/node:10-browsers
<<: *set_workdir
steps:
- *restore_yarn_cache
- run: cd client && yarn install --frozen-lockfile
- *save_yarn_cache
- run: cd client && yarn run eslint
workflows:
version: 2
get_code_and_test:
jobs:
- get_code
- py27_lint:
<<: *requires_get_code
- py27_unit:
<<: *requires_get_code
- py27_first_startup:
<<: *requires_get_code
- py27_docstring:
<<: *requires_get_code
- py35_lint:
<<: *requires_get_code
- py35_unit:
<<: *requires_get_code
- py35_first_startup:
<<: *requires_get_code
- validate_test_tools:
<<: *requires_get_code
- check_py3_compatibility:
<<: *requires_get_code
- js_unit:
<<: *requires_get_code
- js_lint:
<<: *requires_get_code
-57
View File
@@ -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
+1
View File
@@ -49,6 +49,7 @@ The following individuals have contributed code to Galaxy:
* Gianmauro Cuccuru <gmauro@crs4.it>
* Frederik Delaere <frederik.delaere@gmail.com>
* Matthias Desmet <matthias.desmet@ugent.be>
* Matthew Ryan Dillon <matthewrdillon@gmail.com>
* Olivia Doppelt <olivia.doppelt@pasteur.fr>
* Shane Dowling <shane@shanedowling.com>
* John Duddy <jduddy@illumina.com>
+6 -6
View File
@@ -1,15 +1,15 @@
# Location of virtualenv used for development.
VENV?=.venv
# Source virtualenv to execute command (flake8, sphinx, twine, etc...)
IN_VENV=if [ -f $(VENV)/bin/activate ]; then . $(VENV)/bin/activate; fi;
RELEASE_CURR:=16.01
RELEASE_CURR_MINOR_NEXT:=$(shell python scripts/bootstrap_history.py --print-next-minor-version)
RELEASE_CURR_MINOR_NEXT:=$(shell $(IN_VENV) python scripts/bootstrap_history.py --print-next-minor-version)
RELEASE_NEXT:=16.04
# TODO: This needs to be updated with create_release_rc
#RELEASE_NEXT_BRANCH:=release_$(RELEASE_NEXT)
RELEASE_NEXT_BRANCH:=dev
RELEASE_UPSTREAM:=upstream
MY_UPSTREAM:=origin
# Location of virtualenv used for development.
VENV?=.venv
# Source virtualenv to execute command (flake8, sphinx, twine, etc...)
IN_VENV=if [ -f $(VENV)/bin/activate ]; then . $(VENV)/bin/activate; fi;
CONFIG_MANAGE=$(IN_VENV) python lib/galaxy/webapps/config_manage.py
PROJECT_URL?=https://github.com/galaxyproject/galaxy
DOCS_DIR=doc
@@ -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
View File
@@ -1,11 +1,9 @@
.. figure:: https://galaxyproject.org/images/galaxy-logos/galaxy_project_logo.jpg
:alt: Galaxy Logo
The latest information about Galaxy is available via `https://galaxyproject.org/ <https://galaxyproject.org/>`__
The latest information about Galaxy can be found on the `Galaxy Community Hub <https://galaxyproject.org/>`__.
.. image:: https://img.shields.io/badge/questions-galaxy%20biostar-blue.svg
:target: https://biostar.usegalaxy.org
:alt: Ask a question
Community support is available at `Galaxy Help <https://help.galaxyproject.org/>`__.
.. image:: https://img.shields.io/badge/chat-irc.freenode.net%23galaxyproject-blue.svg
:target: https://webchat.freenode.net/?channels=galaxyproject
-12
View File
@@ -1,12 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
],
"plugins": ["transform-vue-template", "@babel/plugin-syntax-dynamic-import"],
"ignore": ["i18n.js", "utils/localization.js", "nls/*"]
}
+11 -13
View File
@@ -2,32 +2,30 @@ Client Build System
===================
Installs, stages, and builds the client-side scripts necessary for running the
Galaxy webapp. There's no need to use this system unless you are modifying or
developing client-side scripts, or are running the development branch of
Galaxy. When started through `run.sh` or any other method that utilizes
`common_startup.sh`, Galaxy will also (since 18.09) *automatically* build
Galaxy webapp. When started through `run.sh` or any other method that utilizes
`scripts/common_startup.sh`, Galaxy will (since 18.09) *automatically* build
the client as a part of server startup, when it detects changes, unless that
functionality is explicitly disabled.
The base dependencies used are Node.js and Yarn. Galaxy now includes these in
the virtual environment, and they can be accessed by activating that with
`source .venv/bin/activate` from the Galaxy root directory.
`. .venv/bin/activate` from the Galaxy root directory.
If you'd like to install your own dependencies, on OSX the easiest way to get
set up is using homebrew and the command `brew install nodejs yarn`. More
information including instructions for other platforms is available at
nodejs.org and yarnpkg.com.
set up is using `homebrew` and the command `brew install nodejs yarn`. More
information, including instructions for other platforms, is available at
https://nodejs.org/ and https://yarnpkg.com/ .
The Galaxy client build has necessarily grown more complex in the past several
years, but we're still trying to keep things as simple as possible for
developers (everyone, really). If you're having any trouble with building the
client after following the instructions below please create an issue on GitHub
years, but we are still trying to keep things as simple as possible for
everyone. If you're having any trouble with building the
client after following the instructions below, please create an issue on GitHub
or reach out for help directly on Gitter at
https://gitter.im/galaxyproject/Lobby.
https://gitter.im/galaxyproject/Lobby .
Complete Client Build
================================================
=====================
There are many moving parts to the client build system, but the entry point for
most people is the 'client' rule in the Makefile at the root of the Galaxy
+2 -2
View File
@@ -96,7 +96,7 @@ export let chartUtilities = {
export { initMasthead } from "components/Masthead/initMasthead";
export { panelManagement } from "onload/globalInits/panelManagement";
export { init_tag_click_function } from "ui/autocom_tagging";
export { mountMakoTags } from "components/Tags";
// Used in common.mako
export { default as store } from "store";
export { default as store } from "storemodern";
+144 -36
View File
@@ -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>' +
+3 -4
View File
@@ -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);
}
+12 -24
View File
@@ -1,6 +1,7 @@
/** Masthead Collection **/
import _ from "underscore";
import $ from "jquery";
import axios from "axios";
import Backbone from "backbone";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
@@ -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);
}
+12 -3
View File
@@ -231,9 +231,18 @@ var CenterPanel = Backbone.View.extend({
/** Display a view in the center panel, hide iframe */
display: function(view) {
var contentWindow = this.$frame[0].contentWindow || {};
var message = contentWindow.onbeforeunload && contentWindow.onbeforeunload();
var Galaxy = getGalaxyInstance();
let Galaxy = getGalaxyInstance();
let contentWindow = this.$frame[0].contentWindow || {};
let message;
try {
message = contentWindow.onbeforeunload && contentWindow.onbeforeunload();
} catch (err) {
// This can happen when external content is displayed in this iframe // CORS violation
contentWindow = {};
console.warn(
"Iframe unload exception. This can happen when external content is displayed in the page iframe and causes a CORS violation -- likely harmless."
);
}
if (!message || confirm(message)) {
contentWindow.onbeforeunload = undefined;
this.$frame.attr("src", "about:blank").hide();
+12 -1
View File
@@ -1,5 +1,6 @@
import $ from "jquery";
import Backbone from "backbone";
import { getAppRoot } from "onload/loadConfig";
import { getGalaxyInstance } from "app";
import QUERY_STRING from "utils/query-string-parsing";
import Ui from "mvc/ui/ui-misc";
@@ -12,6 +13,14 @@ var Router = Backbone.Router.extend({
this.options = options;
},
executeUseRouter: function(url) {
let prefix = getAppRoot();
if (url.startsWith(prefix)) {
url = url.replace(prefix, "/");
}
return this.push(url);
},
/** helper to push a new navigation state */
push: function(url, data) {
data = data || {};
@@ -19,10 +28,12 @@ var Router = Backbone.Router.extend({
.toString(36)
.substr(2);
url += url.indexOf("?") == -1 ? "?" : "&";
url += $.param(data, true);
let bustParam = $.param(data, true);
url += bustParam;
let Galaxy = getGalaxyInstance();
Galaxy.params = data;
this.navigate(url, { trigger: true });
window.history.replaceState(window.history.state, "", window.location.pathname.replace(bustParam, ""));
},
/** override to parse query string into obj and send to each route */
@@ -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 */
+20 -18
View File
@@ -7,6 +7,7 @@ import STATES from "mvc/dataset/states";
import faIconButton from "ui/fa-icon-button";
import BASE_MVC from "mvc/base-mvc";
import _l from "utils/localization";
import { mountNametags } from "components/Nametags";
var logNamespace = "dataset";
/*==============================================================================
@@ -42,6 +43,20 @@ export var DatasetListItemView = _super.extend(
this.linkTarget = attributes.linkTarget || "_blank";
},
// mount new vue component for tags
render: function() {
let result = _super.prototype.render.apply(this, arguments);
this._mountVueNametags();
return result;
},
_mountVueNametags: function() {
let container = this.$(".nametags")[0];
let { id, model_class, tags } = this.model.attributes;
let storeKey = `${model_class}-${id}`;
mountNametags({ storeKey, tags }, container);
},
/** event listeners */
_setUpListeners: function() {
_super.prototype._setUpListeners.call(this);
@@ -64,11 +79,11 @@ export var DatasetListItemView = _super.extend(
self.render();
});
} else {
if (_.has(model.changed, "tags") && _.keys(model.changed).length === 1) {
// If only the tags have changed, rerender specifically
// the titlebar region. Otherwise default to the full
// render.
self.$(".nametags").html(self._renderNametags());
if (_.has(self.model.changed, "tags") && _.keys(self.model.changed).length === 2) {
// If only the tags and update time have changed,
// rerender specifically the titlebar region.
// Otherwise default to the full render.
self._mountVueNametags();
} else {
self.render();
}
@@ -327,19 +342,6 @@ export var DatasetListItemView = _super.extend(
</div>`);
},
_renderNametags: function() {
var tpl = _.template(
[
"<% _.each(_.sortBy(_.uniq(tags), function(x) { return x }), function(tag){ %>",
'<% if (tag.indexOf("name:") == 0){ %>',
'<span class="badge badge-primary badge-tags"><%- tag.slice(5) %></span>',
"<% } %>",
"<% }); %>"
].join("")
);
return tpl({ tags: this.model.get("tags") });
},
// ......................................................................... misc
events: _.extend(_.clone(_super.prototype.events), {
"click .display-btn": function(ev) {
+1 -1
View File
@@ -203,7 +203,7 @@ export default Backbone.View.extend({
.append(
$("<div/>")
.addClass("ui-form-field")
.append($("<span/>").addClass("ui-form-info form-text text-muted"))
.append($("<span/>").addClass("ui-form-info form-text text-muted mt-2"))
.append($("<div/>").addClass("ui-form-backdrop"))
)
.append($("<div/>").addClass("ui-form-preview"));
@@ -29,6 +29,7 @@ export default Backbone.Model.extend({
boolean: "_fieldBoolean",
drill_down: "_fieldDrilldown",
color: "_fieldColor",
group_tag: "_fieldSelect",
hidden: "_fieldHidden",
hidden_data: "_fieldHidden",
baseurl: "_fieldHidden",
@@ -125,7 +126,7 @@ export default Backbone.Model.extend({
/** Text input field */
_fieldText: function(input_def) {
// field replaces e.g. a select field
if (input_def.options && input_def.data) {
if (input_def.data_ref || input_def.options && input_def.data) {
input_def.area = input_def.multiple;
if (Utils.isEmpty(input_def.value)) {
input_def.value = null;
+47 -14
View File
@@ -8,6 +8,8 @@ import Templates from "mvc/grid/grid-template";
import PopupMenu from "mvc/ui/popup-menu";
import LoadingIndicator from "ui/loading-indicator";
import { init_refresh_on_change } from "onload/globalInits/init_refresh_on_change";
import store from "../../store";
import slugify from "slugify";
// This is necessary so that, when nested arrays are used in ajax/post/get methods, square brackets ('[]') are
// not appended to the identifier of a nested array.
@@ -25,17 +27,21 @@ export default Backbone.View.extend({
this.active_tab = grid_config.active_tab;
var self = this;
// Why is this a global?
window.add_tag_to_grid_filter = (tag_name, tag_value) => {
// Put tag name and value together.
var tag = tag_name + (tag_value !== undefined && tag_value !== "" ? `:${tag_value}` : "");
var advanced_search = $("#advanced-search").is(":visible");
if (!advanced_search) {
$("#standard-search").slideToggle("fast");
$("#advanced-search").slideToggle("fast");
// Subscribe to changes in the store, currently just storing
// tag changes from the tagging components, but that will change
// when we rework the grid. This subscription ties this older grid
// code to the new vue components
store.watch(
state => state.gridSearch.searchTags,
newTags => {
let tagArray = Array.from(newTags);
self.grid.add_filter("tags", tagArray, false);
self.openAdvancedSearch();
self.render_filter_button("tags", tagArray);
self.go_page_one();
self.execute();
}
self.add_filter_condition("tags", tag);
};
);
if (grid_config.url_base && !grid_config.items) {
LoadingIndicator.markViewAsLoading(this);
@@ -66,6 +72,14 @@ export default Backbone.View.extend({
}
},
openAdvancedSearch: function() {
var isOpen = $("#advanced-search").is(":visible");
if (!isOpen) {
$("#standard-search").slideToggle("fast");
$("#advanced-search").slideToggle("fast");
}
},
// refresh frames
handle_refresh: function(refresh_frames) {
if (refresh_frames) {
@@ -99,6 +113,9 @@ export default Backbone.View.extend({
// append main template
this.$el.html(Templates.grid(options));
// add a class identifier for styling purposes
this.$el.addClass(this.getRootClassName(grid_config));
// update div contents
this.$el.find("#grid-table-header").html(Templates.header(options));
this.$el.find("#grid-table-body").html(Templates.body(options));
@@ -310,6 +327,14 @@ export default Backbone.View.extend({
// Add condition to grid.
this.grid.add_filter(name, value, true);
this.render_filter_button(name, value);
// execute
this.go_page_one();
this.execute();
},
render_filter_button: function(name, value) {
// Add button that displays filter and provides a button to delete it.
var t = $(Templates.filter_element(name, value));
var self = this;
@@ -324,10 +349,6 @@ export default Backbone.View.extend({
// append to container
var container = this.$el.find(`#${name}-filtering-criteria`);
container.append(t);
// execute
this.go_page_one();
this.execute();
},
// Remove a condition to the grid filter; this adds the condition and refreshes the grid.
@@ -335,6 +356,11 @@ export default Backbone.View.extend({
// Remove filter condition.
this.grid.remove_filter(name, value);
// update vuex if the one criteria we're currently tracking changes
if (name == "tags") {
store.dispatch("removeSearchTag", { text: value });
}
// Execute
this.go_page_one();
this.execute();
@@ -665,5 +691,12 @@ export default Backbone.View.extend({
});
}
});
},
// Generates a class name at the root of the view that we can
// use for conditional styling in the various kinds of grids
// instead of acres of if/then statements in javascript
getRootClassName({ title = "grid" }) {
return slugify(title).toLowerCase();
}
});
+1 -2
View File
@@ -1,7 +1,6 @@
import _ from "underscore";
import DATASET_LI from "mvc/dataset/dataset-li";
import BASE_MVC from "mvc/base-mvc";
import HISTORY_ITEM_LI from "mvc/history/history-item-li";
import _l from "utils/localization";
//==============================================================================
@@ -39,7 +38,7 @@ HDAListItemView.prototype.templates = (() => {
<span class="name">${_.escape(dataset.name)}</span>
</div>
</br>
${HISTORY_ITEM_LI.nametagTemplate(dataset)}
<div class="nametags"></div>
</div>
`;
+18 -2
View File
@@ -2,8 +2,8 @@ import _ from "underscore";
import STATES from "mvc/dataset/states";
import DC_LI from "mvc/collection/collection-li";
import DC_VIEW from "mvc/collection/collection-view";
import HISTORY_ITEM_LI from "mvc/history/history-item-li";
import _l from "utils/localization";
import { mountNametags } from "components/Nametags";
//==============================================================================
var _super = DC_LI.DCListItemView;
@@ -13,12 +13,19 @@ var HDCAListItemView = _super.extend(
/** @lends HDCAListItemView.prototype */ {
className: `${_super.prototype.className} history-content`,
render: function() {
let result = _super.prototype.render.apply(this, arguments);
this._mountNametags("initialize");
return result;
},
/** event listeners */
_setUpListeners: function() {
_super.prototype._setUpListeners.call(this);
var renderListen = (model, options) => {
// We want this to swap immediately without extra animations.
this.render(0);
this._mountNametags("listener");
};
if (this.model.jobStatesSummary) {
this.listenTo(this.model.jobStatesSummary, "change", renderListen);
@@ -28,6 +35,15 @@ var HDCAListItemView = _super.extend(
});
},
_mountNametags(context) {
let container = this.$el.find(".nametags")[0];
if (container) {
let { id, model_class, tags } = this.model.attributes;
let storeKey = `${model_class}-${id}`;
mountNametags({ storeKey, tags }, container);
}
},
/** Override to provide the proper collections panels as the foldout */
_getFoldoutPanelClass: function() {
return DC_VIEW.CollectionView;
@@ -137,7 +153,7 @@ HDCAListItemView.prototype.templates = (() => {
</div>
<div class="state-description">
</div>
${HISTORY_ITEM_LI.nametagTemplate(collection)}
<div class="nametags"><!-- Nametags mount here (hdca-li) --></div>
</div>
`;
@@ -1,21 +0,0 @@
import _ from "underscore";
import Utils from "utils/utils";
function _templateNametag(tag) {
return `<span style="${Utils.generateTagStyle(tag.slice(5))}" class="badge badge-primary badge-tags">${_.escape(
tag.slice(5)
)}</span>`;
}
function nametagTemplate(historyItem) {
let uniqueNametags = _.filter(_.uniq(historyItem.tags), t => t.indexOf("name:") === 0);
let nametagsDisplay = _.sortBy(uniqueNametags).map(_templateNametag);
return `
<div class="nametags" title="${uniqueNametags.length} nametags">
${nametagsDisplay.join("")}
</div>`;
}
export default {
nametagTemplate: nametagTemplate
};
@@ -224,16 +224,7 @@ var CurrentHistoryView = _super.extend(
/** In this override, get and set current panel preferences when editor is used */
_renderTags: function($where) {
var panel = this;
// render tags and show/hide based on preferences
_super.prototype._renderTags.call(panel, $where);
if (panel.preferences.get("tagsEditorShown")) {
panel.tagsEditor.toggle(true);
}
// store preference when shown or hidden
panel.listenTo(panel.tagsEditor, "hiddenUntilActivated:shown hiddenUntilActivated:hidden", tagsEditor => {
panel.preferences.set("tagsEditorShown", tagsEditor.hidden);
});
return _super.prototype._renderTags.call(this, $where);
},
/** In this override, get and set current panel preferences when editor is used */
@@ -5,7 +5,7 @@ import HISTORY_VIEW from "mvc/history/history-view";
import HDA_MODEL from "mvc/history/hda-model";
import HDA_LI_EDIT from "mvc/history/hda-li-edit";
import HDCA_LI_EDIT from "mvc/history/hdca-li-edit";
import TAGS from "mvc/tag";
import { mountModelTags } from "components/Tags";
import ANNOTATIONS from "mvc/annotation";
import LIST_COLLECTION_CREATOR from "mvc/collection/list-collection-creator";
import PAIR_COLLECTION_CREATOR from "mvc/collection/pair-collection-creator";
@@ -155,28 +155,31 @@ var HistoryViewEdit = _super.extend(
/** render the tags sub-view controller */
_renderTags: function($where) {
var panel = this;
this.tagsEditor = new TAGS.TagsEditor({
let el = $where.find(".controls .tags-display")[0];
let propsData = {
model: this.model,
el: $where.find(".controls .tags-display"),
onshowFirstTime: function() {
this.render();
},
// show hide sub-view tag editors when this is shown/hidden
onshow: function() {
panel.toggleHDATagEditors(true, panel.fxSpeed);
},
onhide: function() {
panel.toggleHDATagEditors(false, panel.fxSpeed);
},
$activator: faIconButton({
title: _l("Edit history tags"),
classes: "history-tag-btn",
faIcon: "fa-tags",
tooltipConfig: { placement: "top" }
}).appendTo($where.find(".controls .actions"))
disabled: false,
context: "history-view-edit"
};
let vm = mountModelTags(propsData, el);
// tag icon button open/closes
let activator = faIconButton({
title: _l("Edit history tags"),
classes: "history-tag-btn",
faIcon: "fa-tags",
tooltipConfig: { placement: "top" }
}).appendTo($where.find(".controls .actions"));
activator.on("click", () => {
$(vm.$el).toggleClass("active");
});
return vm;
},
/** render the annotation sub-view controller */
_renderAnnotation: function($where) {
var panel = this;
@@ -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>',
"&nbsp;Modify",
"</button>",
'<button data-toggle="tooltip" data-placement="top" title="Attempt to detect the format of dataset" class="btn btn-secondary toolbtn_detect_datatype toolbar-item mr-1" type="button">',
'<span class="fa fa-undo"></span>',
"&nbsp;Auto-detect datatype",
"</button>",
"<% } %>",
'<% if (item.get("can_user_manage")) { %>',
'<a href="#folders/<%- item.get("folder_id") %>/datasets/<%- item.id %>/permissions">',
@@ -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() {
+28 -20
View File
@@ -344,6 +344,8 @@ _.extend(ToolSection.prototype, VisibilityMixin);
* query.
*/
var ToolSearch = Backbone.Model.extend({
SEARCH_RESERVED_TERMS_FAVORITES: ["#favs", "#favorites", "#favourites"],
defaults: {
search_hint_string: "search tools",
min_chars_for_search: 3,
@@ -365,6 +367,7 @@ var ToolSearch = Backbone.Model.extend({
* Do the search and update the results.
*/
do_search: function() {
let Galaxy = getGalaxyInstance();
var query = this.attributes.query;
// If query is too short, do not search.
@@ -379,26 +382,31 @@ var ToolSearch = Backbone.Model.extend({
if (this.timer) {
clearTimeout(this.timer);
}
// Start a new ajax-request in X ms
$("#search-clear-btn").hide();
$("#search-spinner").show();
var self = this;
this.timer = setTimeout(() => {
// log the search to analytics if present
if (typeof ga !== "undefined") {
ga("send", "pageview", `${getAppRoot()}?q=${q}`);
}
$.get(
self.urlRoot,
{ q: q },
data => {
self.set("results", data);
$("#search-spinner").hide();
$("#search-clear-btn").show();
},
"json"
);
}, 400);
// Catch reserved words
if (this.SEARCH_RESERVED_TERMS_FAVORITES.indexOf(q) >= 0) {
this.set("results", Galaxy.user.getFavorites().tools);
} else {
// Start a new ajax-request in X ms
$("#search-clear-btn").hide();
$("#search-spinner").show();
var self = this;
this.timer = setTimeout(() => {
// log the search to analytics if present
if (typeof ga !== "undefined") {
ga("send", "pageview", `${getAppRoot()}?q=${q}`);
}
$.get(
self.urlRoot,
{ q: q },
data => {
self.set("results", data);
$("#search-spinner").hide();
$("#search-clear-btn").show();
},
"json"
);
}, 400);
}
},
clear_search: function() {
+39 -21
View File
@@ -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 };
+25 -1
View File
@@ -28,7 +28,8 @@ var User = Backbone.Model.extend(baseMVC.LoggableMixin).extend(
total_disk_usage: 0,
nice_total_disk_usage: "",
quota_percent: null,
is_admin: false
is_admin: false,
preferences: {}
},
/** Set up and bind events
@@ -53,6 +54,29 @@ var User = Backbone.Model.extend(baseMVC.LoggableMixin).extend(
return this.get("is_admin");
},
updatePreferences: function(name, new_value) {
let preferences = this.get("preferences");
preferences[name] = JSON.stringify(new_value);
this.preferences = preferences;
},
getFavorites: function() {
let preferences = this.get("preferences");
if (preferences && preferences.favorites) {
return JSON.parse(preferences.favorites);
} else {
return {
tools: []
};
}
},
updateFavorites: function(object_type, new_favorites) {
let favorites = this.getFavorites();
favorites[object_type] = new_favorites[object_type];
this.updatePreferences("favorites", favorites);
},
/** Load a user with the API using an id.
* If getting an anonymous user or no access to a user id, pass the User.CURRENT_ID_STR
* (e.g. 'current') and the API will return the current transaction's user data.
@@ -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);
}
});
+19 -16
View File
@@ -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");
});
+2 -2
View File
@@ -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(
-12
View File
@@ -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()");
});
});
+17
View File
@@ -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
}
});
+43
View File
@@ -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));
});
});
});
-369
View File
@@ -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;
}
};
-13
View File
@@ -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);
+71
View File
@@ -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 -1
View File
@@ -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 });
};
+5
View File
@@ -0,0 +1,5 @@
// This file exists purely to make unit testing easier
export function redirectToUrl(url) {
window.location = url;
}
+2 -69
View File
@@ -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 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+25 -54
View File
@@ -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 */
-4
View File
@@ -304,10 +304,6 @@
}
}
.tags-display {
// Hidden by default; the interface toggles this.
display: none;
}
.annotation-display {
display: none;
@extend %vertical-spacing;
+8 -11
View File
@@ -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);
}
+31
View File
@@ -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;
}
}
-7
View File
@@ -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 {
+1 -1
View File
@@ -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;
-1
View File
@@ -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;
+14 -13
View File
@@ -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
View File
@@ -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);
+50 -22
View File
@@ -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);
+2 -1
View File
@@ -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);
+36 -19
View File
@@ -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
View File
@@ -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",
+23 -5
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -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
+28 -11
View File
@@ -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
View File
@@ -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