mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge remote-tracking branch 'upstream/dev' into dnd-upload
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
exclude=$(sed -e 's|^|./|' -e 's|/$||' .ci/flake8_ignorelist.txt | paste -s -d ',' - )
|
||||
autopep8 -i -r --exclude $exclude --select E11,E101,E127,E201,E202,E22,E301,E302,E303,E304,E306,E711,W291,W292,W293,W391 ./lib/ ./test/
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
if command -v ack-grep >/dev/null; then
|
||||
ACK=ack-grep
|
||||
else
|
||||
ACK=ack
|
||||
fi
|
||||
|
||||
|
||||
PYTHON2_ONLY_MODULES="__builtin__ _winreg BaseHTTPServer CGIHTTPServer \
|
||||
ConfigParser Cookie cookielib copy_reg cPickle cStringIO Dialog dummy_thread \
|
||||
FileDialog gdbm htmlentitydefs HTMLParser httplib Queue robotparser \
|
||||
ScrolledText SimpleDialog SimpleHTTPServer SimpleXMLRPCServer SocketServer \
|
||||
StringIO thread Tix tkColorChooser tkCommonDialog Tkconstants Tkdnd tkFont \
|
||||
Tkinter tkFileDialog tkMessageBox tkSimpleDialog ttk urllib urllib2 urlparse \
|
||||
xmlrpclib"
|
||||
|
||||
ret=0
|
||||
for mod in $PYTHON2_ONLY_MODULES; do
|
||||
$ACK --type python \
|
||||
--ignore-dir=.git \
|
||||
--ignore-dir=.tox \
|
||||
--ignore-dir=.venv \
|
||||
--ignore-dir=.venv3 \
|
||||
--ignore-dir=client/node_modules \
|
||||
--ignore-dir=database \
|
||||
--ignore-dir=doc/build \
|
||||
--ignore-dir=eggs \
|
||||
--ignore-dir=static/maps \
|
||||
--ignore-dir=static/scripts \
|
||||
"^import $mod(\n|\.)|^from $mod import "
|
||||
if [ $? -eq 0 ]; then ret=1; fi
|
||||
done
|
||||
|
||||
exit $ret
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
n_tests=$(pytest --collect-only --ignore=test/functional lib/galaxy_test/ test/ | grep 'tests collected' | sed -e 's/[^0-9]*\([0-9]*\) tests collected.*/\1/')
|
||||
n_tests_extra_classes=$(pytest -o python_classes='Test* *Test *TestCase' --collect-only --ignore=test/functional lib/galaxy_test/ test/ | grep 'tests collected' | sed -e 's/[^0-9]*\([0-9]*\) tests collected.*/\1/')
|
||||
if [ "$n_tests_extra_classes" -gt "$n_tests" ]; then
|
||||
echo "New test class with name not starting with Test introduced, change it to have tests collected by pytest"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Check test class names
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.ci/check_test_class_names.sh'
|
||||
- 'lib/galaxy_test/**'
|
||||
- 'test/**'
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.7']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Cache pip dir
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
|
||||
- name: Install Python dependencies
|
||||
run: pip install -r requirements.txt -r lib/galaxy/dependencies/dev-requirements.txt
|
||||
- name: Run tests
|
||||
run: .ci/check_test_class_names.sh
|
||||
@@ -59,24 +59,11 @@ jobs:
|
||||
- name: Lint converters
|
||||
run: |
|
||||
mapfile -t TOOL_ARRAY < tool_list.txt
|
||||
for CONVERTER in "${TOOL_ARRAY[@]}"; do
|
||||
planemo lint --skip citations,stdio,help --report_level warn "$CONVERTER"
|
||||
done
|
||||
planemo lint --skip citations,stdio,help --report_level warn "${TOOL_ARRAY[@]}"
|
||||
- name: Run tests
|
||||
run: |
|
||||
mkdir -p json_output
|
||||
EXIT_CODE=0
|
||||
mapfile -t TOOL_ARRAY < tool_list.txt
|
||||
for CONVERTER in "${TOOL_ARRAY[@]}"; do
|
||||
json=$(mktemp -u -p json_output --suff .json)
|
||||
planemo test --test_output_json "$json" --galaxy_python_version ${{ matrix.python-version }} --galaxy_root 'galaxy root' "$CONVERTER" || EXIT_CODE=$?
|
||||
done
|
||||
planemo merge_test_reports json_output/*.json tool_test_output.json
|
||||
planemo test_reports tool_test_output.json --test_output tool_test_output.html
|
||||
if [ "$EXIT_CODE" -ne 0 ]; then
|
||||
echo "Unsuccessful tests found, inspect the 'Converter test results' artifact for details."
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
planemo test --galaxy_python_version ${{ matrix.python-version }} --galaxy_root 'galaxy root' "${TOOL_ARRAY[@]}"
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
|
||||
@@ -18,6 +18,7 @@ jobs:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.7']
|
||||
steps:
|
||||
@@ -33,7 +34,6 @@ jobs:
|
||||
run: echo "version=$(python -c 'import sys; print("-".join(str(v) for v in sys.version_info))')" >> $GITHUB_OUTPUT
|
||||
- name: Cache pip dir
|
||||
uses: actions/cache@v3
|
||||
id: pip-cache
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }}
|
||||
@@ -42,9 +42,11 @@ jobs:
|
||||
with:
|
||||
path: .tox
|
||||
key: tox-cache-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-mulled
|
||||
- name: Install Apptainer's singularity
|
||||
uses: eWaterCycle/setup-apptainer@v2
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
- name: run tests
|
||||
- name: Run tests
|
||||
run: tox -e mulled
|
||||
working-directory: 'galaxy root'
|
||||
- uses: actions/upload-artifact@v3
|
||||
|
||||
@@ -37,15 +37,17 @@ jobs:
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }}
|
||||
- name: Cache galaxy venv
|
||||
- name: Cache tox env
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: 'galaxy root/.venv'
|
||||
key: gxy-venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-unit
|
||||
path: .tox
|
||||
key: tox-cache-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('galaxy root/requirements.txt') }}-unit
|
||||
- name: Install ffmpeg
|
||||
run: sudo apt-get update && sudo apt-get -y install ffmpeg
|
||||
- name: Install tox
|
||||
run: pip install tox
|
||||
- name: Run tests
|
||||
run: ./run_tests.sh --coverage -u
|
||||
run: tox -e unit-coverage
|
||||
working-directory: 'galaxy root'
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.1.0
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.7
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 4.0.1
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 5.0.4
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
@@ -15,20 +14,21 @@ repos:
|
||||
types: [file]
|
||||
types_or: [javascript, jsx, ts, tsx, vue]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.1.0 # Use the ref you want to point at
|
||||
rev: v4.3.0 # Use the ref you want to point at
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: check-merge-conflict
|
||||
- id: check-symlinks
|
||||
- id: destroyed-symlinks
|
||||
- id: end-of-file-fixer
|
||||
- id: name-tests-test
|
||||
- repo: https://github.com/detailyang/pre-commit-shell
|
||||
rev: v1.0.6
|
||||
rev: 1.0.5
|
||||
hooks:
|
||||
- id: shell-lint
|
||||
args: [--format=json]
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.14.0
|
||||
rev: 0.19.2
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- repo: local
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
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:=22.09
|
||||
RELEASE_CURR:=23.0
|
||||
RELEASE_UPSTREAM:=upstream
|
||||
CONFIG_MANAGE=$(IN_VENV) python lib/galaxy/config/config_manage.py
|
||||
PROJECT_URL?=https://github.com/galaxyproject/galaxy
|
||||
@@ -47,9 +47,6 @@ format: ## Format Python code base
|
||||
remove-unused-imports: ## Remove unused imports in Python code base
|
||||
$(IN_VENV) autoflake --in-place --remove-all-unused-imports --recursive --verbose lib/ test/
|
||||
|
||||
list-dependency-updates: setup-venv
|
||||
$(IN_VENV) pip list --outdated --format=columns
|
||||
|
||||
docs-slides-ready:
|
||||
test -f plantuml.jar || wget http://jaist.dl.sourceforge.net/project/plantuml/plantuml.jar
|
||||
java -jar plantuml.jar -c $(DOC_SOURCE_DIR)/slideshow/architecture/images/plantuml_options.txt -tsvg $(SLIDESHOW_DIR)/architecture/images/ *.plantuml.txt
|
||||
|
||||
+32
-5
@@ -24,16 +24,20 @@
|
||||
"vue/valid-v-slot": "error",
|
||||
"vue/v-slot-style": ["error", { "atComponent": "v-slot", "default": "v-slot", "named": "longform" }],
|
||||
|
||||
// vue/multi-word-component names is considered an error, that we
|
||||
// downgrade to a warning here. It should be prioritized to get fixed
|
||||
// and compliant.
|
||||
// TODO: We want to fix or add inline exceptions documenting these.
|
||||
// Downgrade the severity of some rules to warnings as a transition measure.
|
||||
// For example, vue/multi-word-component names is considered an error,
|
||||
// but that kind of refactoring is best done slowly, one bit at a time
|
||||
// as those components are touched.
|
||||
"vue/multi-word-component-names": "warn",
|
||||
"vue/prop-name-casing": "warn",
|
||||
"vue/require-prop-types": "warn",
|
||||
"vue/require-default-prop": "warn",
|
||||
"vue/no-v-html": "warn",
|
||||
|
||||
// Increase the severity of some rules to errors
|
||||
"vue/attributes-order": "error",
|
||||
"vue/order-in-components": "error",
|
||||
|
||||
// Prettier compromises/workarounds -- mostly #wontfix?
|
||||
"vue/html-indent": "off",
|
||||
"vue/max-attributes-per-line": "off",
|
||||
@@ -54,6 +58,29 @@
|
||||
"vuejs-accessibility/mouse-events-have-key-events": "warn",
|
||||
"vuejs-accessibility/no-autofocus": "error",
|
||||
"vuejs-accessibility/tabindex-no-positive": "error"
|
||||
|
||||
},
|
||||
"ignorePatterns": ["src/qunit", "src/mocha", "src/libs", "src/nls", "src/legacy"]
|
||||
"ignorePatterns": ["src/qunit", "src/mocha", "src/libs", "src/nls", "src/legacy"],
|
||||
"overrides": [{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/recommended",
|
||||
"plugin:compat/recommended",
|
||||
"plugin:vuejs-accessibility/recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": { "jsx": true },
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": 0
|
||||
},
|
||||
"plugins": ["@typescript-eslint"]
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
16.15.1
|
||||
18.12.1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# Composables
|
||||
|
||||
Composables are way of splitting up your code into distinct, reusable chunks.
|
||||
They can replace providers, mixins and more. Any code you can put into a component, can also be written as a composable.
|
||||
Using them effectively can make your code more reusable, decoupled, and easier to follow.
|
||||
|
||||
**More about Composables:**
|
||||
|
||||
* [Composables Overview](https://vuejs.org/guide/reusability/composables.html)
|
||||
* [Composition API](https://vuejs.org/api/composition-api-setup.html)
|
||||
* [\<script setup\>](https://vuejs.org/api/sfc-script-setup.html)
|
||||
|
||||
## Using Composables in the Composition API
|
||||
|
||||
Example: accessing the current user from the store
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useCurrentUser } from "composables/user";
|
||||
|
||||
const { currentUser } = useCurrentUser();
|
||||
</script>
|
||||
```
|
||||
|
||||
You can now access the current user with `currentUser.value`.
|
||||
|
||||
## Using Composables in the Options API
|
||||
|
||||
Composables are not limited to the composition api. This is the same example from above, using the options api.
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import { useCurrentUser } from "composables/user";
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const { currentUser } = useCurrentUser();
|
||||
return { currentUser };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
You can now access the current user with `this.currentUser` from anywhere within the component.
|
||||
|
||||
## Testing Components with Composable Stores
|
||||
|
||||
When writing a test which includes a component that has a composable store (like useCurrentUser),
|
||||
there are two ways to test it.
|
||||
|
||||
### Mocking the store
|
||||
|
||||
You can provide the store in the mount function as follows:
|
||||
|
||||
```js
|
||||
const wrapper = shallowMount(TestedComponent,
|
||||
localVue,
|
||||
provide: { store },
|
||||
});
|
||||
```
|
||||
|
||||
`store` must be a Vuex store.
|
||||
The `mockModule` helper can help creating a store for the required modules:
|
||||
|
||||
```js
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
user: mockModule(userStore),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking the composable
|
||||
|
||||
The second option is to mock the composable:
|
||||
|
||||
```js
|
||||
import { useCurrentUser } from "composables/user";
|
||||
|
||||
jest.mock("composables/user");
|
||||
useCurrentUser.mockReturnValue({
|
||||
currentUser: {}
|
||||
});
|
||||
```
|
||||
|
||||
While simpler in this example, you may need to manually mock more return values and composables than the other method, depending on the composables the component is using.
|
||||
|
||||
## Using Composables for more than Stores
|
||||
|
||||
Composables can be of great use to extract any reactive code from your components. For an example of this, take a look at [userFilterObjectArray](https://github.com/galaxyproject/galaxy/blob/dev/client/src/composables/utils/filter.js).
|
||||
|
||||
Usage:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useFilterObjectArray } from "composables/utils/filter";
|
||||
|
||||
const filteredArray = useFilterObjectArray(
|
||||
someReactiveArray,
|
||||
searchValue,
|
||||
["name", "description"]
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
It's a simple filtering function, but fully reactive.
|
||||
Whenever any of the inputs changes, the return value is re-computed, without having to call the function again.
|
||||
@@ -1,3 +1,7 @@
|
||||
**Notice** Consider using [Composables](composables.md) instead of Providers. They offer more functionality and need less boilerplate.
|
||||
|
||||
---
|
||||
|
||||
We are using components in two very distinct ways. The first, "normal", kind of component will
|
||||
probably look familiar to anybody whis is already passingly familiar with Vue. Here the relevant
|
||||
information comes in as properties, any internal variables get defined in "data", changes go out as
|
||||
@@ -27,7 +31,7 @@ restriction that Vue needs a single root element in which to render.
|
||||
<template>
|
||||
<AutoComplete
|
||||
:options="options"
|
||||
:value="doodad.category"
|
||||
:value="doodad.category"
|
||||
@select="saveCategory"
|
||||
/>
|
||||
</template>
|
||||
@@ -35,7 +39,7 @@ restriction that Vue needs a single root element in which to render.
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
props: {
|
||||
doodad: { type: Object, required: true },
|
||||
options: { type: Array, required: true },
|
||||
},
|
||||
@@ -51,7 +55,7 @@ export default {
|
||||
|
||||
This component accepts a mandatory input object (doodad), lets the user play with a category prop,
|
||||
then emits a fresh object after it's done. So what, what's the big deal? The important part to walk
|
||||
away from this dumb example is the things that are NOT in this sample component.
|
||||
away from this dumb example is the things that are NOT in this sample component.
|
||||
|
||||
This component doesn't save the data. This component doesn't make ajax calls, and this component
|
||||
doesn't mutate its props. What it does do is to allow the user to edit some object named "doodad"
|
||||
@@ -149,8 +153,8 @@ describe("A renderless component", () => {
|
||||
},
|
||||
});
|
||||
|
||||
// waits for "updated" Vue lifecycle hook to fire on the renderless
|
||||
// component. This is often good enough for waiting for
|
||||
// waits for "updated" Vue lifecycle hook to fire on the renderless
|
||||
// component. This is often good enough for waiting for
|
||||
// an initial ajax load to finish, for example
|
||||
await waitForLifecyleEvent(wrapper.vm, "updated");
|
||||
})
|
||||
@@ -161,4 +165,4 @@ describe("A renderless component", () => {
|
||||
// ...more tests
|
||||
})
|
||||
})
|
||||
```
|
||||
```
|
||||
|
||||
+8
-4
@@ -32,7 +32,7 @@ const STATIC_PLUGIN_BUILD_IDS = [
|
||||
"pv",
|
||||
"nora",
|
||||
"venn",
|
||||
"ts_visjs"
|
||||
"ts_visjs",
|
||||
];
|
||||
const DIST_PLUGIN_BUILD_IDS = ["new_user"];
|
||||
const PLUGIN_BUILD_IDS = Array.prototype.concat(DIST_PLUGIN_BUILD_IDS, STATIC_PLUGIN_BUILD_IDS);
|
||||
@@ -128,7 +128,7 @@ function buildPlugins(callback, forceRebuild) {
|
||||
// Stage current hash to .orig for debugging and to
|
||||
// force a plugin rebuild in the event of a failure
|
||||
// (i.e. -- we're committed to a new build of this plugin).
|
||||
fs.renameSync(hashFilePath, `${hashFilePath}.orig`)
|
||||
fs.renameSync(hashFilePath, `${hashFilePath}.orig`);
|
||||
}
|
||||
} else {
|
||||
console.log(`No build hashfile detected for ${pluginName}, generating now.`);
|
||||
@@ -150,8 +150,12 @@ function buildPlugins(callback, forceRebuild) {
|
||||
);
|
||||
console.log(`Building ${pluginName}`);
|
||||
if (
|
||||
child_process.spawnSync("yarn", ["build"], { cwd: pluginDir, stdio: "inherit", shell: true })
|
||||
.status === 0
|
||||
child_process.spawnSync("yarn", ["build"], {
|
||||
cwd: pluginDir,
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
env: { ...process.env, NODE_OPTIONS: "--openssl-legacy-provider" },
|
||||
}).status === 0
|
||||
) {
|
||||
console.log(`Successfully built, saving build state to ${hashFilePath}`);
|
||||
child_process.exec(`(git rev-parse HEAD 2>/dev/null || echo \`\`) > ${hashFilePath}`);
|
||||
|
||||
+17
-9
@@ -67,6 +67,7 @@
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-regexp": "^0.4.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"pinia": "^2.0.23",
|
||||
"popper.js": "^1.16.1",
|
||||
"pretty-bytes": "^6.0.0",
|
||||
"pyre-to-regexp": "^0.0.5",
|
||||
@@ -90,7 +91,7 @@
|
||||
"vue-multiselect": "^2.1.6",
|
||||
"vue-observe-visibility": "^1.0.0",
|
||||
"vue-prismjs": "^1.2.0",
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-rx": "^6.2.0",
|
||||
"vue-virtual-scroll-list": "^2.3.3",
|
||||
"vuedraggable": "2.24.3",
|
||||
@@ -113,15 +114,15 @@
|
||||
"webpack-production-maps": "GXY_BUILD_SOURCEMAPS=1 NODE_ENV=production webpack",
|
||||
"gulp": "gulp",
|
||||
"save-build-hash": "(git rev-parse HEAD 2>/dev/null || echo '') >../static/client_build_hash.txt",
|
||||
"format": "prettier --write 'src/style/scss/**/*.scss' 'src/**/{*.js,*.vue}' '!src/libs/**'",
|
||||
"format-check": "prettier --check 'src/style/scss/**/*.scss' 'src/**/{*.js,*.vue}' '!src/libs/**'",
|
||||
"format": "prettier --write 'src/style/scss/**/*.scss' 'src/**/{*.ts,*.js,*.vue}' 'tests/jest/standalone/{*.ts,*.js}' '!src/libs/**'",
|
||||
"format-check": "prettier --check 'src/style/scss/**/*.scss' 'src/**/{*.js,*.vue}' 'tests/jest/standalone/{*.ts,*.js}' '!src/libs/**'",
|
||||
"prettier": "yarn run format",
|
||||
"test": "yarn run qunit && yarn run jest",
|
||||
"jest": "jest --config tests/jest/jest.config.js",
|
||||
"jest-watch": "jest --config tests/jest/jest.config.js --watch",
|
||||
"qunit": "karma start tests/karma/karma.config.qunit.js",
|
||||
"eslint-precommit": "eslint -c .eslintrc.json --quiet --ext .js,.vue src",
|
||||
"eslint": "eslint -c .eslintrc.json --ext .js,.vue src"
|
||||
"eslint-precommit": "eslint -c .eslintrc.json --quiet --ext .js,.vue,.ts src tests/jest/standalone",
|
||||
"eslint": "eslint -c .eslintrc.json --ext .js,.vue,.ts src tests/jest/standalone"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.9",
|
||||
@@ -130,8 +131,11 @@
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.18.9",
|
||||
"@babel/preset-env": "^7.18.9",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@cerner/duplicate-package-checker-webpack-plugin": "^2.3.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@vue/test-utils": "^1.3.0",
|
||||
"@vue/vue2-jest": "^29.1.0",
|
||||
"amdi18n-loader": "^0.9.3",
|
||||
@@ -146,10 +150,10 @@
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||
"del": "^6.0.0",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-plugin-compat": "^4.0.2",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^1.2.0",
|
||||
"eslint-plugin-vue": "^9.7.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^2.0.0",
|
||||
"expose-loader": "^4.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"ignore-loader": "^0.1.2",
|
||||
@@ -157,7 +161,6 @@
|
||||
"jest": "^27.4.7",
|
||||
"jest-location-mock": "^1.0.9",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"jest-transform-yaml": "1.0.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"karma": "^6.3.16",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
@@ -175,12 +178,17 @@
|
||||
"sinon": "^13.0.1",
|
||||
"store": "^2.0.12",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-loader": "^9.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"typescript-eslint": "^0.0.1-alpha.0",
|
||||
"vue-loader": "^15.10.0",
|
||||
"vue-template-compiler": "^2.7.8",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.9.3",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"yaml-jest": "^1.2.0",
|
||||
"yaml-loader": "^0.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -27,7 +27,7 @@ if (!window.Galaxy) {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.error("Detected redefinition of window.Galaxy -- skipping, but this should be investigated.", serverPath());
|
||||
console.debug("Skipping, window.Galaxy already exists.", serverPath());
|
||||
}
|
||||
|
||||
export default window.Galaxy;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { computed } from "vue";
|
||||
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { useConfig } from "composables/useConfig";
|
||||
import { useConfig } from "composables/config";
|
||||
import UtcDate from "components/UtcDate";
|
||||
import License from "components/License/License";
|
||||
import ExternalLink from "components/ExternalLink";
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
</div>
|
||||
<b>{{ l("New Type") }}: </b>
|
||||
<multiselect
|
||||
class="datatype-dropdown"
|
||||
v-model="selectedDatatype"
|
||||
class="datatype-dropdown"
|
||||
deselect-label="Can't remove this value"
|
||||
track-by="id"
|
||||
label="text"
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
</div>
|
||||
<b>{{ l("Database/Build") }}: </b>
|
||||
<multiselect
|
||||
class="database-dropdown"
|
||||
v-model="selectedGenome"
|
||||
class="database-dropdown"
|
||||
deselect-label="Can't remove this value"
|
||||
track-by="id"
|
||||
label="text"
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
class="search-query"
|
||||
size="sm"
|
||||
autocomplete="off"
|
||||
:disabled="includeAdvBtn && showAdvanced"
|
||||
:placeholder="placeholder"
|
||||
@input="delayQuery"
|
||||
@change="setQuery"
|
||||
@keydown.esc="setQuery('')" />
|
||||
<b-input-group-append>
|
||||
<b-button
|
||||
v-if="includeAdvBtn"
|
||||
v-if="enableAdvanced"
|
||||
size="sm"
|
||||
:pressed="showAdvanced"
|
||||
:variant="showAdvanced ? 'info' : 'secondary'"
|
||||
@@ -27,7 +26,6 @@
|
||||
class="search-clear"
|
||||
size="sm"
|
||||
:title="titleClear | l"
|
||||
:disabled="includeAdvBtn && showAdvanced"
|
||||
data-description="reset query"
|
||||
@click="clearBox">
|
||||
<icon v-if="loading" icon="spinner" spin />
|
||||
@@ -55,7 +53,7 @@ export default {
|
||||
type: Number,
|
||||
default: 1000,
|
||||
},
|
||||
includeAdvBtn: {
|
||||
enableAdvanced: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
@@ -42,8 +42,8 @@ if (props.size) {
|
||||
<div class="stripe"></div>
|
||||
</div>
|
||||
<component
|
||||
v-else
|
||||
:is="element"
|
||||
v-else
|
||||
class="heading"
|
||||
:class="[sizeClass, props.bold ? 'font-weight-bold' : '', props.inline ? 'inline' : '']">
|
||||
<FontAwesomeIcon v-if="props.icon" :icon="props.icon" />
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
<div class="m-3">
|
||||
<div v-if="modelTitle">
|
||||
<h1 class="h-sm">About this {{ modelTitle }}</h1>
|
||||
<h2 class="h-md">{{ details.title || details.name }}</h2>
|
||||
<StatelessTags v-if="details.tags" class="tags mt-2" :value="details.tags" :disabled="true" />
|
||||
<h2 class="h-md">{{ item.title || item.name }}</h2>
|
||||
<img class="float-right" :src="gravatarSource" alt="user avatar" />
|
||||
<StatelessTags v-if="item.tags" class="tags mt-2" :value="item.tags" :disabled="true" />
|
||||
<br />
|
||||
<h2 class="h-sm">Author</h2>
|
||||
<div>{{ owner }}</div>
|
||||
@@ -25,7 +26,7 @@
|
||||
<router-link :to="publishedByUser"> Published {{ plural }} by {{ owner }}. </router-link>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingSpan v-else message="Loading details" />
|
||||
<LoadingSpan v-else message="Loading item details" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,21 +41,24 @@ export default {
|
||||
StatelessTags,
|
||||
},
|
||||
props: {
|
||||
details: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
gravatarSource() {
|
||||
return `https://secure.gravatar.com/avatar/${this.item.email_hash}?d=identicon`;
|
||||
},
|
||||
modelTitle() {
|
||||
const modelClass = this.details ? this.details.model_class : "Item";
|
||||
const modelClass = this.item ? this.item.model_class : "Item";
|
||||
if (modelClass == "StoredWorkflow") {
|
||||
return "Workflow";
|
||||
}
|
||||
return modelClass;
|
||||
},
|
||||
owner() {
|
||||
return this.details.owner || this.details.username || "Unavailable";
|
||||
return this.item.owner || this.item.username || "Unavailable";
|
||||
},
|
||||
plural() {
|
||||
if (this.modelTitle == "History") {
|
||||
@@ -66,7 +70,7 @@ export default {
|
||||
return this.plural.toLowerCase();
|
||||
},
|
||||
publishedByUser() {
|
||||
return `/${this.pluralPath}/list_published?f-username=${this.details.username}`;
|
||||
return `/${this.pluralPath}/list_published?f-username=${this.item.username}`;
|
||||
},
|
||||
urlAll() {
|
||||
return `/${this.pluralPath}/list_published`;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
methods: {
|
||||
async confirm(message, options = {}) {
|
||||
return this.$bvModal.msgBoxConfirm(message, {
|
||||
title: "Please Confirm",
|
||||
titleClass: "h-md",
|
||||
hideHeaderClose: false,
|
||||
...options,
|
||||
});
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div aria-labelledby="dataset-attributes-heading">
|
||||
<h1 id="dataset-attributes-heading" class="h-lg" v-localize>Edit Dataset Attributes</h1>
|
||||
<h1 id="dataset-attributes-heading" v-localize class="h-lg">Edit Dataset Attributes</h1>
|
||||
<b-alert v-if="messageText" :variant="messageVariant" show>
|
||||
{{ messageText | l }}
|
||||
</b-alert>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import MountTarget from "./FormRadio";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("FormRadio", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(MountTarget, {
|
||||
propsData: {
|
||||
value: false,
|
||||
options: [],
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
it("basics", async () => {
|
||||
const noInput = wrapper.find("[type='radio']");
|
||||
expect(noInput.exists()).toBe(false);
|
||||
const n = 3;
|
||||
const options = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
options.push([`label_${i}`, `value_${i}`]);
|
||||
}
|
||||
await wrapper.setProps({ options });
|
||||
const inputs = wrapper.findAll("[type='radio']");
|
||||
const labels = wrapper.findAll(".custom-control-label");
|
||||
expect(inputs.length).toBe(n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
await inputs.at(i).setChecked();
|
||||
expect(labels.at(i).text()).toBe(`label_${i}`);
|
||||
expect(inputs.at(i).attributes("value")).toBe(`value_${i}`);
|
||||
expect(wrapper.emitted()["input"][i][0]).toBe(`value_${i}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const $emit = defineEmits(["input"]);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const currentValue = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
},
|
||||
set: (val) => {
|
||||
$emit("input", val);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-form-radio-group v-model="currentValue" stacked>
|
||||
<b-form-radio v-for="(option, index) in options" :key="index" :value="option[1]">
|
||||
{{ option[0] }}
|
||||
</b-form-radio>
|
||||
</b-form-radio-group>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import FormRadio from "./FormRadio";
|
||||
|
||||
const $emit = defineEmits(["input"]);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
default: null,
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
display: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const currentValue = computed({
|
||||
get: () => {
|
||||
return props.value;
|
||||
},
|
||||
set: (val) => {
|
||||
$emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
/** Provides formatted select options. */
|
||||
const currentOptions = computed(() => {
|
||||
const data = props.data;
|
||||
const options = props.options;
|
||||
if (options && options.length > 0) {
|
||||
return options;
|
||||
} else if (data && data.length > 0) {
|
||||
return data.map((option) => {
|
||||
return [option.label, option.value];
|
||||
});
|
||||
}
|
||||
return [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form-radio v-if="display == 'radio'" v-model="currentValue" :options="currentOptions" />
|
||||
</template>
|
||||
@@ -57,11 +57,11 @@ export default {
|
||||
},
|
||||
collapsedEnableIcon: {
|
||||
type: String,
|
||||
default: "fa fa-caret-square-o-down",
|
||||
default: "far fa-caret-square-down",
|
||||
},
|
||||
collapsedDisableIcon: {
|
||||
type: String,
|
||||
default: "fa fa-caret-square-o-up",
|
||||
default: "far fa-caret-square-up",
|
||||
},
|
||||
validationScrollTo: {
|
||||
type: Array,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import FormElement from "./FormElement";
|
||||
import FormHidden from "./Elements/FormHidden";
|
||||
import FormInput from "./Elements/FormInput";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -17,53 +19,80 @@ describe("FormElement", () => {
|
||||
title: "title_text",
|
||||
},
|
||||
localVue,
|
||||
stubs: {
|
||||
FormInput: { template: "<div>form-input</div>" },
|
||||
FormHidden: { template: "<div>form-hidden</div>" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("check props", async () => {
|
||||
const help = wrapper.find(".ui-form-info");
|
||||
expect(help.text()).toBe("help_text");
|
||||
|
||||
const error = wrapper.find(".ui-form-error-text");
|
||||
expect(error.text()).toBe("error_text");
|
||||
|
||||
await wrapper.setProps({ error: "" });
|
||||
const no_error = wrapper.findAll(".ui-form-error");
|
||||
expect(no_error.length).toBe(0);
|
||||
|
||||
const title = wrapper.find(".ui-form-title");
|
||||
expect(title.text()).toBe("title_text");
|
||||
expect(title.text()).toContain("title_text");
|
||||
});
|
||||
|
||||
it("check collapsibles and other features", async () => {
|
||||
await wrapper.setProps({ disabled: true });
|
||||
expect(wrapper.findAll(".ui-form-field").length).toEqual(0);
|
||||
|
||||
await wrapper.setProps({ disabled: false });
|
||||
expect(wrapper.findAll(".ui-form-field").length).toEqual(1);
|
||||
await wrapper.setProps({ default_value: "default_value", collapsible_value: "collapsible_value" });
|
||||
|
||||
await wrapper.setProps({
|
||||
attributes: { default_value: "default_value", collapsible_value: "collapsible_value" },
|
||||
});
|
||||
expect(wrapper.find(".ui-form-title-text").text()).toEqual("title_text");
|
||||
expect(wrapper.findAll("span[title='Disable']").length).toEqual(1);
|
||||
expect(wrapper.findAll("button[title='Disable']").length).toEqual(1);
|
||||
expect(wrapper.emitted().input[0][0]).toEqual("initial_value");
|
||||
|
||||
await wrapper.find(".ui-form-collapsible-icon").trigger("click");
|
||||
expect(wrapper.emitted().input[1][0]).toEqual("collapsible_value");
|
||||
expect(wrapper.emitted().input[1][1]).toEqual("input");
|
||||
|
||||
await wrapper.setProps({
|
||||
collapsedEnableText: "Enable Collapsible",
|
||||
collapsedDisableText: "Disable Collapsible",
|
||||
});
|
||||
expect(wrapper.findAll("span[title='Enable Collapsible']").length).toEqual(1);
|
||||
expect(wrapper.findAll("span[title='Disable Collapsible']").length).toEqual(0);
|
||||
expect(wrapper.findAll("button[title='Enable Collapsible']").length).toEqual(1);
|
||||
expect(wrapper.findAll("button[title='Disable Collapsible']").length).toEqual(0);
|
||||
|
||||
await wrapper.find(".ui-form-collapsible-icon").trigger("click");
|
||||
expect(wrapper.emitted().input[2][0]).toEqual("default_value");
|
||||
expect(wrapper.findAll("span[title='Disable Collapsible']").length).toEqual(1);
|
||||
expect(wrapper.findAll("span[title='Enable Collapsible']").length).toEqual(0);
|
||||
expect(wrapper.findAll("button[title='Disable Collapsible']").length).toEqual(1);
|
||||
expect(wrapper.findAll("button[title='Enable Collapsible']").length).toEqual(0);
|
||||
});
|
||||
|
||||
it("check type matching", async () => {
|
||||
await wrapper.setProps({ type: "text" });
|
||||
expect(wrapper.find("div[id='input'").text()).toEqual("form-input");
|
||||
expect(wrapper.findComponent(FormInput).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(FormHidden).exists()).toBe(false);
|
||||
|
||||
await wrapper.setProps({ attributes: { titleonly: true } });
|
||||
expect(wrapper.find("div[id='input'").text()).toEqual("form-hidden");
|
||||
expect(wrapper.findComponent(FormHidden).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(FormInput).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("marks required values", async () => {
|
||||
await wrapper.setProps({ type: "text", attributes: { optional: false } });
|
||||
expect(wrapper.find(".ui-form-title-star").exists()).toBe(true);
|
||||
expect(wrapper.find(".ui-form-title-message").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("marks optional values", async () => {
|
||||
await wrapper.setProps({ type: "text", attributes: { optional: true } });
|
||||
expect(wrapper.find(".ui-form-title-star").exists()).toBe(false);
|
||||
expect(wrapper.find(".ui-form-title-message").text()).toContain("optional");
|
||||
});
|
||||
|
||||
it("warns about empty required values", async () => {
|
||||
await wrapper.setProps({ type: "text", value: "", attributes: { optional: false } });
|
||||
expect(wrapper.find(".ui-form-title-star").exists()).toBe(true);
|
||||
expect(wrapper.find(".ui-form-title-message").text()).toContain("required");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,270 +1,344 @@
|
||||
<script setup>
|
||||
import FormBoolean from "./Elements/FormBoolean";
|
||||
import FormHidden from "./Elements/FormHidden";
|
||||
import FormInput from "./Elements/FormInput";
|
||||
import FormParameter from "./Elements/FormParameter";
|
||||
import FormSelection from "./Elements/FormSelection";
|
||||
import FormColor from "./Elements/FormColor";
|
||||
import FormDirectory from "./Elements/FormDirectory";
|
||||
import FormNumber from "./Elements/FormNumber";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { ref, computed, useAttrs } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: "identifier",
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
refreshOnChange: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
backbonejs: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
attributes: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
collapsedEnableText: {
|
||||
type: String,
|
||||
default: "Enable",
|
||||
},
|
||||
collapsedDisableText: {
|
||||
type: String,
|
||||
default: "Disable",
|
||||
},
|
||||
collapsedEnableIcon: {
|
||||
type: String,
|
||||
default: "far fa-caret-square-down",
|
||||
},
|
||||
collapsedDisableIcon: {
|
||||
type: String,
|
||||
default: "far fa-caret-square-up",
|
||||
},
|
||||
connectedEnableText: {
|
||||
type: String,
|
||||
default: "Remove connection from module.",
|
||||
},
|
||||
connectedDisableText: {
|
||||
type: String,
|
||||
default: "Add connection to module.",
|
||||
},
|
||||
connectedEnableIcon: {
|
||||
type: String,
|
||||
default: "fa fa-times",
|
||||
},
|
||||
connectedDisableIcon: {
|
||||
type: String,
|
||||
default: "fa fa-arrows-alt-h",
|
||||
},
|
||||
workflowBuildingMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["input", "change"]);
|
||||
|
||||
/** TODO: remove attrs computed.
|
||||
useAttrs is *not* reactive, and does not play nice with type safety.
|
||||
It is present for compatibility with the legacy "FormParameter" component,
|
||||
but should be removed as soon as that component is removed.
|
||||
*/
|
||||
const attrs = computed(() => props.attributes || useAttrs());
|
||||
const collapsibleValue = computed(() => attrs.value["collapsible_value"]);
|
||||
const defaultValue = computed(() => attrs.value["default_value"]);
|
||||
const connectedValue = { __class__: "ConnectedValue" };
|
||||
|
||||
const connected = ref(false);
|
||||
const collapsed = ref(false);
|
||||
|
||||
const collapsible = computed(() => !props.disabled && collapsibleValue.value !== undefined);
|
||||
const connectable = computed(() => collapsible.value && attrs.value["connectable"]);
|
||||
|
||||
// Determines to wether expand or collapse the input
|
||||
{
|
||||
setValue(props.value);
|
||||
const valueJson = JSON.stringify(props.value);
|
||||
connected.value = valueJson === JSON.stringify(connectedValue);
|
||||
collapsed.value =
|
||||
connected.value ||
|
||||
(collapsibleValue.value !== undefined && valueJson === JSON.stringify(collapsibleValue.value));
|
||||
}
|
||||
|
||||
/** Submits a changed value. */
|
||||
function setValue(value) {
|
||||
emit("input", value, props.id);
|
||||
emit("change", props.refreshOnChange);
|
||||
}
|
||||
|
||||
/** Handles collapsible toggle. */
|
||||
function onCollapse() {
|
||||
collapsed.value = !collapsed.value;
|
||||
connected.value = false;
|
||||
if (collapsed.value) {
|
||||
setValue(collapsibleValue.value);
|
||||
} else {
|
||||
setValue(defaultValue.value);
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles connected state. */
|
||||
function onConnect() {
|
||||
connected.value = !connected.value;
|
||||
collapsed.value = connected.value;
|
||||
if (connected.value) {
|
||||
setValue(connectedValue);
|
||||
} else {
|
||||
setValue(defaultValue.value);
|
||||
}
|
||||
}
|
||||
|
||||
const isHidden = computed(() => attrs.value["hidden"]);
|
||||
const elementId = computed(() => `form-element-${props.id}`);
|
||||
const hasError = computed(() => Boolean(props.error));
|
||||
const showPreview = computed(() => (collapsed.value && attrs.value["collapsible_preview"]) || props.disabled);
|
||||
const showField = computed(() => !collapsed.value && !props.disabled);
|
||||
|
||||
const previewText = computed(() => attrs.value["text_value"]);
|
||||
const helpText = computed(() => {
|
||||
const helpArgument = attrs.value["argument"];
|
||||
if (helpArgument && !props.help.includes(`(${helpArgument})`)) {
|
||||
return `${props.help} (${helpArgument})`;
|
||||
} else {
|
||||
return props.help;
|
||||
}
|
||||
});
|
||||
|
||||
const currentValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
setValue(val);
|
||||
},
|
||||
});
|
||||
|
||||
const isHiddenType = computed(
|
||||
() => ["hidden", "hidden_data", "baseurl"].includes(props.type) || (props.attributes && props.attributes.titleonly)
|
||||
);
|
||||
|
||||
const collapseText = computed(() => (collapsed.value ? props.collapsedEnableText : props.collapsedDisableText));
|
||||
const connectText = computed(() => (connected.value ? props.connectedEnableText : props.connectedDisableText));
|
||||
|
||||
const isEmpty = computed(() => {
|
||||
if (currentValue.value === null || currentValue.value === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (["text", "integer", "float", "password"].includes(props.type) && currentValue.value === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const isRequired = computed(() => attrs.value["optional"] === false);
|
||||
const isRequiredType = computed(() => props.type !== "boolean");
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faExclamation, faTimes, faArrowsAltH } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretSquareDown, faCaretSquareUp } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
library.add(faExclamation, faTimes, faArrowsAltH, faCaretSquareDown, faCaretSquareUp);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="!isHidden" :id="elementId" :class="['ui-form-element section-row', cls]">
|
||||
<div
|
||||
v-show="!isHidden"
|
||||
:id="elementId"
|
||||
class="ui-form-element section-row"
|
||||
:class="{ alert: hasError, 'alert-info': hasError }">
|
||||
<div v-if="hasError" class="ui-form-error">
|
||||
<span class="fa fa-exclamation mr-1" />
|
||||
<span class="ui-form-error-text" v-html="error" />
|
||||
<FontAwesomeIcon class="mr-1" icon="fa-exclamation" />
|
||||
<span class="ui-form-error-text" v-html="props.error" />
|
||||
</div>
|
||||
|
||||
<div class="ui-form-title">
|
||||
<div v-if="collapsible || connectable">
|
||||
<span v-if="collapsible && !connected" class="ui-form-collapsible-icon icon" @click="onCollapse">
|
||||
<span v-if="collapsed" :class="collapsedEnableIcon" :title="collapsedEnableText" />
|
||||
<span v-else :class="collapsedDisableIcon" :title="collapsedDisableText" />
|
||||
<span v-if="collapsible || connectable">
|
||||
<b-button
|
||||
v-if="collapsible && !connected"
|
||||
class="ui-form-collapsible-icon"
|
||||
:title="collapseText"
|
||||
@click="onCollapse">
|
||||
<FontAwesomeIcon v-if="collapsed" :icon="props.collapsedEnableIcon" />
|
||||
<FontAwesomeIcon v-else :icon="props.collapsedDisableIcon" />
|
||||
</b-button>
|
||||
|
||||
<b-button v-if="connectable" class="ui-form-connected-icon" :title="connectText" @click="onConnect">
|
||||
<FontAwesomeIcon v-if="connected" :icon="props.connectedEnableIcon" />
|
||||
<FontAwesomeIcon v-else :icon="props.connectedDisableIcon" />
|
||||
</b-button>
|
||||
|
||||
<span v-if="props.title" class="ui-form-title-text ml-1">
|
||||
{{ props.title }}
|
||||
</span>
|
||||
<span v-if="connectable" class="ui-form-connected-icon icon" @click="onConnect">
|
||||
<span v-if="connected" :class="connectedEnableIcon" :title="connectedEnableText" />
|
||||
<span v-else :class="connectedDisableIcon" :title="connectedDisableText" />
|
||||
</span>
|
||||
<span class="ui-form-title-text ml-1">
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="ui-form-title-text">{{ title }}</span>
|
||||
</span>
|
||||
<span v-else-if="props.title" class="ui-form-title-text">{{ props.title }}</span>
|
||||
|
||||
<span
|
||||
v-if="isRequired && isRequiredType && props.title"
|
||||
v-b-tooltip.hover
|
||||
class="ui-form-title-star"
|
||||
title="required"
|
||||
:class="{ warning: isEmpty }">
|
||||
*
|
||||
<span v-if="isEmpty" class="ui-form-title-message warning"> required </span>
|
||||
</span>
|
||||
<span v-else-if="isRequiredType && props.title" class="ui-form-title-message"> - optional </span>
|
||||
</div>
|
||||
<div v-if="showField" class="ui-form-field" :data-label="title">
|
||||
<FormBoolean v-if="type == 'boolean'" :id="id" v-model="currentValue" />
|
||||
<FormHidden v-else-if="isHiddenType" :id="id" v-model="currentValue" :info="attrs['info']" />
|
||||
|
||||
<div v-if="showField" class="ui-form-field" :data-label="props.title">
|
||||
<FormBoolean v-if="props.type === 'boolean'" :id="props.id" v-model="currentValue" />
|
||||
<FormHidden v-else-if="isHiddenType" :id="props.id" v-model="currentValue" :info="attrs['info']" />
|
||||
<FormNumber
|
||||
v-else-if="type == 'integer' || type == 'float'"
|
||||
:id="id"
|
||||
v-else-if="props.type === 'integer' || props.type === 'float'"
|
||||
:id="props.id"
|
||||
v-model="currentValue"
|
||||
:max="attrs.max"
|
||||
:min="attrs.min"
|
||||
:type="type"
|
||||
:workflow-building-mode="workflowBuildingMode" />
|
||||
<FormColor v-else-if="type == 'color'" :id="id" v-model="currentValue" />
|
||||
<FormDirectory v-else-if="type == 'directory_uri'" v-model="currentValue" />
|
||||
<FormSelection
|
||||
v-else-if="props.type == 'select' && attrs.display == 'radio'"
|
||||
:id="id"
|
||||
v-model="currentValue"
|
||||
:data="attrs.data"
|
||||
:display="attrs.display"
|
||||
:options="attrs.options"
|
||||
:optional="attrs.optional"
|
||||
:multiple="attrs.multiple" />
|
||||
<FormColor v-else-if="props.type === 'color'" :id="props.id" v-model="currentValue" />
|
||||
<FormDirectory v-else-if="props.type === 'directory_uri'" v-model="currentValue" />
|
||||
<FormParameter
|
||||
v-else-if="backbonejs"
|
||||
:id="id"
|
||||
ref="params"
|
||||
:id="props.id"
|
||||
v-model="currentValue"
|
||||
:data-label="title"
|
||||
:type="type"
|
||||
:data-label="props.title"
|
||||
:type="props.type"
|
||||
:attributes="attrs" />
|
||||
<FormInput v-else :id="id" v-model="currentValue" :area="attrs['area']" />
|
||||
<FormInput v-else :id="props.id" v-model="currentValue" :area="attrs['area']" />
|
||||
</div>
|
||||
<div v-if="showPreview" class="ui-form-preview" v-html="previewText" />
|
||||
<span v-if="!!helpText" class="ui-form-info form-text text-muted" v-html="helpText" />
|
||||
|
||||
<div v-if="showPreview" class="ui-form-preview pt-1 pl-2 mt-1">{{ previewText }}</div>
|
||||
<span v-if="Boolean(helpText)" class="ui-form-info form-text text-muted" v-html="helpText" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "underscore";
|
||||
import FormBoolean from "./Elements/FormBoolean";
|
||||
import FormHidden from "./Elements/FormHidden";
|
||||
import FormInput from "./Elements/FormInput";
|
||||
import FormParameter from "./Elements/FormParameter";
|
||||
import FormColor from "./Elements/FormColor";
|
||||
import FormDirectory from "./Elements/FormDirectory";
|
||||
import FormNumber from "./Elements/FormNumber";
|
||||
<style lang="scss" scoped>
|
||||
@import "theme/blue.scss";
|
||||
@import "~@fortawesome/fontawesome-free/scss/_variables";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormBoolean,
|
||||
FormHidden,
|
||||
FormInput,
|
||||
FormNumber,
|
||||
FormColor,
|
||||
FormParameter,
|
||||
FormDirectory,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: "identifer",
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
value: {
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
refreshOnChange: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
backbonejs: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
attributes: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
collapsedEnableText: {
|
||||
type: String,
|
||||
default: "Enable",
|
||||
},
|
||||
collapsedDisableText: {
|
||||
type: String,
|
||||
default: "Disable",
|
||||
},
|
||||
collapsedEnableIcon: {
|
||||
type: String,
|
||||
default: "fa fa-caret-square-o-down",
|
||||
},
|
||||
collapsedDisableIcon: {
|
||||
type: String,
|
||||
default: "fa fa-caret-square-o-up",
|
||||
},
|
||||
connectedEnableText: {
|
||||
type: String,
|
||||
default: "Remove connection from module.",
|
||||
},
|
||||
connectedDisableText: {
|
||||
type: String,
|
||||
default: "Add connection to module.",
|
||||
},
|
||||
connectedEnableIcon: {
|
||||
type: String,
|
||||
default: "fa fa fa-times",
|
||||
},
|
||||
connectedDisableIcon: {
|
||||
type: String,
|
||||
default: "fa fa-arrows-h",
|
||||
},
|
||||
workflowBuildingMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collapsed: false,
|
||||
connected: false,
|
||||
connectedValue: { __class__: "ConnectedValue" },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
elementId() {
|
||||
return `form-element-${this.id}`;
|
||||
},
|
||||
argument() {
|
||||
return this.attrs["argument"];
|
||||
},
|
||||
attrs() {
|
||||
return this.attributes || this.$attrs;
|
||||
},
|
||||
cls() {
|
||||
return this.hasError && "alert alert-info";
|
||||
},
|
||||
collapsible() {
|
||||
return !this.disabled && this.collapsibleValue !== undefined;
|
||||
},
|
||||
collapsibleValue() {
|
||||
return this.attrs["collapsible_value"];
|
||||
},
|
||||
collapsiblePreview() {
|
||||
return this.attrs["collapsible_preview"];
|
||||
},
|
||||
connectable() {
|
||||
return this.collapsible && this.attrs["connectable"];
|
||||
},
|
||||
currentValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.setValue(val);
|
||||
},
|
||||
},
|
||||
defaultValue() {
|
||||
return this.attrs["default_value"];
|
||||
},
|
||||
hasError() {
|
||||
return !!this.error;
|
||||
},
|
||||
helpText() {
|
||||
const help = this.help;
|
||||
const helpArgument = this.argument;
|
||||
if (helpArgument && help.indexOf(`(${helpArgument})`) == -1) {
|
||||
return `${help} (${helpArgument})`;
|
||||
}
|
||||
return help;
|
||||
},
|
||||
isHidden() {
|
||||
return this.attrs["hidden"];
|
||||
},
|
||||
isHiddenType() {
|
||||
return (
|
||||
["hidden", "hidden_data", "baseurl"].includes(this.type) ||
|
||||
(this.attributes && this.attributes.titleonly)
|
||||
);
|
||||
},
|
||||
previewText() {
|
||||
return _.escape(this.textValue).replace(/\n/g, "<br>");
|
||||
},
|
||||
showField() {
|
||||
return !this.collapsed && !this.disabled;
|
||||
},
|
||||
showPreview() {
|
||||
return (this.collapsed && this.collapsiblePreview) || this.disabled;
|
||||
},
|
||||
textValue() {
|
||||
return this.attrs["text_value"];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.initialState();
|
||||
},
|
||||
methods: {
|
||||
/** Submits a changed value. */
|
||||
setValue(value) {
|
||||
this.$emit("input", value, this.id);
|
||||
this.$emit("change", this.refreshOnChange);
|
||||
},
|
||||
/**
|
||||
* Determines to wether expand or collapse the input.
|
||||
*/
|
||||
initialState() {
|
||||
this.setValue(this.value);
|
||||
const collapsibleValue = this.collapsibleValue;
|
||||
const value = JSON.stringify(this.value);
|
||||
this.connected = value == JSON.stringify(this.connectedValue);
|
||||
this.collapsed =
|
||||
this.connected || (collapsibleValue !== undefined && value == JSON.stringify(collapsibleValue));
|
||||
},
|
||||
/**
|
||||
* Handles collapsible toggle.
|
||||
*/
|
||||
onCollapse() {
|
||||
this.collapsed = !this.collapsed;
|
||||
this.connected = false;
|
||||
if (this.collapsed) {
|
||||
this.setValue(this.collapsibleValue);
|
||||
} else {
|
||||
this.setValue(this.defaultValue);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Handles connected state.
|
||||
*/
|
||||
onConnect() {
|
||||
this.connected = !this.connected;
|
||||
this.collapsed = this.connected;
|
||||
if (this.connected) {
|
||||
this.setValue(this.connectedValue);
|
||||
} else {
|
||||
this.setValue(this.defaultValue);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
.ui-form-element {
|
||||
margin-top: $margin-v * 0.25;
|
||||
margin-bottom: $margin-v * 0.25;
|
||||
overflow: visible;
|
||||
clear: both;
|
||||
|
||||
.ui-form-title {
|
||||
word-wrap: break-word;
|
||||
font-weight: bold;
|
||||
|
||||
.ui-form-title-message {
|
||||
font-size: $font-size-base * 0.7;
|
||||
font-weight: 300;
|
||||
vertical-align: text-top;
|
||||
color: $text-light;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ui-form-title-star {
|
||||
color: $text-light;
|
||||
font-weight: 300;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: $brand-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-form-field {
|
||||
position: relative;
|
||||
margin-top: $margin-v * 0.25;
|
||||
}
|
||||
|
||||
&:deep(.ui-form-collapsible-icon),
|
||||
&:deep(.ui-form-connected-icon) {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
font-size: 1.2em;
|
||||
|
||||
&:hover {
|
||||
color: $brand-info;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(input, index) in inputs" :key="index">
|
||||
<div v-if="input.type == 'conditional'">
|
||||
<FormElement
|
||||
:id="conditionalPrefix(input, input.test_param.name)"
|
||||
v-model="input.test_param.value"
|
||||
:title="input.test_param.label"
|
||||
:type="input.test_param.type"
|
||||
:help="input.test_param.help"
|
||||
:refresh-on-change="false"
|
||||
:disabled="sustainConditionals"
|
||||
:attributes="input.test_param"
|
||||
:backbonejs="true"
|
||||
@change="onChange" />
|
||||
<div v-for="(caseDetails, caseId) in input.cases" :key="caseId">
|
||||
<FormNode
|
||||
v-if="conditionalMatch(input, caseId)"
|
||||
v-bind="$props"
|
||||
:inputs="caseDetails.inputs"
|
||||
:prefix="getPrefix(input.name)" />
|
||||
<div v-if="input.type == 'conditional'" class="ui-portlet-section mt-3">
|
||||
<div class="portlet-header">
|
||||
<b>{{ input.test_param.label }}</b>
|
||||
</div>
|
||||
<div class="portlet-content">
|
||||
<FormElement
|
||||
:id="conditionalPrefix(input, input.test_param.name)"
|
||||
v-model="input.test_param.value"
|
||||
:type="input.test_param.type"
|
||||
:help="input.test_param.help"
|
||||
:refresh-on-change="false"
|
||||
:disabled="sustainConditionals"
|
||||
:attributes="input.test_param"
|
||||
:backbonejs="true"
|
||||
@change="onChange" />
|
||||
<div v-for="(caseDetails, caseId) in input.cases" :key="caseId">
|
||||
<FormNode
|
||||
v-if="conditionalMatch(input, caseId)"
|
||||
v-bind="$props"
|
||||
:inputs="caseDetails.inputs"
|
||||
:prefix="getPrefix(input.name)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="input.type == 'repeat'">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@save="$emit('update:dsc', $event)">
|
||||
<template v-slot:name>
|
||||
<!-- eslint-disable-next-line vuejs-accessibility/heading-has-content -->
|
||||
<h2 class="h-md" v-short="dsc.name || 'Collection'" data-description="collection name display" />
|
||||
<h2 v-short="dsc.name || 'Collection'" class="h-md" data-description="collection name display" />
|
||||
<CollectionDescription
|
||||
:job-state-summary="jobState"
|
||||
:collection-type="dsc.collection_type"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
class="rounded-0 text-decoration-none"
|
||||
size="sm"
|
||||
variant="link"
|
||||
:href="downloadUrl"
|
||||
@click="onDownload">
|
||||
<Icon class="mr-1" icon="download" />
|
||||
<span>Download</span>
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { mapActions } from "vuex";
|
||||
import { HistoryItemsProvider } from "components/providers/storeProviders";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Published :details="history">
|
||||
<Published :item="history">
|
||||
<template v-slot>
|
||||
<HistoryView :id="id" />
|
||||
</template>
|
||||
|
||||
@@ -67,15 +67,15 @@ export default {
|
||||
selectedCollections: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch("history/loadHistoryById", this.id);
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ getHistoryById: "history/getHistoryById" }),
|
||||
history() {
|
||||
return this.getHistoryById(this.id);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch("history/loadHistoryById", this.id);
|
||||
},
|
||||
methods: {
|
||||
onViewCollection(collection) {
|
||||
this.selectedCollections = [...this.selectedCollections, collection];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="m-3 details" data-description="edit details">
|
||||
<b-button
|
||||
v-if="writeable && !currentUser.isAnonymous"
|
||||
:disabled="currentUser.isAnonymous || !writeable"
|
||||
class="edit-button ml-1 float-right"
|
||||
data-description="editor toggle"
|
||||
size="sm"
|
||||
|
||||
@@ -189,7 +189,7 @@ import { getAppRoot } from "onload/loadConfig";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { Services } from "./services";
|
||||
import { fields } from "./table-fields";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { initLibrariesIcons } from "components/Libraries/icons";
|
||||
import { MAX_DESCRIPTION_LENGTH, DEFAULT_PER_PAGE, onError } from "components/Libraries/library-utils";
|
||||
import LibraryEditField from "components/Libraries/LibraryEditField";
|
||||
|
||||
@@ -276,7 +276,7 @@ import { Services } from "./services";
|
||||
import Utils from "utils/utils";
|
||||
import linkifyHtml from "linkify-html";
|
||||
import { fields } from "./table-fields";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import FolderTopBar from "./TopToolbar/FolderTopBar";
|
||||
import { initFolderTableIcons } from "components/Libraries/icons";
|
||||
import { MAX_DESCRIPTION_LENGTH, DEFAULT_PER_PAGE } from "components/Libraries/library-utils";
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ import { Services } from "components/Libraries/LibraryFolder/services";
|
||||
import LibraryBreadcrumb from "components/Libraries/LibraryFolder/LibraryBreadcrumb";
|
||||
import download from "components/Libraries/LibraryFolder/TopToolbar/download";
|
||||
import CopyToClipboard from "components/CopyToClipboard";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { fieldTitles } from "components/Libraries/LibraryFolder/LibraryFolderDataset/constants";
|
||||
import { DbKeyProvider, DatatypesProvider } from "components/providers";
|
||||
import SingleItemSelector from "components/SingleItemSelector";
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ import Vue from "vue";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { Services } from "components/Libraries/LibraryPermissions/services";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import PermissionsHeader from "components/Libraries/LibraryPermissions/PermissionsHeader";
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ import Vue from "vue";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { Services } from "components/Libraries/LibraryPermissions/services";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import PermissionsInputField from "components/Libraries/LibraryPermissions/PermissionsInputField";
|
||||
import PermissionsHeader from "components/Libraries/LibraryPermissions/PermissionsHeader";
|
||||
|
||||
@@ -125,7 +125,7 @@ import { initTopBarIcons } from "components/Libraries/icons";
|
||||
import mod_import_dataset from "./import-to-history/import-dataset";
|
||||
import mod_import_collection from "./import-to-history/import-collection";
|
||||
import mod_add_datasets from "./add-datasets";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import download from "./download";
|
||||
import mod_utils from "utils/utils";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import _l from "utils/localization";
|
||||
import mod_library_model from "./library-model";
|
||||
import _ from "underscore";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import _l from "utils/localization";
|
||||
import mod_library_model from "./library-model";
|
||||
import _ from "underscore";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import $ from "jquery";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { getAppRoot } from "onload";
|
||||
|
||||
function processDownload(url, data, method) {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import mod_library_model from "../library-model";
|
||||
import _ from "underscore";
|
||||
import Backbone from "backbone";
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import _l from "utils/localization";
|
||||
import mod_library_model from "../library-model";
|
||||
import _ from "underscore";
|
||||
|
||||
@@ -43,7 +43,7 @@ import Vue from "vue";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { Services } from "components/Libraries/LibraryPermissions/services";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import PermissionsHeader from "components/Libraries/LibraryPermissions/PermissionsHeader";
|
||||
import { extractRoles } from "components/Libraries/library-utils";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
|
||||
export const MAX_DESCRIPTION_LENGTH = 40;
|
||||
export const DEFAULT_PER_PAGE = 10;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { safePath } from "utils/redirect";
|
||||
import MountTarget from "./ChangePassword";
|
||||
|
||||
// mock routes
|
||||
jest.mock("utils/redirect");
|
||||
const mockSafePath = jest.fn();
|
||||
safePath.mockImplementation(() => mockSafePath);
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("ChangePassword", () => {
|
||||
let wrapper;
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
wrapper = mount(MountTarget, {
|
||||
propsData: {
|
||||
messageText: "message_text",
|
||||
messageVariant: "message_variant",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it("basics", async () => {
|
||||
const cardHeader = wrapper.find(".card-header");
|
||||
expect(cardHeader.text()).toBe("Change your password");
|
||||
const inputs = wrapper.findAll("input");
|
||||
expect(inputs.length).toBe(2);
|
||||
const firstPwdField = inputs.at(0);
|
||||
expect(firstPwdField.attributes("type")).toBe("password");
|
||||
await firstPwdField.setValue("test_first_pwd");
|
||||
const secondPwdField = inputs.at(1);
|
||||
expect(secondPwdField.attributes("type")).toBe("password");
|
||||
await secondPwdField.setValue("test_second_pwd");
|
||||
const submitButton = wrapper.find("button[type='submit']");
|
||||
await submitButton.trigger("submit");
|
||||
const postedData = JSON.parse(axiosMock.history.post[0].data);
|
||||
expect(postedData.password).toBe("test_first_pwd");
|
||||
expect(postedData.confirm).toBe("test_second_pwd");
|
||||
});
|
||||
|
||||
it("props", async () => {
|
||||
await wrapper.setProps({
|
||||
token: "test_token",
|
||||
expiredUser: "expired_user",
|
||||
});
|
||||
const input = wrapper.find("input");
|
||||
expect(input.attributes("type")).toBe("password");
|
||||
await input.setValue("current_password");
|
||||
const submitButton = wrapper.find("button[type='submit']");
|
||||
await submitButton.trigger("submit");
|
||||
const postedData = JSON.parse(axiosMock.history.post[0].data);
|
||||
expect(postedData.token).toBe("test_token");
|
||||
expect(postedData.id).toBe("expired_user");
|
||||
expect(postedData.current).toBe("current_password");
|
||||
const alert = wrapper.find(".alert");
|
||||
expect(alert.text()).toBe("message_text");
|
||||
});
|
||||
});
|
||||
+31
-24
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<b-form @submit.prevent="submit">
|
||||
<b-alert :show="messageShow" :variant="messageVariant">
|
||||
{{ messageText }}
|
||||
<b-alert v-if="!!message" :variant="variant" show>
|
||||
{{ message }}
|
||||
</b-alert>
|
||||
<b-card header="Change your password">
|
||||
<b-form-group v-if="user" label="Current Password">
|
||||
<b-form-group v-if="expiredUser" label="Current Password">
|
||||
<b-form-input v-model="current" type="password" />
|
||||
</b-form-group>
|
||||
<b-form-group label="New Password"> <b-form-input v-model="password" type="password" /> </b-form-group>
|
||||
@@ -17,48 +17,55 @@
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload";
|
||||
import { safePath } from "utils/redirect";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
token: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
expiredUser: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
messageText: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
messageVariant: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
return {
|
||||
token: Galaxy.params.token,
|
||||
user: Galaxy.params.expired_user,
|
||||
password: null,
|
||||
confirm: null,
|
||||
current: null,
|
||||
messageText: Galaxy.params.message,
|
||||
messageVariant: Galaxy.params.status,
|
||||
message: this.messageText,
|
||||
variant: this.messageVariant,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageShow() {
|
||||
return this.messageText != null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submit: function (ev) {
|
||||
const urlRoot = getAppRoot();
|
||||
ev.preventDefault();
|
||||
submit() {
|
||||
axios
|
||||
.post(`${urlRoot}user/change_password`, {
|
||||
.post(safePath("/user/change_password"), {
|
||||
token: this.token,
|
||||
id: this.user,
|
||||
id: this.expiredUser,
|
||||
current: this.current,
|
||||
password: this.password,
|
||||
confirm: this.confirm,
|
||||
})
|
||||
.then((response) => {
|
||||
window.location = `${urlRoot}`;
|
||||
window.location = safePath(`/`);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.messageVariant = "danger";
|
||||
const message = error.response.data && error.response.data.err_msg;
|
||||
this.messageText = message || "Password change failed for an unknown reason.";
|
||||
this.variant = "danger";
|
||||
const message = error.response && error.response.data && error.response.data.err_msg;
|
||||
this.message = message || "Password change failed for an unknown reason.";
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -0,0 +1,62 @@
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import MountTarget from "./LoginForm";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("LoginForm", () => {
|
||||
let wrapper;
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
wrapper = mount(MountTarget, {
|
||||
propsData: {
|
||||
sessionCsrfToken: "sessionCsrfToken",
|
||||
},
|
||||
localVue,
|
||||
stubs: {
|
||||
ExternalLogin: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it("basics", async () => {
|
||||
const cardHeader = wrapper.find(".card-header");
|
||||
expect(cardHeader.text()).toBe("Welcome to Galaxy, please log in");
|
||||
const inputs = wrapper.findAll("input");
|
||||
expect(inputs.length).toBe(2);
|
||||
const usernameField = inputs.at(0);
|
||||
expect(usernameField.attributes("type")).toBe("text");
|
||||
await usernameField.setValue("test_user");
|
||||
const pwdField = inputs.at(1);
|
||||
expect(pwdField.attributes("type")).toBe("password");
|
||||
await pwdField.setValue("test_pwd");
|
||||
const submitButton = wrapper.find("button[type='submit']");
|
||||
await submitButton.trigger("submit");
|
||||
const postedData = JSON.parse(axiosMock.history.post[0].data);
|
||||
expect(postedData.login).toBe("test_user");
|
||||
expect(postedData.password).toBe("test_pwd");
|
||||
});
|
||||
|
||||
it("props", async () => {
|
||||
const $register = "[id='register-toggle']";
|
||||
expect(wrapper.findAll($register).length).toBe(0);
|
||||
await wrapper.setProps({
|
||||
allowUserCreation: true,
|
||||
enableOidc: true,
|
||||
showWelcomeWithLogin: true,
|
||||
welcomeUrl: "welcome_url",
|
||||
});
|
||||
const register = wrapper.find($register);
|
||||
expect(register.text()).toBe("Register here.");
|
||||
const welcomePage = wrapper.find("iframe");
|
||||
expect(welcomePage.attributes("src")).toBe("welcome_url");
|
||||
});
|
||||
});
|
||||
+50
-27
@@ -3,10 +3,10 @@
|
||||
<div class="row justify-content-md-center">
|
||||
<template v-if="!confirmURL">
|
||||
<div class="col col-lg-6">
|
||||
<b-alert :show="messageShow" :variant="messageVariant">
|
||||
<b-alert :show="!!messageText" :variant="messageVariant">
|
||||
{{ messageText }}
|
||||
</b-alert>
|
||||
<b-form id="login" @submit.prevent="submitGalaxyLogin()">
|
||||
<b-form id="login" @submit.prevent="submitLogin()">
|
||||
<b-card no-body header="Welcome to Galaxy, please log in">
|
||||
<b-card-body>
|
||||
<div>
|
||||
@@ -18,14 +18,14 @@
|
||||
<b-form-input v-model="password" name="password" type="password" />
|
||||
<b-form-text>
|
||||
Forgot password?
|
||||
<a href="javascript:void(0)" role="button" @click="reset">
|
||||
<a href="javascript:void(0)" role="button" @click.prevent="resetLogin">
|
||||
Click here to reset your password.
|
||||
</a>
|
||||
</b-form-text>
|
||||
</b-form-group>
|
||||
<b-button name="login" type="submit">Login</b-button>
|
||||
</div>
|
||||
<div v-if="enable_oidc">
|
||||
<div v-if="enableOidc">
|
||||
<!-- OIDC login-->
|
||||
<external-login :login_page="true" />
|
||||
</div>
|
||||
@@ -62,9 +62,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<new-user-confirmation :redirect="redirect" @setRedirect="setRedirect" />
|
||||
<new-user-confirmation
|
||||
:registration-warning-message="registrationWarningMessage"
|
||||
:terms-url="termsUrl"
|
||||
@setRedirect="setRedirect" />
|
||||
</template>
|
||||
<div v-if="show_welcome_with_login" class="col">
|
||||
<div v-if="showWelcomeWithLogin" class="col">
|
||||
<b-embed type="iframe" :src="welcomeUrlWithRoot" aspect="1by1" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,8 +79,7 @@ import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { safePath } from "utils/redirect";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import NewUserConfirmation from "components/login/NewUserConfirmation";
|
||||
import NewUserConfirmation from "./NewUserConfirmation";
|
||||
import ExternalLogin from "components/User/ExternalIdentities/ExternalLogin";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
@@ -88,51 +90,73 @@ export default {
|
||||
NewUserConfirmation,
|
||||
},
|
||||
props: {
|
||||
show_welcome_with_login: {
|
||||
allowUserCreation: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
welcome_url: {
|
||||
enableOidc: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
redirect: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
registrationWarningMessage: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
sessionCsrfToken: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showWelcomeWithLogin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
termsUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
welcomeUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const galaxy = getGalaxyInstance();
|
||||
return {
|
||||
login: null,
|
||||
password: null,
|
||||
url: null,
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
allowUserCreation: galaxy.config.allow_user_creation,
|
||||
redirect: galaxy.params.redirect,
|
||||
session_csrf_token: galaxy.session_csrf_token,
|
||||
enable_oidc: galaxy.config.enable_oidc,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageShow() {
|
||||
return this.messageText != null;
|
||||
},
|
||||
confirmURL() {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.has("confirm") && urlParams.get("confirm") == "true";
|
||||
},
|
||||
welcomeUrlWithRoot() {
|
||||
return safePath(this.welcome_url);
|
||||
return safePath(this.welcomeUrl);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleLogin() {
|
||||
this.$emit("toggle-login");
|
||||
},
|
||||
submitGalaxyLogin(method) {
|
||||
submitLogin() {
|
||||
let redirect = this.redirect;
|
||||
if (localStorage.getItem("redirect_url")) {
|
||||
this.redirect = localStorage.getItem("redirect_url");
|
||||
redirect = localStorage.getItem("redirect_url");
|
||||
}
|
||||
axios
|
||||
.post(safePath(`/user/login`), this.$data)
|
||||
.post(safePath("/user/login"), {
|
||||
login: this.login,
|
||||
password: this.password,
|
||||
redirect: redirect,
|
||||
session_csrf_token: this.sessionCsrfToken,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.message && data.status) {
|
||||
alert(data.message);
|
||||
@@ -154,10 +178,9 @@ export default {
|
||||
setRedirect(url) {
|
||||
localStorage.setItem("redirect_url", url);
|
||||
},
|
||||
reset(ev) {
|
||||
ev.preventDefault();
|
||||
resetLogin() {
|
||||
axios
|
||||
.post(safePath(`/user/reset_password`), { email: this.login })
|
||||
.post(safePath("/user/reset_password"), { email: this.login })
|
||||
.then((response) => {
|
||||
this.messageVariant = "info";
|
||||
this.messageText = response.data.message;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import MountTarget from "./LoginIndex";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("LoginIndex", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(MountTarget, {
|
||||
propsData: {
|
||||
allowUserCreation: false,
|
||||
sessionCsrfToken: "sessionCsrfToken",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
it("switching between register and login", async () => {
|
||||
const cardHeader = wrapper.find(".card-header");
|
||||
expect(cardHeader.text()).toBe("Welcome to Galaxy, please log in");
|
||||
const $registerToggle = "[id=register-toggle]";
|
||||
const missingToggle = wrapper.find($registerToggle);
|
||||
expect(missingToggle.exists()).toBeFalsy();
|
||||
await wrapper.setProps({ allowUserCreation: true });
|
||||
const registerToggle = wrapper.find($registerToggle);
|
||||
expect(registerToggle.exists()).toBeTruthy();
|
||||
await registerToggle.trigger("click");
|
||||
const newCardHeader = wrapper.find(".card-header");
|
||||
expect(newCardHeader.text()).toBe("Create a Galaxy account");
|
||||
const $loginToggle = "[id=login-toggle]";
|
||||
const loginToggle = wrapper.find($loginToggle);
|
||||
await loginToggle.trigger("click");
|
||||
const oldCardHeader = wrapper.find(".card-header");
|
||||
expect(oldCardHeader.text()).toBe("Welcome to Galaxy, please log in");
|
||||
expect(wrapper.vm.showLoginLink).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div>
|
||||
<login-form
|
||||
v-if="showLogin"
|
||||
:allow-user-creation="allowUserCreation"
|
||||
:enable-oidc="enableOidc"
|
||||
:redirect="redirect"
|
||||
:registration-warning-message="registrationWarningMessage"
|
||||
:session-csrf-token="sessionCsrfToken"
|
||||
:show-welcome-with-login="showWelcomeWithLogin"
|
||||
:terms-url="termsUrl"
|
||||
:welcome-url="welcomeUrl"
|
||||
@toggle-login="toggleLogin" />
|
||||
<register-form
|
||||
v-else
|
||||
:enable-oidc="enableOidc"
|
||||
:mailing-join-addr="mailingJoinAddr"
|
||||
:prefer-custos-login="preferCustosLogin"
|
||||
:registration-warning-message="registrationWarningMessage"
|
||||
:show-login-link="showLoginLink"
|
||||
:server-mail-configured="serverMailConfigured"
|
||||
:session-csrf-token="sessionCsrfToken"
|
||||
:terms-url="termsUrl"
|
||||
@toggle-login="toggleLogin" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import LoginForm from "./LoginForm";
|
||||
import RegisterForm from "./RegisterForm";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoginForm,
|
||||
RegisterForm,
|
||||
},
|
||||
props: {
|
||||
allowUserCreation: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
enableOidc: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
mailingJoinAddr: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
preferCustosLogin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
redirect: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
registrationWarningMessage: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
showLoginLink: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
sessionCsrfToken: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
serverMailConfigured: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showWelcomeWithLogin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
termsUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
welcomeUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showLogin: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleLogin() {
|
||||
this.showLogin = !this.showLogin;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import MountTarget from "./NewUserConfirmation";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("NewUserConfirmation", () => {
|
||||
let wrapper;
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
wrapper = mount(MountTarget, {
|
||||
propsData: {},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it("basics", async () => {
|
||||
const cardHeader = wrapper.find(".card-header");
|
||||
expect(cardHeader.text()).toBe("Confirm new account creation");
|
||||
const inputs = wrapper.findAll("input");
|
||||
expect(inputs.length).toBe(1);
|
||||
const checkField = inputs.at(0);
|
||||
expect(checkField.attributes("type")).toBe("checkbox");
|
||||
const submitButton = wrapper.find("button[name='confirm']");
|
||||
await submitButton.trigger("click");
|
||||
expect(axiosMock.history.post.length).toBe(0);
|
||||
await checkField.setChecked();
|
||||
await submitButton.trigger("click");
|
||||
const postedData = axiosMock.history.post[0];
|
||||
expect(postedData.url).toBe("/authnz/custos/create_user?token=null");
|
||||
await wrapper.setProps({ registrationWarningMessage: "registration warning message" });
|
||||
const alert = wrapper.find(".alert");
|
||||
expect(alert.text()).toBe("registration warning message");
|
||||
await wrapper.setProps({ termsUrl: "terms_url" });
|
||||
const termsFrame = wrapper.find("iframe");
|
||||
expect(termsFrame.attributes("src")).toBe("terms_url");
|
||||
const $toggle = "a[id=login-toggle]";
|
||||
const loginToggle = wrapper.find($toggle);
|
||||
expect(loginToggle.text()).toBe("Log in here.");
|
||||
});
|
||||
});
|
||||
+21
-38
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col" :class="{ 'col-lg-6': !isAdmin }">
|
||||
<b-alert :show="showRegistrationWarning" variant="info">
|
||||
{{ registration_warning_message }}
|
||||
<div class="col col-lg-6">
|
||||
<b-alert :show="!!registrationWarningMessage" variant="info">
|
||||
{{ registrationWarningMessage }}
|
||||
</b-alert>
|
||||
<b-alert :show="messageShow" :variant="messageVariant">
|
||||
<b-alert :show="!!messageText" :variant="messageVariant">
|
||||
{{ messageText }}
|
||||
</b-alert>
|
||||
<b-form id="confirmation" @submit.prevent="submit()">
|
||||
@@ -18,9 +18,9 @@
|
||||
connect this account via <strong>User Preferences</strong>.
|
||||
<a
|
||||
href="https://galaxyproject.org/authnz/use/oidc/idps/custos/#link-an-existing-galaxy-account"
|
||||
target="_blank"
|
||||
>More details here.</a
|
||||
>
|
||||
target="_blank">
|
||||
More details here.
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
If you wish to continue and create a new account, select
|
||||
@@ -31,27 +31,26 @@
|
||||
subject to termination and data deletion on public Galaxy servers. Connect existing
|
||||
account now to continue to use your existing data and avoid possible loss of data.
|
||||
</p>
|
||||
|
||||
<b-form-group>
|
||||
<b-form-checkbox v-model="termsRead">
|
||||
I have read and accept these terms to create a new Galaxy account.
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
<b-button name="confirm" type="submit" :disabled="!termsRead" @click.prevent="submit"
|
||||
>Yes, create new account</b-button
|
||||
>
|
||||
<b-button name="confirm" type="submit" :disabled="!termsRead" @click.prevent="submit">
|
||||
Yes, create new account
|
||||
</b-button>
|
||||
<b-button name="cancel" type="submit" @click.prevent="login">No, go back to login</b-button>
|
||||
</b-card-body>
|
||||
<b-card-footer v-if="!isAdmin">
|
||||
<b-card-footer>
|
||||
Already have an account?
|
||||
<a id="login-toggle" href="javascript:void(0)" role="button" @click.prevent="login"
|
||||
>Log in here.</a
|
||||
>
|
||||
<a id="login-toggle" href="javascript:void(0)" role="button" @click.prevent="login">
|
||||
Log in here.
|
||||
</a>
|
||||
</b-card-footer>
|
||||
</b-card>
|
||||
</b-form>
|
||||
</div>
|
||||
<div v-if="terms_url" class="col">
|
||||
<div v-if="termsUrl" class="col">
|
||||
<b-embed type="iframe" :src="termsUrlwithRoot" aspect="1by1" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,53 +60,37 @@
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { safePath } from "utils/redirect";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
registration_warning_message: {
|
||||
registrationWarningMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
redirect: {
|
||||
termsUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
terms_url: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const galaxy = getGalaxyInstance();
|
||||
return {
|
||||
username: null,
|
||||
subscribe: null,
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
session_csrf_token: galaxy.session_csrf_token,
|
||||
isAdmin: galaxy.user.isAdmin(),
|
||||
termsRead: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageShow() {
|
||||
return this.messageText != null;
|
||||
},
|
||||
showRegistrationWarning() {
|
||||
return this.registration_warning_message != null;
|
||||
},
|
||||
termsUrlwithRoot() {
|
||||
return safePath(this.terms_url);
|
||||
return safePath(this.termsUrl);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
login() {
|
||||
// set url to redirect user to 3rd party management after login
|
||||
this.$emit("setRedirect", "user/external_ids");
|
||||
this.$emit("setRedirect", "/user/external_ids");
|
||||
window.location = safePath("/login");
|
||||
},
|
||||
submit() {
|
||||
@@ -0,0 +1,44 @@
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import MountTarget from "./RegisterForm";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("RegisterForm", () => {
|
||||
let wrapper;
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
wrapper = mount(MountTarget, {
|
||||
propsData: {
|
||||
sessionCsrfToken: "sessionCsrfToken",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it("basics", async () => {
|
||||
const cardHeader = wrapper.find(".card-header");
|
||||
expect(cardHeader.text()).toBe("Create a Galaxy account");
|
||||
const inputs = wrapper.findAll("input");
|
||||
expect(inputs.length).toBe(4);
|
||||
const usernameField = inputs.at(0);
|
||||
expect(usernameField.attributes("type")).toBe("text");
|
||||
await usernameField.setValue("test_user");
|
||||
const pwdField = inputs.at(1);
|
||||
expect(pwdField.attributes("type")).toBe("password");
|
||||
await pwdField.setValue("test_pwd");
|
||||
const submitButton = wrapper.find("button[type='submit']");
|
||||
await submitButton.trigger("submit");
|
||||
const postedData = JSON.parse(axiosMock.history.post[0].data);
|
||||
expect(postedData.email).toBe("test_user");
|
||||
expect(postedData.password).toBe("test_pwd");
|
||||
});
|
||||
});
|
||||
+55
-45
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col" :class="{ 'col-lg-6': !isAdmin }">
|
||||
<b-alert :show="showRegistrationWarning" variant="info">
|
||||
{{ registration_warning_message }}
|
||||
<div class="col col-lg-6">
|
||||
<b-alert :show="!!registrationWarningMessage" variant="info">
|
||||
{{ registrationWarningMessage }}
|
||||
</b-alert>
|
||||
<b-alert :show="messageShow" :variant="messageVariant">
|
||||
<b-alert :show="!!messageText" :variant="messageVariant">
|
||||
{{ messageText }}
|
||||
</b-alert>
|
||||
<b-form id="registration" @submit.prevent="submit()">
|
||||
@@ -45,32 +45,32 @@
|
||||
</b-form-group>
|
||||
<b-form-group label="Public name">
|
||||
<b-form-input v-model="username" name="username" type="text" />
|
||||
<b-form-text
|
||||
>Your public name is an identifier that will be used to generate addresses for
|
||||
<b-form-text>
|
||||
Your public name is an identifier that will be used to generate addresses for
|
||||
information you share publicly. Public names must be at least three characters
|
||||
in length and contain only lower-case letters, numbers, dots, underscores, and
|
||||
dashes ('.', '_', '-').</b-form-text
|
||||
>
|
||||
dashes ('.', '_', '-').
|
||||
</b-form-text>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
v-if="mailing_join_addr && server_mail_configured"
|
||||
v-if="mailingJoinAddr && serverMailConfigured"
|
||||
label="Subscribe to mailing list">
|
||||
<input v-model="subscribe" name="subscribe" type="checkbox" />
|
||||
</b-form-group>
|
||||
<b-button name="create" type="submit" :disabled="disableCreate">Create</b-button>
|
||||
</b-card-body>
|
||||
</b-collapse>
|
||||
<b-card-footer v-if="!isAdmin">
|
||||
<b-card-footer v-if="showLoginLink">
|
||||
Already have an account?
|
||||
<a id="login-toggle" href="javascript:void(0)" role="button" @click.prevent="toggleLogin"
|
||||
>Log in here.</a
|
||||
>
|
||||
<a id="login-toggle" href="javascript:void(0)" role="button" @click.prevent="toggleLogin">
|
||||
Log in here.
|
||||
</a>
|
||||
</b-card-footer>
|
||||
</b-card>
|
||||
</b-form>
|
||||
</div>
|
||||
<div v-if="terms_url" class="col">
|
||||
<b-embed type="iframe" :src="terms_url" aspect="1by1" />
|
||||
<div v-if="termsUrl" class="col">
|
||||
<b-embed type="iframe" :src="termsUrl" aspect="1by1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,9 +79,8 @@
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload";
|
||||
import ExternalLogin from "components/User/ExternalIdentities/ExternalLogin.vue";
|
||||
import { safePath } from "utils/redirect";
|
||||
import ExternalLogin from "components/User/ExternalIdentities/ExternalLogin";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
@@ -90,29 +89,44 @@ export default {
|
||||
ExternalLogin,
|
||||
},
|
||||
props: {
|
||||
registration_warning_message: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
server_mail_configured: {
|
||||
enableOidc: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
mailing_join_addr: {
|
||||
showLoginLink: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
mailingJoinAddr: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
preferCustosLogin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
redirect: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
terms_url: {
|
||||
registrationWarningMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
serverMailConfigured: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
sessionCsrfToken: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
termsUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const galaxy = getGalaxyInstance();
|
||||
return {
|
||||
disableCreate: false,
|
||||
email: null,
|
||||
@@ -122,37 +136,33 @@ export default {
|
||||
subscribe: null,
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
session_csrf_token: galaxy.session_csrf_token,
|
||||
isAdmin: galaxy.user.isAdmin(),
|
||||
enable_oidc: galaxy.config.enable_oidc,
|
||||
prefer_custos_login: galaxy.config.prefer_custos_login,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageShow() {
|
||||
return this.messageText != null;
|
||||
},
|
||||
showRegistrationWarning() {
|
||||
return this.registration_warning_message != null;
|
||||
},
|
||||
custosPreferred() {
|
||||
return this.enable_oidc && this.prefer_custos_login;
|
||||
return this.enableOidc && this.preferCustosLogin;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleLogin() {
|
||||
this.$emit("toggle-login");
|
||||
},
|
||||
submit(method) {
|
||||
submit() {
|
||||
this.disableCreate = true;
|
||||
const rootUrl = getAppRoot();
|
||||
axios
|
||||
.post(`${rootUrl}user/create`, this.$data)
|
||||
.post(safePath("/user/create"), {
|
||||
email: this.email,
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
confirm: this.confirm,
|
||||
subscribe: this.subscribe,
|
||||
session_csrf_token: this.sessionCsrfToken,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.message && response.data.status) {
|
||||
alert(response.data.message);
|
||||
}
|
||||
window.location = this.redirect || `${rootUrl}welcome/new`;
|
||||
window.location = this.redirect || safePath("/welcome/new");
|
||||
})
|
||||
.catch((error) => {
|
||||
this.disableCreate = false;
|
||||
@@ -0,0 +1,43 @@
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import MountTarget from "./WorkflowDisplay";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("WorkflowDisplay", () => {
|
||||
let wrapper;
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
const data = {};
|
||||
axiosMock.onGet(`/api/workflows/workflow_id/download?style=preview`).reply(200, data);
|
||||
wrapper = mount(MountTarget, {
|
||||
propsData: {
|
||||
args: {
|
||||
workflow_id: "workflow_id",
|
||||
},
|
||||
embedded: false,
|
||||
expanded: false,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it("basics", async () => {
|
||||
const cardHeader = wrapper.find(".card-header");
|
||||
expect(cardHeader.text()).toBe("Workflow:");
|
||||
const downloadUrl = wrapper.find("[data-description='workflow download']");
|
||||
expect(downloadUrl.attributes("href")).toBe("/api/workflows/workflow_id/download?format=json-download");
|
||||
const importUrl = wrapper.find("[data-description='workflow import']");
|
||||
expect(importUrl.attributes("href")).toBe("/workflow/imp?id=workflow_id");
|
||||
const dataRequest = axiosMock.history.get[0].url;
|
||||
expect(dataRequest).toBe("/api/workflows/workflow_id/download?style=preview");
|
||||
});
|
||||
});
|
||||
@@ -78,10 +78,10 @@ export default {
|
||||
return this.itemContent ? this.itemContent.name : "...";
|
||||
},
|
||||
downloadUrl() {
|
||||
return safePath("/api/workflows/${this.args.workflow_id}/download?format=json-download");
|
||||
return safePath(`/api/workflows/${this.args.workflow_id}/download?format=json-download`);
|
||||
},
|
||||
importUrl() {
|
||||
return safePath("/workflow/imp?id=${this.args.workflow_id}");
|
||||
return safePath(`/workflow/imp?id=${this.args.workflow_id}`);
|
||||
},
|
||||
itemUrl() {
|
||||
return `/api/workflows/${this.args.workflow_id}/download?style=preview`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<config-provider v-slot="{ config, loading }">
|
||||
<Published :details="page">
|
||||
<Published :item="page">
|
||||
<template v-slot>
|
||||
<div v-if="!loading">
|
||||
<markdown
|
||||
@@ -57,7 +57,7 @@ export default {
|
||||
},
|
||||
created() {
|
||||
urlData({ url: this.dataUrl }).then((data) => {
|
||||
this.page = { ...data };
|
||||
this.page = data;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { rethrowSimple } from "utils/simple-error";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
@@ -36,7 +36,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faEye, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import MarkdownEditor from "components/Markdown/MarkdownEditor";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { save } from "./util";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
<template>
|
||||
<div v-infinite-scroll="loadMore" infinite-scroll-disabled="busy">
|
||||
<b-overlay :show="busy" opacity="0.5">
|
||||
<b-table ref="toolTable" striped bordered :fields="fields" :items="buffer">
|
||||
<template v-slot:cell(name)="row">
|
||||
<span v-if="!row.item.help_text"
|
||||
><b>{{ row.item.name }}</b> {{ row.item.description }}</span
|
||||
>
|
||||
<span v-else>
|
||||
<b-link href="javascript:void(0)" role="button" @click="toggleHelp(row)">
|
||||
<b>{{ row.item.name }}</b> {{ row.item.description }}
|
||||
</b-link>
|
||||
<p v-if="!row.item._showDetails && row.item.summary" v-html="row.item.summary" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:row-details="row">
|
||||
<b-card v-if="row.item.help_text">
|
||||
<p class="mb-1" v-html="row.item.help_text" />
|
||||
<a
|
||||
:href="row.item.target === 'galaxy_main' ? 'javascript:void(0)' : row.item.link"
|
||||
@click.stop="onOpen(row.item)">
|
||||
Click here to open the tool
|
||||
</a>
|
||||
</b-card>
|
||||
</template>
|
||||
<template v-slot:cell(section)="row">
|
||||
{{ row.item.panel_section_name }}
|
||||
</template>
|
||||
<template v-slot:cell(workflow)="row">
|
||||
<span
|
||||
v-if="row.item.is_workflow_compatible"
|
||||
v-b-tooltip.hover
|
||||
class="fa fa-check text-success"
|
||||
title="Is Workflow Compatible" />
|
||||
<span v-else v-b-tooltip.hover class="fa fa-times text-danger" title="Not Workflow Compatible" />
|
||||
</template>
|
||||
<template v-slot:cell(target)="row">
|
||||
<span
|
||||
v-if="row.item.target === 'galaxy_main'"
|
||||
v-b-tooltip.hover
|
||||
class="fa fa-check text-success"
|
||||
title="Is Local" />
|
||||
<span v-else v-b-tooltip.hover class="fa fa-times text-danger" title="Not Local" />
|
||||
</template>
|
||||
<template v-slot:cell(open)="row">
|
||||
<b-button
|
||||
v-b-tooltip.hover.top
|
||||
:title="'Open Tool' | localize"
|
||||
class="fa fa-play"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
:href="row.item.target === 'galaxy_main' ? 'javascript:void(0)' : row.item.link"
|
||||
@click.stop="onOpen(row.item)" />
|
||||
</template>
|
||||
</b-table>
|
||||
</b-overlay>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const loadTimeout = 100;
|
||||
const defaultBufferLen = 4;
|
||||
import _l from "utils/localization";
|
||||
import infiniteScroll from "vue-infinite-scroll";
|
||||
import { useGlobalUploadModal } from "composables/useGlobalUploadModal";
|
||||
import { fetchData } from "./services";
|
||||
|
||||
export default {
|
||||
directives: { infiniteScroll },
|
||||
props: {
|
||||
tools: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
buffer: {},
|
||||
bufferLen: defaultBufferLen,
|
||||
busy: false,
|
||||
fields: [
|
||||
{
|
||||
key: "name",
|
||||
label: _l("Name"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "section",
|
||||
label: _l("Section"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "workflow",
|
||||
label: _l("Workflow Compatible"),
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "target",
|
||||
label: _l("Local Tool"),
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "open",
|
||||
label: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
setup() {
|
||||
const { openGlobalUploadModal } = useGlobalUploadModal();
|
||||
return { openGlobalUploadModal };
|
||||
},
|
||||
created() {
|
||||
const initData = { summary: "", help_text: "", _showDetails: false, is_fetched: false };
|
||||
this.buffer = this.tools.slice(0, this.bufferLen);
|
||||
this.buffer.forEach((tool) => Object.assign(tool, { ...initData }));
|
||||
this.buffer.forEach(async (tool, index) => {
|
||||
await this.fetchHelp(index);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onOpen(tool) {
|
||||
if (tool.id === "upload1") {
|
||||
this.openGlobalUploadModal();
|
||||
} else if (tool.form_style === "regular") {
|
||||
// encode spaces in tool.id
|
||||
const toolId = tool.id;
|
||||
const toolVersion = tool.version;
|
||||
this.$router.push({ path: `/?tool_id=${encodeURIComponent(toolId)}&version=${toolVersion}` });
|
||||
}
|
||||
},
|
||||
async loadMore() {
|
||||
if (this.buffer.length < this.tools.length) {
|
||||
this.busy = true;
|
||||
setTimeout(() => {
|
||||
this.bufferLen += defaultBufferLen;
|
||||
this.buffer = this.tools.slice(0, this.bufferLen);
|
||||
this.buffer.forEach(async (tool, index) => {
|
||||
await this.fetchHelp(index);
|
||||
});
|
||||
this.busy = false;
|
||||
}, loadTimeout);
|
||||
}
|
||||
},
|
||||
toggleHelp(row) {
|
||||
row.toggleDetails();
|
||||
this.$refs.toolTable.refresh();
|
||||
},
|
||||
async fetchHelp(index) {
|
||||
const id = this.buffer[index]["id"];
|
||||
if (!this.buffer[index]["is_fetched"]) {
|
||||
this.buffer[index]["is_fetched"] = true; // tags the tool's help as being fetched
|
||||
await fetchData(`api/tools/${id}/build`).then((response) => {
|
||||
if (response.help && response.help != "\n") {
|
||||
this.buffer[index]["help_text"] = response.help;
|
||||
this.buffer[index]["summary"] = this.parseHelp(response.help);
|
||||
} else {
|
||||
this.buffer[index]["help_text"] = ""; // for cases where helpText == '\n'
|
||||
}
|
||||
});
|
||||
this.$refs.toolTable.refresh();
|
||||
}
|
||||
},
|
||||
parseHelp(help) {
|
||||
let summary = "";
|
||||
const parser = new DOMParser();
|
||||
const helpDoc = parser.parseFromString(help, "text/html");
|
||||
const xpaths = [
|
||||
"//strong[text()='What it does']/../following-sibling::*",
|
||||
"//strong[text()='What It Does']/../following-sibling::*",
|
||||
"//h1[text()='Synopsis']/following-sibling::*",
|
||||
"//strong[text()='Syntax']/../following-sibling::*",
|
||||
];
|
||||
const matches = [];
|
||||
xpaths.forEach((xpath) => {
|
||||
matches.push(
|
||||
helpDoc.evaluate(xpath, helpDoc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
|
||||
);
|
||||
});
|
||||
matches.forEach((match) => {
|
||||
if (match) {
|
||||
summary += match.innerHTML + "\n";
|
||||
}
|
||||
});
|
||||
return summary;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,65 +0,0 @@
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import axios from "axios";
|
||||
import { filterToolSections, filterTools } from "../utilities";
|
||||
|
||||
describe("ToolBox", () => {
|
||||
const toolsMock = [
|
||||
{
|
||||
model_class: "ToolSection",
|
||||
id: "group_1",
|
||||
name: "Group 1",
|
||||
elems: [
|
||||
{
|
||||
model_class: "Tool1",
|
||||
id: "tool1",
|
||||
name: "Tool 1",
|
||||
link: "/tool_runner?tool_id=tool1",
|
||||
panel_section_id: "group_1",
|
||||
panel_section_name: "Group 1",
|
||||
},
|
||||
{
|
||||
model_class: "Tool2",
|
||||
id: "tool2",
|
||||
name: "Tool 2",
|
||||
link: "/tool_runner?tool_id=tool2",
|
||||
panel_section_id: "group_1",
|
||||
panel_section_name: "Group 1",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
model_class: "ToolSection",
|
||||
id: "group_2",
|
||||
name: "Group 2",
|
||||
elems: [
|
||||
{
|
||||
model_class: "Tool3",
|
||||
id: "tool3",
|
||||
name: "Tool 3",
|
||||
link: "/tool_runner?tool_id=tool3",
|
||||
panel_section_id: "group_2",
|
||||
panel_section_name: "Group 2",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const resultsMock = ["tool1", "tool3"];
|
||||
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
it("test filter functions correctly matching: (1) Tools store array-of-objects with (2) Results array", async () => {
|
||||
axiosMock
|
||||
.onGet(`/api/tools`)
|
||||
.replyOnce(200, toolsMock)
|
||||
.onGet(/api\/tools?.*/)
|
||||
.replyOnce(200, resultsMock);
|
||||
const toolsResults = filterTools(toolsMock, resultsMock);
|
||||
const toolsResultsSection = filterToolSections(toolsMock, resultsMock);
|
||||
expect(toolsResults.length).toBe(2);
|
||||
expect(toolsResultsSection.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import DelayedInput from "components/Common/DelayedInput";
|
||||
import ToolSearch from "./ToolSearch";
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -8,7 +9,9 @@ describe("ToolSearch", () => {
|
||||
const wrapper = mount(ToolSearch, {
|
||||
propsData: {
|
||||
currentPanelView: "default",
|
||||
enableAdvanced: false,
|
||||
showAdvanced: false,
|
||||
toolbox: [],
|
||||
},
|
||||
localVue,
|
||||
stubs: {
|
||||
@@ -16,17 +19,23 @@ describe("ToolSearch", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find("[data-description='toggle advanced search']").exists()).toBe(false);
|
||||
expect(wrapper.find("[description='advanced tool filters']").exists()).toBe(false);
|
||||
await wrapper.setProps({ showAdvanced: true });
|
||||
await wrapper.setProps({ enableAdvanced: true, showAdvanced: true });
|
||||
expect(wrapper.find("[data-description='toggle advanced search']").exists()).toBe(true);
|
||||
expect(wrapper.find("[description='advanced tool filters']").exists()).toBe(true);
|
||||
|
||||
const filterInputs = {
|
||||
"[placeholder='any tool name']": "name-filter",
|
||||
name: "name-filter",
|
||||
"[placeholder='any section']": "section-filter",
|
||||
"[placeholder='any id']": "id-filter",
|
||||
"[placeholder='any description']": "desc-filter",
|
||||
"[placeholder='any help text']": "help-filter",
|
||||
};
|
||||
|
||||
// Now add filters in all input fields in the advanced menu
|
||||
// Add name filter (comes from DelayedInput emitting the query prop)
|
||||
wrapper.findComponent(DelayedInput).vm.$emit("change", filterInputs["name"]);
|
||||
|
||||
// Now add remaining filters (other than name) in the advanced menu
|
||||
Object.entries(filterInputs).forEach(([selector, value]) => {
|
||||
const filterInput = wrapper.find(selector);
|
||||
if (filterInput.vm && filterInput.props().type == "text") {
|
||||
@@ -37,16 +46,17 @@ describe("ToolSearch", () => {
|
||||
// Test: values are stored in the filterSettings object
|
||||
expect(Object.values(wrapper.vm.filterSettings)).toEqual(Object.values(filterInputs));
|
||||
|
||||
// Test: clearing the filters
|
||||
const clearButton = wrapper.find("[description='clear filters']");
|
||||
expect(Object.values(wrapper.vm.filterSettings).length).toBe(4);
|
||||
await clearButton.trigger("click");
|
||||
expect(Object.values(wrapper.vm.filterSettings).length).toBe(0);
|
||||
// Test: changing panel view should change search by section field to search by ontology
|
||||
expect(wrapper.find("[placeholder='any section']").exists()).toBe(true);
|
||||
await wrapper.setProps({ currentPanelView: "ontology:edam_operations" });
|
||||
expect(wrapper.find("[placeholder='any section']").exists()).toBe(false);
|
||||
expect(wrapper.find("[placeholder='any ontology']").exists()).toBe(true);
|
||||
await wrapper.setProps({ currentPanelView: "default" });
|
||||
|
||||
// Test: keyup.esc (should toggle the view out)
|
||||
const nameField = wrapper.find("[placeholder='any tool name']");
|
||||
// Test: keyup.esc (should toggle the view out) --- doesn't work from name (DelayedInput) field
|
||||
const sectionField = wrapper.find("[placeholder='any section']");
|
||||
expect(wrapper.emitted()["update:show-advanced"]).toBeUndefined();
|
||||
await nameField.trigger("keyup.esc");
|
||||
await sectionField.trigger("keyup.esc");
|
||||
expect(wrapper.emitted()["update:show-advanced"].length - 1).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<small v-if="showAdvanced">Filter by name:</small>
|
||||
<DelayedInput
|
||||
:class="!showAdvanced && 'mb-3'"
|
||||
:query="query"
|
||||
:loading="loading"
|
||||
:show-advanced="showAdvanced"
|
||||
include-adv-btn
|
||||
:placeholder="placeholder"
|
||||
:enable-advanced="enableAdvanced"
|
||||
:placeholder="showAdvanced ? 'any name' : placeholder"
|
||||
@change="checkQuery"
|
||||
@onToggle="onToggle" />
|
||||
<div
|
||||
v-if="showAdvanced"
|
||||
class="mt-2"
|
||||
description="advanced tool filters"
|
||||
@keyup.enter="onSearch"
|
||||
@keyup.esc="onToggle(false)">
|
||||
<small>Filter by tool name:</small>
|
||||
<b-form-input v-model="filterSettings['name']" size="sm" placeholder="any tool name" />
|
||||
<small class="mt-1">Filter by section:</small>
|
||||
<b-form-input v-model="filterSettings['panelSectionName']" size="sm" placeholder="any section" />
|
||||
<small class="mt-1">Filter by {{ sectionLabel }}:</small>
|
||||
<b-form-input
|
||||
v-model="filterSettings['section']"
|
||||
autocomplete="off"
|
||||
size="sm"
|
||||
:placeholder="`any ${sectionLabel}`"
|
||||
list="sectionSelect" />
|
||||
<b-form-datalist id="sectionSelect" :options="sectionNames"></b-form-datalist>
|
||||
<small class="mt-1">Filter by id:</small>
|
||||
<b-form-input v-model="filterSettings['id']" size="sm" placeholder="any id" />
|
||||
<small class="mt-1">Filter by description:</small>
|
||||
<b-form-input v-model="filterSettings['description']" size="sm" placeholder="any description" />
|
||||
<small class="mt-1">Filter by help text:</small>
|
||||
<b-form-input v-model="filterSettings['help']" size="sm" placeholder="any help text" />
|
||||
<div class="mt-3">
|
||||
<b-button class="mr-1" size="sm" variant="primary" @click="onSearch">
|
||||
<icon icon="search" />
|
||||
<span>{{ "Search" | localize }}</span>
|
||||
</b-button>
|
||||
<b-button size="sm" description="clear filters" @click="filterSettings = {}">
|
||||
<b-button size="sm" @click="onToggle(false)">
|
||||
<icon icon="redo" />
|
||||
<span>{{ "Clear" | localize }}</span>
|
||||
<span>{{ "Cancel" | localize }}</span>
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,10 +41,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import DelayedInput from "components/Common/DelayedInput";
|
||||
import { normalizeTools, searchToolsByKeys } from "../utilities.js";
|
||||
|
||||
export default {
|
||||
name: "ToolSearch",
|
||||
@@ -53,6 +55,10 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
enableAdvanced: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "search tools",
|
||||
@@ -65,12 +71,15 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
toolbox: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
favorites: ["#favs", "#favorites", "#favourites"],
|
||||
minQueryLength: 3,
|
||||
loading: false,
|
||||
filterSettings: {},
|
||||
};
|
||||
},
|
||||
@@ -79,27 +88,29 @@ export default {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
return Galaxy.user.getFavorites().tools;
|
||||
},
|
||||
sectionNames() {
|
||||
return this.toolbox.map((section) =>
|
||||
section.name !== undefined && section.name !== "Uncategorized" ? section.name : ""
|
||||
);
|
||||
},
|
||||
sectionLabel() {
|
||||
return this.currentPanelView === "default" ? "section" : "ontology";
|
||||
},
|
||||
toolsList() {
|
||||
return normalizeTools(this.toolbox);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
checkQuery(q) {
|
||||
this.filterSettings["name"] = q;
|
||||
this.$emit("onQuery", q);
|
||||
if (q && q.length >= this.minQueryLength) {
|
||||
if (this.favorites.includes(q)) {
|
||||
this.$emit("onResults", this.favoritesResults);
|
||||
} else {
|
||||
this.loading = true;
|
||||
axios
|
||||
.get(`${getAppRoot()}api/tools`, {
|
||||
params: { q, view: this.currentPanelView },
|
||||
})
|
||||
.then((response) => {
|
||||
this.loading = false;
|
||||
this.$emit("onResults", response.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = false;
|
||||
this.$emit("onError", err);
|
||||
});
|
||||
// keys with sorting order
|
||||
const keys = { exact: 2, name: 1, description: 0 };
|
||||
this.$emit("onResults", searchToolsByKeys(this.toolsList, keys, q));
|
||||
}
|
||||
} else {
|
||||
this.$emit("onResults", null);
|
||||
@@ -111,7 +122,7 @@ export default {
|
||||
delete this.filterSettings[filter];
|
||||
}
|
||||
}
|
||||
this.$router.push({ path: "/tools/advanced_search", query: this.filterSettings });
|
||||
this.$router.push({ path: "/tools/list", query: this.filterSettings });
|
||||
},
|
||||
onToggle(toggleAdvanced) {
|
||||
this.$emit("update:show-advanced", toggleAdvanced);
|
||||
|
||||
@@ -2,9 +2,9 @@ import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import ToolSection from "./ToolSection";
|
||||
|
||||
import { useConfig } from "composables/useConfig";
|
||||
import { useConfig } from "composables/config";
|
||||
|
||||
jest.mock("composables/useConfig");
|
||||
jest.mock("composables/config");
|
||||
useConfig.mockReturnValue({
|
||||
config: {
|
||||
toolbox_auto_sort: true,
|
||||
|
||||
@@ -53,7 +53,7 @@ import ToolPanelLabel from "./ToolPanelLabel";
|
||||
import ariaAlert from "utils/ariaAlert";
|
||||
import ToolPanelLinks from "./ToolPanelLinks";
|
||||
|
||||
import { useConfig } from "composables/useConfig";
|
||||
import { useConfig } from "composables/config";
|
||||
|
||||
export default {
|
||||
name: "ToolSection",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import Vuex from "vuex";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import axios from "axios";
|
||||
import { filterToolSections, filterTools } from "./utilities";
|
||||
import ToolBox from "./ToolBox";
|
||||
import toolsList from "./testToolsList";
|
||||
import { userStore } from "store/userStore";
|
||||
import { useConfig } from "composables/config";
|
||||
|
||||
jest.mock("composables/config");
|
||||
useConfig.mockReturnValue({
|
||||
config: {
|
||||
toolbox_auto_sort: true,
|
||||
},
|
||||
isLoaded: true,
|
||||
});
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("ToolBox", () => {
|
||||
const toolsMock = toolsList;
|
||||
const resultsMock = ["join1", "join_collections", "find1"];
|
||||
let axiosMock;
|
||||
|
||||
const searches = {
|
||||
join: ["join1", "join_collections", "find1"],
|
||||
find: ["find1"],
|
||||
remove: ["remove1", "remove_duplicate"],
|
||||
fi: null,
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
let store;
|
||||
let state;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
user: {
|
||||
state,
|
||||
actions: {
|
||||
loadUser: jest.fn(),
|
||||
},
|
||||
getters: userStore.getters,
|
||||
namespaced: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = mount(ToolBox, {
|
||||
propsData: {
|
||||
toolbox: toolsList,
|
||||
currentPanelView: "default",
|
||||
storedWorkflowMenuEntries: [],
|
||||
},
|
||||
store,
|
||||
localVue,
|
||||
stubs: {
|
||||
icon: { template: "<div></div>" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("test filter functions correctly matching: (1) Tools store array-of-objects with (2) Results array", async () => {
|
||||
axiosMock
|
||||
.onGet(`/api/tools`)
|
||||
.replyOnce(200, toolsMock)
|
||||
.onGet(/api\/tools?.*/)
|
||||
.replyOnce(200, resultsMock);
|
||||
const toolsResults = filterTools(toolsMock, resultsMock);
|
||||
const toolsResultsSection = filterToolSections(toolsMock, resultsMock);
|
||||
expect(toolsResults.length).toBe(3);
|
||||
expect(toolsResultsSection.length).toBe(2);
|
||||
});
|
||||
|
||||
it("test toolbox client search", async () => {
|
||||
for (const [query, results] of Object.entries(searches)) {
|
||||
await wrapper.setData({ query: query });
|
||||
expect(wrapper.vm.results).toEqual(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -3,12 +3,12 @@
|
||||
<div unselectable="on">
|
||||
<div class="unified-panel-header-inner">
|
||||
<nav class="d-flex justify-content-between mx-3 my-2">
|
||||
<h2 v-if="!showAdvanced" v-localize id="toolbox-heading" class="m-1 h-sm">Tools</h2>
|
||||
<h2 v-else v-localize id="toolbox-heading" class="m-1 h-sm">Advanced Tool Search</h2>
|
||||
<h2 v-if="!showAdvanced" id="toolbox-heading" v-localize class="m-1 h-sm">Tools</h2>
|
||||
<h2 v-else id="toolbox-heading" v-localize class="m-1 h-sm">Advanced Tool Search</h2>
|
||||
|
||||
<div v-if="!showAdvanced" class="panel-header-buttons">
|
||||
<div class="panel-header-buttons">
|
||||
<b-button-group>
|
||||
<favorites-button :query="query" @onFavorites="onQuery" />
|
||||
<favorites-button v-if="!showAdvanced" :query="query" @onFavorites="onQuery" />
|
||||
<panel-view-button
|
||||
v-if="panelViews && Object.keys(panelViews).length > 1"
|
||||
:panel-views="panelViews"
|
||||
@@ -21,9 +21,11 @@
|
||||
</div>
|
||||
<div class="unified-panel-controls">
|
||||
<tool-search
|
||||
enable-advanced
|
||||
:current-panel-view="currentPanelView"
|
||||
:placeholder="titleSearchTools"
|
||||
:show-advanced.sync="showAdvanced"
|
||||
:toolbox="toolbox"
|
||||
:query="query"
|
||||
@onQuery="onQuery"
|
||||
@onResults="onResults" />
|
||||
@@ -72,7 +74,7 @@ import { UploadButton } from "components/Upload";
|
||||
import { useGlobalUploadModal } from "composables/useGlobalUploadModal";
|
||||
import FavoritesButton from "./Buttons/FavoritesButton";
|
||||
import PanelViewButton from "./Buttons/PanelViewButton";
|
||||
import { filterToolSections, filterTools } from "./utilities";
|
||||
import { filterToolSections, filterTools, hasResults } from "./utilities";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload";
|
||||
import _l from "utils/localization";
|
||||
@@ -133,7 +135,7 @@ export default {
|
||||
if (this.showSections) {
|
||||
return filterToolSections(this.toolbox, this.results);
|
||||
} else {
|
||||
return filterTools(this.toolbox, this.results);
|
||||
return hasResults(this.results) ? filterTools(this.toolbox, this.results) : this.toolbox;
|
||||
}
|
||||
},
|
||||
isUser() {
|
||||
|
||||
@@ -18,9 +18,16 @@
|
||||
<tool-search
|
||||
:current-panel-view="currentPanelView"
|
||||
placeholder="search tools"
|
||||
:toolbox="toolbox"
|
||||
:query="query"
|
||||
@onQuery="onQuery"
|
||||
@onResults="onResults" />
|
||||
<div v-if="queryTooShort" class="pb-2">
|
||||
<b-badge class="alert-danger w-100">Search string too short!</b-badge>
|
||||
</div>
|
||||
<div v-else-if="noResults" class="pb-2">
|
||||
<b-badge class="alert-danger w-100">No results found!</b-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-panel-body">
|
||||
<div class="toolMenuContainer">
|
||||
@@ -108,6 +115,12 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
queryTooShort() {
|
||||
return this.query && this.query.length < 3;
|
||||
},
|
||||
noResults() {
|
||||
return this.query && this.results.length === 0;
|
||||
},
|
||||
hasWorkflowSection() {
|
||||
return this.workflows.length > 0;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
[
|
||||
{
|
||||
"model_class": "ToolSection",
|
||||
"id": "group_1",
|
||||
"name": "Group 1",
|
||||
"elems": [
|
||||
{
|
||||
"model_class": "Tool",
|
||||
"name": "Join",
|
||||
"id": "join1",
|
||||
"description": "two files",
|
||||
"panel_section_id": "group_1",
|
||||
"panel_section_name": "Group 1"
|
||||
},
|
||||
{
|
||||
"model_class": "Tool",
|
||||
"name": "Remove",
|
||||
"id": "remove1",
|
||||
"description": "lines",
|
||||
"panel_section_id": "group_1",
|
||||
"panel_section_name": "Group 1"
|
||||
},
|
||||
{
|
||||
"model_class": "Tool",
|
||||
"name": "Cut",
|
||||
"id": "cut1",
|
||||
"description": "lines",
|
||||
"panel_section_id": "group_1",
|
||||
"panel_section_name": "Group 1"
|
||||
},
|
||||
{
|
||||
"model_class": "Tool",
|
||||
"name": "Find",
|
||||
"id": "find1",
|
||||
"description": "joined dataset",
|
||||
"panel_section_id": "group_1",
|
||||
"panel_section_name": "Group 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"model_class": "ToolSection",
|
||||
"id": "group_2",
|
||||
"name": "Group 2",
|
||||
"elems": [
|
||||
{
|
||||
"model_class": "Tool",
|
||||
"name": "Join Collections",
|
||||
"id": "join_collections",
|
||||
"description": "",
|
||||
"panel_section_id": "group_2",
|
||||
"panel_section_name": "Group 2"
|
||||
},
|
||||
{
|
||||
"model_class": "Tool",
|
||||
"name": "Create Collections",
|
||||
"id": "create_collections",
|
||||
"description": "from datasets",
|
||||
"panel_section_id": "group_2",
|
||||
"panel_section_name": "Group 2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"model_class": "Tool",
|
||||
"name": "Remove duplicate",
|
||||
"id": "remove_duplicate",
|
||||
"description": "lines from dataset",
|
||||
"panel_section_id": "remove_duplicate",
|
||||
"panel_section_name": "Remove duplicate"
|
||||
}
|
||||
]
|
||||
@@ -1,20 +1,64 @@
|
||||
/**
|
||||
* Utilities file for Tool Search (panel/client search + advanced/backend search)
|
||||
*/
|
||||
import { orderBy } from "lodash";
|
||||
|
||||
const TOOLS_RESULTS_SORT_LABEL = "apiSort";
|
||||
const TOOLS_RESULTS_SECTIONS_HIDE = ["Expression Tools"];
|
||||
|
||||
// - Takes filterSettings = {"name": "Tool Name", "section": "Collection", ...}
|
||||
// - Takes panelView (if not 'default', does ontology search at backend)
|
||||
// - Takes toolbox (to find ontology id if given ontology name)
|
||||
// - Returns parsed Whoosh query
|
||||
// e.g. fn call: createWhooshQuery(filterSettings, 'ontology:edam_topics', toolbox)
|
||||
// can return:
|
||||
// query = "(name:(skew) name_exact:(skew) description:(skew)) AND (edam_topics:(topic_0797) AND )"
|
||||
export function createWhooshQuery(filterSettings, panelView, toolbox) {
|
||||
let query = "(";
|
||||
// add description+name_exact fields = name, to do a combined OrGroup at backend
|
||||
const name = filterSettings["name"];
|
||||
if (name) {
|
||||
query += "name:(" + name + ") ";
|
||||
query += "name_exact:(" + name + ") ";
|
||||
query += "description:(" + name + ")";
|
||||
}
|
||||
query += ") AND (";
|
||||
for (const [key, filterValue] of Object.entries(filterSettings)) {
|
||||
// get ontology keys if view is not default
|
||||
if (key === "section" && panelView !== "default") {
|
||||
const ontology = toolbox.find(({ name }) => name && name.toLowerCase().match(filterValue.toLowerCase()));
|
||||
if (ontology) {
|
||||
let ontologyKey = "";
|
||||
if (panelView === "ontology:edam_operations") {
|
||||
ontologyKey = "edam_operations";
|
||||
} else if (panelView === "ontology:edam_topics") {
|
||||
ontologyKey = "edam_topics";
|
||||
}
|
||||
query += ontologyKey + ":(" + ontology.id + ") AND ";
|
||||
} else {
|
||||
query += key + ":(" + filterValue + ") AND ";
|
||||
}
|
||||
} else if (key == "id") {
|
||||
query += "id_exact:(" + filterValue + ") AND ";
|
||||
} else if (key != "name") {
|
||||
query += key + ":(" + filterValue + ") AND ";
|
||||
}
|
||||
}
|
||||
query += ")";
|
||||
return query;
|
||||
}
|
||||
|
||||
// Given toolbox and search results, returns filtered tool results
|
||||
export function filterTools(tools, results) {
|
||||
let toolsResults = [];
|
||||
if (hasResults(results)) {
|
||||
tools = normalizeTools(tools);
|
||||
toolsResults = mapToolsResults(tools, results);
|
||||
toolsResults = sortToolsResults(toolsResults);
|
||||
} else {
|
||||
toolsResults = tools;
|
||||
}
|
||||
tools = normalizeTools(tools);
|
||||
toolsResults = mapToolsResults(tools, results);
|
||||
toolsResults = sortToolsResults(toolsResults);
|
||||
toolsResults = removeDuplicateResults(toolsResults);
|
||||
return toolsResults;
|
||||
}
|
||||
|
||||
// Given toolbox and search results, returns filtered tool results by sections
|
||||
export function filterToolSections(tools, results) {
|
||||
let toolsResults = [];
|
||||
let toolsResultsSection = [];
|
||||
@@ -23,7 +67,6 @@ export function filterToolSections(tools, results) {
|
||||
tools = flattenToolsSection(section);
|
||||
toolsResultsSection = mapToolsResults(tools, results);
|
||||
toolsResultsSection = sortToolsResults(toolsResultsSection);
|
||||
|
||||
return {
|
||||
...section,
|
||||
elems: toolsResultsSection,
|
||||
@@ -36,7 +79,31 @@ export function filterToolSections(tools, results) {
|
||||
return toolsResults;
|
||||
}
|
||||
|
||||
function normalizeTools(tools) {
|
||||
export function hasResults(results) {
|
||||
return Array.isArray(results) && results.length > 0;
|
||||
}
|
||||
|
||||
// Given toolbox, keys to sort/search results by and a search query,
|
||||
// Returns tool ids sorted by order of keys that are being searched
|
||||
export function searchToolsByKeys(tools, keys, query) {
|
||||
const returnedTools = [];
|
||||
for (const tool of tools) {
|
||||
for (const key of Object.keys(keys)) {
|
||||
const actualValue = tool[key] ? tool[key].toLowerCase() : "";
|
||||
const queryLowerCase = query.toLowerCase();
|
||||
if (actualValue.match(queryLowerCase)) {
|
||||
// do we care for exact matches && is it an exact match ?
|
||||
const order = keys.exact && actualValue === queryLowerCase ? keys.exact : keys[key];
|
||||
returnedTools.push({ id: tool.id, order });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// sorting results by indexed order of keys
|
||||
return orderBy(returnedTools, ["order"], ["desc"]).map((tool) => tool.id);
|
||||
}
|
||||
|
||||
export function normalizeTools(tools) {
|
||||
tools = hideToolsSection(tools);
|
||||
tools = flattenTools(tools);
|
||||
return tools;
|
||||
@@ -66,6 +133,18 @@ function mapToolsResults(tools, results) {
|
||||
return toolsResults;
|
||||
}
|
||||
|
||||
function removeDuplicateResults(results) {
|
||||
const uniqueTools = [];
|
||||
return results.filter((tool) => {
|
||||
if (!uniqueTools.includes(tool.id)) {
|
||||
uniqueTools.push(tool.id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setSort(tool, results) {
|
||||
return { [TOOLS_RESULTS_SORT_LABEL]: results.indexOf(tool.id) };
|
||||
}
|
||||
@@ -104,7 +183,3 @@ function flattenTools(tools) {
|
||||
});
|
||||
return normalizedTools;
|
||||
}
|
||||
|
||||
function hasResults(results) {
|
||||
return Array.isArray(results) && results.length > 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import toolsList from "components/ToolsView/testData/toolsList";
|
||||
import { createWhooshQuery, filterTools, filterToolSections, normalizeTools, searchToolsByKeys } from "./utilities";
|
||||
|
||||
describe("test helpers in tool searching utilities", () => {
|
||||
it("test parsing helper that converts settings to whoosh query", async () => {
|
||||
const settings = {
|
||||
name: "Filter",
|
||||
id: "__FILTER_FAILED_DATASETS__",
|
||||
help: "downstream",
|
||||
};
|
||||
const q = createWhooshQuery(settings, "default", []);
|
||||
|
||||
// OrGroup (at backend) on name, name_exact, description
|
||||
expect(q).toContain("name:(Filter) name_exact:(Filter) description:(Filter)");
|
||||
// AndGroup (explicit at frontend) on all other settings
|
||||
expect(q).toContain("id_exact:(__FILTER_FAILED_DATASETS__) AND help:(downstream)");
|
||||
});
|
||||
|
||||
it("test tool search helper that searches for tools given keys", async () => {
|
||||
const q = "collection";
|
||||
let expectedResults = [
|
||||
"__FILTER_FAILED_DATASETS__",
|
||||
"__FILTER_EMPTY_DATASETS__",
|
||||
"__UNZIP_COLLECTION__",
|
||||
"__ZIP_COLLECTION__",
|
||||
];
|
||||
let keys = { description: 1, name: 0 };
|
||||
let results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
|
||||
expect(results).toEqual(expectedResults);
|
||||
|
||||
expectedResults = [
|
||||
"__UNZIP_COLLECTION__",
|
||||
"__ZIP_COLLECTION__",
|
||||
"__FILTER_FAILED_DATASETS__",
|
||||
"__FILTER_EMPTY_DATASETS__",
|
||||
];
|
||||
keys = { description: 0, name: 1 };
|
||||
results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
|
||||
expect(results).toEqual(expectedResults);
|
||||
});
|
||||
|
||||
it("test tool filtering helpers on toolsList given list of ids", async () => {
|
||||
const ids = ["__FILTER_FAILED_DATASETS__", "liftOver1"];
|
||||
// check length of first section from imported const toolsList
|
||||
expect(toolsList.find((section) => section.id == "collection_operations").elems).toHaveLength(4);
|
||||
// check length of same section from filtered toolsList
|
||||
expect(
|
||||
filterToolSections(toolsList, ids).find((section) => section.id == "collection_operations").elems
|
||||
).toHaveLength(1);
|
||||
// check length of filtered tools without sections
|
||||
expect(filterTools(toolsList, ids)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faDownload, faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||
library.add(faDownload, faSpinner);
|
||||
import ConfigProvider from "components/providers/ConfigProvider";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import axios from "axios";
|
||||
import { safePath } from "utils/redirect";
|
||||
export default {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
methods: {
|
||||
showToast(message, title, variant) {
|
||||
this.$bvToast.toast(message, {
|
||||
variant,
|
||||
title,
|
||||
toaster: "b-toaster-bottom-right",
|
||||
append: true,
|
||||
solid: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import ToolSource from "./ToolSource.vue";
|
||||
import { useConfig } from "composables/useConfig";
|
||||
import { useConfig } from "composables/config";
|
||||
import { useCurrentUser } from "composables/user";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
|
||||
+35
-17
@@ -1,10 +1,21 @@
|
||||
<template>
|
||||
<ToolsProvider v-slot="{ loading, result: itemsLoaded }" :filter-settings="filterSettings">
|
||||
<ToolsProvider
|
||||
v-slot="{ loading, result: itemsLoaded }"
|
||||
:filter-settings="filterSettings"
|
||||
:toolbox="toolbox"
|
||||
:panel-view="panelView">
|
||||
<section class="overflow-auto h-100" @scroll="onScroll">
|
||||
<div class="mb-2">
|
||||
<h1 class="h-lg mb-2">Advanced Tool Search Results</h1>
|
||||
<span v-if="itemsLoaded.length !== 0 && hasFilters" class="row">
|
||||
<span class="col d-inline-block">
|
||||
<span class="row mb-2">
|
||||
<span class="col">
|
||||
<h1 v-if="hasFilters" class="d-inline-block h-lg">Advanced Tool Search Results</h1>
|
||||
<h1 v-else class="d-inline-block h-lg">
|
||||
Consolidated view of {{ itemsLoaded.length }} available tools.
|
||||
</h1>
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="itemsLoaded.length !== 0" class="row">
|
||||
<span v-if="hasFilters" class="col d-inline-block">
|
||||
Found {{ itemsLoaded.length }} tools for
|
||||
<a id="popover-filters" href="javascript:void(0)">filters</a>.
|
||||
<b-popover target="popover-filters" triggers="hover" placement="top">
|
||||
@@ -14,6 +25,11 @@
|
||||
>: {{ value }}
|
||||
</div>
|
||||
</b-popover>
|
||||
Click <a href="javascript:void(0)" @click.stop="showAllTools">here</a> for a consolidated view
|
||||
of all tools in this Galaxy instance.
|
||||
</span>
|
||||
<span v-else class="col d-inline-block">
|
||||
No filters applied. Please add filters to the menu in the Tool Panel.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -21,14 +37,11 @@
|
||||
<b-alert v-if="loading" class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Loading Advanced Search Results" />
|
||||
</b-alert>
|
||||
<b-alert v-else-if="!hasFilters" class="m-2" variant="secondary" show>
|
||||
Please add filters to the menu in the Tool Panel.
|
||||
</b-alert>
|
||||
<b-alert v-else-if="!itemsLoaded || itemsLoaded.length == 0" class="m-2" variant="info" show>
|
||||
No tools found for the entered filters.
|
||||
</b-alert>
|
||||
<div v-else>
|
||||
<ToolAdvancedSearchResults :tools="itemsLoaded" />
|
||||
<ToolsListTable :tools="itemsLoaded" />
|
||||
</div>
|
||||
</div>
|
||||
<ScrollToTopButton :offset="offset" @click="scrollToTop" />
|
||||
@@ -39,13 +52,13 @@
|
||||
<script>
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import { ToolsProvider } from "components/providers/storeProviders";
|
||||
import ToolAdvancedSearchResults from "./ToolAdvancedSearchResults";
|
||||
import ToolsListTable from "./ToolsListTable";
|
||||
import ScrollToTopButton from "./ScrollToTopButton";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpan,
|
||||
ToolAdvancedSearchResults,
|
||||
ToolsListTable,
|
||||
ToolsProvider,
|
||||
ScrollToTopButton,
|
||||
},
|
||||
@@ -54,7 +67,7 @@ export default {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
panelSectionName: {
|
||||
section: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
@@ -62,7 +75,7 @@ export default {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
description: {
|
||||
help: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
@@ -73,15 +86,17 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
toolbox() {
|
||||
return this.$store.getters["panels/currentPanel"];
|
||||
},
|
||||
panelView() {
|
||||
return this.$store.getters["panels/currentPanelView"];
|
||||
},
|
||||
filterSettings() {
|
||||
const newFilterSettings = {};
|
||||
Object.entries(this.$props).forEach(([filter, value]) => {
|
||||
if (value && value !== "") {
|
||||
if (filter === "panelSectionName") {
|
||||
newFilterSettings["panel_section_name"] = value;
|
||||
} else {
|
||||
newFilterSettings[filter] = value;
|
||||
}
|
||||
newFilterSettings[filter] = value;
|
||||
}
|
||||
});
|
||||
return newFilterSettings;
|
||||
@@ -97,6 +112,9 @@ export default {
|
||||
scrollToTop() {
|
||||
this.$el.scrollTop = 0;
|
||||
},
|
||||
showAllTools() {
|
||||
this.$router.push({ path: "/tools/list" });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div v-infinite-scroll="loadTools" infinite-scroll-disabled="busy">
|
||||
<b-table striped bordered :fields="fields" :items="buffer">
|
||||
<template v-slot:cell(name)="row">
|
||||
<span v-if="!row.item.help">
|
||||
<b>{{ row.item.name }}</b> {{ row.item.description }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<b-link href="javascript:void(0)" role="button" @click.stop="row.toggleDetails()">
|
||||
<b>{{ row.item.name }}</b> {{ row.item.description }}
|
||||
</b-link>
|
||||
<p v-if="!row.item._showDetails && row.item.summary" v-html="row.item.summary" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:row-details="row">
|
||||
<b-card v-if="row.item.help">
|
||||
<p class="mb-1" v-html="row.item.help" />
|
||||
<a
|
||||
:href="row.item.target === 'galaxy_main' ? 'javascript:void(0)' : row.item.link"
|
||||
@click.stop="onOpen(row.item)">
|
||||
Click here to open the tool
|
||||
</a>
|
||||
</b-card>
|
||||
</template>
|
||||
<template v-slot:cell(section)="row">
|
||||
{{ row.item.panel_section_name }}
|
||||
</template>
|
||||
<template v-slot:cell(workflow)="row">
|
||||
<span
|
||||
v-if="row.item.is_workflow_compatible"
|
||||
v-b-tooltip.hover
|
||||
class="fa fa-check text-success"
|
||||
title="Is Workflow Compatible" />
|
||||
<span v-else v-b-tooltip.hover class="fa fa-times text-danger" title="Not Workflow Compatible" />
|
||||
</template>
|
||||
<template v-slot:cell(target)="row">
|
||||
<span
|
||||
v-if="row.item.target === 'galaxy_main'"
|
||||
v-b-tooltip.hover
|
||||
class="fa fa-check text-success"
|
||||
title="Is Local" />
|
||||
<span v-else v-b-tooltip.hover class="fa fa-times text-danger" title="Not Local" />
|
||||
</template>
|
||||
<template v-slot:cell(open)="row">
|
||||
<b-button
|
||||
v-b-tooltip.hover.top
|
||||
:title="'Open Tool' | localize"
|
||||
class="fa fa-play"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
:href="row.item.target === 'galaxy_main' ? 'javascript:void(0)' : row.item.link"
|
||||
@click.stop="onOpen(row.item)" />
|
||||
</template>
|
||||
</b-table>
|
||||
<div>
|
||||
<i v-if="allLoaded">All {{ tools.length > 1 ? tools.length : "" }} results loaded</i>
|
||||
<b-overlay :show="busy" opacity="0.5" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import _l from "utils/localization";
|
||||
import infiniteScroll from "vue-infinite-scroll";
|
||||
import { openGlobalUploadModal } from "components/Upload";
|
||||
import { fetchData } from "./services";
|
||||
|
||||
const defaultBufferLen = 4;
|
||||
const loadTimeout = 100;
|
||||
|
||||
export default {
|
||||
directives: { infiniteScroll },
|
||||
props: {
|
||||
tools: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
allLoaded: false,
|
||||
bufferLen: 0,
|
||||
busy: false,
|
||||
fields: [
|
||||
{
|
||||
key: "name",
|
||||
label: _l("Name"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "section",
|
||||
label: _l("Section"),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: "workflow",
|
||||
label: _l("Workflow Compatible"),
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "target",
|
||||
label: _l("Local Tool"),
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
key: "open",
|
||||
label: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
buffer() {
|
||||
return this.tools.slice(0, this.bufferLen);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadTools();
|
||||
},
|
||||
methods: {
|
||||
onOpen(tool) {
|
||||
if (tool.id === "upload1") {
|
||||
openGlobalUploadModal();
|
||||
} else if (tool.form_style === "regular") {
|
||||
// encode spaces in tool.id
|
||||
const toolId = tool.id;
|
||||
const toolVersion = tool.version;
|
||||
this.$router.push({ path: `/?tool_id=${encodeURIComponent(toolId)}&version=${toolVersion}` });
|
||||
}
|
||||
},
|
||||
loadTools() {
|
||||
if (this.tools && !this.busy && this.bufferLen < this.tools.length) {
|
||||
this.busy = true;
|
||||
setTimeout(() => {
|
||||
this.bufferLen += defaultBufferLen;
|
||||
this.tools.slice(this.bufferLen - defaultBufferLen, this.bufferLen).forEach(async (tool) => {
|
||||
await this.fetchHelp(tool);
|
||||
});
|
||||
this.busy = false;
|
||||
}, loadTimeout);
|
||||
} else if (this.bufferLen >= this.tools.length) {
|
||||
this.allLoaded = true;
|
||||
}
|
||||
},
|
||||
async fetchHelp(tool) {
|
||||
await fetchData(`api/tools/${tool.id}/build`).then((response) => {
|
||||
const help = response.help;
|
||||
Vue.set(tool, "_showDetails", false); // maybe not needed
|
||||
if (help && help != "\n") {
|
||||
Vue.set(tool, "help", help);
|
||||
Vue.set(tool, "summary", this.parseHelp(help));
|
||||
} else {
|
||||
Vue.set(tool, "help", ""); // for cases where helpText == '\n'
|
||||
}
|
||||
});
|
||||
},
|
||||
parseHelp(help) {
|
||||
let summary = "";
|
||||
const parser = new DOMParser();
|
||||
const helpDoc = parser.parseFromString(help, "text/html");
|
||||
const xpaths = [
|
||||
"//strong[text()='What it does']/../following-sibling::*",
|
||||
"//strong[text()='What It Does']/../following-sibling::*",
|
||||
"//h1[text()='Synopsis']/following-sibling::*",
|
||||
"//strong[text()='Syntax']/../following-sibling::*",
|
||||
];
|
||||
const matches = [];
|
||||
xpaths.forEach((xpath) => {
|
||||
matches.push(
|
||||
helpDoc.evaluate(xpath, helpDoc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
|
||||
);
|
||||
});
|
||||
matches.forEach((match) => {
|
||||
if (match) {
|
||||
summary += match.innerHTML + "\n";
|
||||
}
|
||||
});
|
||||
return summary;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -32,7 +32,7 @@ getAPIKey();
|
||||
|
||||
<template>
|
||||
<section class="api-key d-flex flex-column">
|
||||
<h1 class="h-lg" v-localize>Manage API Key</h1>
|
||||
<h1 v-localize class="h-lg">Manage API Key</h1>
|
||||
|
||||
<span v-localize class="mb-2">
|
||||
An API key will allow you to access via web API. Please note that this key acts as an alternate means to
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</b-alert>
|
||||
|
||||
<hgroup class="cloud-auth-title">
|
||||
<h1 class="h-lg" v-localize>Manage Cloud Authorization</h1>
|
||||
<h1 v-localize class="h-lg">Manage Cloud Authorization</h1>
|
||||
<nav class="operations">
|
||||
<ul>
|
||||
<li class="cloudKeyHelp">
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="text-center my-5">
|
||||
<div v-if="allSourcesUnlimited">
|
||||
<h2>You've got <b>unlimited</b> disk quota</h2>
|
||||
<h3 class="h-sm" v-localize>All your storage sources have unlimited disk space. Enjoy!</h3>
|
||||
<h3 v-localize class="h-sm">All your storage sources have unlimited disk space. Enjoy!</h3>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h2>
|
||||
You've got <b> {{ niceTotalQuota }} </b> of total disk quota
|
||||
</h2>
|
||||
<h3 class="h-sm" v-localize>
|
||||
<h3 v-localize class="h-sm">
|
||||
This is the maximum disk space that you can use across all your storage sources. Unlimited storage
|
||||
sources are not taken into account
|
||||
</h3>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<b-container fluid class="p-0">
|
||||
<h1 class="h-lg" v-localize>User preferences</h1>
|
||||
<h1 v-localize class="h-lg">User preferences</h1>
|
||||
<b-alert :variant="messageVariant" :show="!!message">
|
||||
{{ message }}
|
||||
</b-alert>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Published :details="visualization">
|
||||
<Published :item="visualization">
|
||||
<template v-slot>
|
||||
<CenterFrame :src="getUrl" />
|
||||
</template>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<FormElement
|
||||
:id="actionNames.ChangeDatatypeAction__newtype"
|
||||
:value="formData[actionNames.ChangeDatatypeAction__newtype]"
|
||||
:options="datatypeExtensions"
|
||||
:attributes="{ options: datatypeExtensions }"
|
||||
title="Change datatype"
|
||||
type="select"
|
||||
backbonejs
|
||||
|
||||
@@ -229,6 +229,7 @@ import WorkflowAttributes from "./Attributes";
|
||||
import ZoomControl from "./ZoomControl";
|
||||
import WorkflowNode from "./Node";
|
||||
import Vue from "vue";
|
||||
import { ConfirmDialog } from "composables/confirmDialog";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -362,6 +363,11 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
id(newId, oldId) {
|
||||
if (oldId) {
|
||||
this._loadCurrent(newId);
|
||||
}
|
||||
},
|
||||
annotation(newAnnotation, oldAnnotation) {
|
||||
if (newAnnotation != oldAnnotation) {
|
||||
this.hasChanges = true;
|
||||
@@ -490,7 +496,7 @@ export default {
|
||||
this.showInPanel = "attributes";
|
||||
},
|
||||
onEditSubworkflow(contentId) {
|
||||
const editUrl = `/workflows/edit?id=${contentId}`;
|
||||
const editUrl = `/workflows/edit?workflow_id=${contentId}`;
|
||||
this.onNavigate(editUrl);
|
||||
},
|
||||
async onClone(node) {
|
||||
@@ -536,7 +542,7 @@ export default {
|
||||
if (stepCount < 10) {
|
||||
this.copyIntoWorkflow(workflowId);
|
||||
} else {
|
||||
const confirmed = await this.$bvModal.msgBoxConfirm(
|
||||
const confirmed = await ConfirmDialog.confirm(
|
||||
`Warning this will add ${stepCount} new steps into your current workflow. You may want to consider using a subworkflow instead.`
|
||||
);
|
||||
if (confirmed) {
|
||||
@@ -563,7 +569,7 @@ export default {
|
||||
.then((response) => {
|
||||
this.onWorkflowMessage("Workflow saved as", "success");
|
||||
this.hideModal();
|
||||
this.onNavigate(`${getAppRoot()}workflow/editor?id=${response.data}`, true);
|
||||
this.onNavigate(`${getAppRoot()}workflows/edit?id=${response.data}`, true);
|
||||
})
|
||||
.catch((response) => {
|
||||
this.onWorkflowError("Saving workflow failed, please contact an administrator.");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as d3 from "d3";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
|
||||
// Rendering parameters
|
||||
const zIndex = 1000;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import TrsTool from "./TrsTool";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { Services } from "../services";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { safePath } from "utils/redirect";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { watch } from "vue";
|
||||
import { BButton } from "bootstrap-vue";
|
||||
import { useTaskMonitor } from "composables/useTaskMonitor";
|
||||
import { useTaskMonitor } from "composables/taskMonitor";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
const { isRunning, isCompleted, hasFailed, requestHasFailed, waitForTask } = useTaskMonitor();
|
||||
|
||||
@@ -6,8 +6,8 @@ import ActionButton from "./ActionButton.vue";
|
||||
import StsDownloadButton from "components/StsDownloadButton.vue";
|
||||
import ExportToRemoteButton from "components/Workflow/Invocation/Export/ExportToRemoteButton.vue";
|
||||
import ExportToRemoteModal from "components/Workflow/Invocation/Export/ExportToRemoteModal.vue";
|
||||
import { useMarkdown } from "composables/useMarkdown";
|
||||
import { Toast } from "ui/toast";
|
||||
import { useMarkdown } from "composables/markdown";
|
||||
import { Toast } from "composables/toast";
|
||||
import axios from "axios";
|
||||
|
||||
const { renderMarkdown } = useMarkdown({ openLinksInNewPage: true });
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
import { ref, reactive, inject } from "vue";
|
||||
import { BModal } from "bootstrap-vue";
|
||||
import axios from "axios";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import { safePath } from "utils/redirect";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import ExternalLink from "components/ExternalLink";
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<script>
|
||||
import { safePath } from "utils/redirect";
|
||||
import { urlData } from "utils/url";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Toast } from "composables/toast";
|
||||
import ConfigProvider from "components/providers/ConfigProvider";
|
||||
import Markdown from "components/Markdown/Markdown";
|
||||
import Vue from "vue";
|
||||
|
||||
+12
-6
@@ -1,4 +1,4 @@
|
||||
import Invocations from "./Invocations";
|
||||
import InvocationsList from "./InvocationsList";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
@@ -6,11 +6,14 @@ import { getLocalVue } from "jest/helpers";
|
||||
import mockInvocationData from "./test/json/invocation.json";
|
||||
import { parseISO, formatDistanceToNow } from "date-fns";
|
||||
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
import "jest-location-mock";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("Invocations.vue", () => {
|
||||
const pinia = createPinia();
|
||||
describe("InvocationsList.vue", () => {
|
||||
let axiosMock;
|
||||
let wrapper;
|
||||
|
||||
@@ -28,7 +31,7 @@ describe("Invocations.vue", () => {
|
||||
const propsData = {
|
||||
ownerGrid: false,
|
||||
};
|
||||
wrapper = mount(Invocations, {
|
||||
wrapper = mount(InvocationsList, {
|
||||
propsData,
|
||||
localVue,
|
||||
});
|
||||
@@ -49,9 +52,10 @@ describe("Invocations.vue", () => {
|
||||
const propsData = {
|
||||
ownerGrid: false,
|
||||
};
|
||||
wrapper = mount(Invocations, {
|
||||
wrapper = mount(InvocationsList, {
|
||||
propsData,
|
||||
localVue,
|
||||
pinia,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,9 +72,10 @@ describe("Invocations.vue", () => {
|
||||
storedWorkflowName: "My Workflow",
|
||||
storedWorkflowId: "abcde145678",
|
||||
};
|
||||
wrapper = mount(Invocations, {
|
||||
wrapper = mount(InvocationsList, {
|
||||
propsData,
|
||||
localVue,
|
||||
pinia,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,7 +97,7 @@ describe("Invocations.vue", () => {
|
||||
ownerGrid: false,
|
||||
loading: false,
|
||||
};
|
||||
wrapper = mount(Invocations, {
|
||||
wrapper = mount(InvocationsList, {
|
||||
propsData,
|
||||
computed: {
|
||||
getWorkflowNameByInstanceId: (state) => (id) => "workflow name",
|
||||
@@ -113,6 +118,7 @@ describe("Invocations.vue", () => {
|
||||
},
|
||||
},
|
||||
localVue,
|
||||
pinia,
|
||||
});
|
||||
});
|
||||
|
||||
+11
-4
@@ -80,14 +80,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapCacheActions } from "vuex-cache";
|
||||
import { mapGetters } from "vuex";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { invocationsProvider } from "components/providers/InvocationsProvider";
|
||||
import WorkflowInvocationState from "components/WorkflowInvocationState/WorkflowInvocationState";
|
||||
import WorkflowRunButton from "./WorkflowRunButton.vue";
|
||||
import UtcDate from "components/UtcDate";
|
||||
import { mapCacheActions } from "vuex-cache";
|
||||
import { mapGetters } from "vuex";
|
||||
import { useWorkflowStore } from "stores/workflowStore";
|
||||
import paginationMixin from "./paginationMixin";
|
||||
|
||||
export default {
|
||||
@@ -125,7 +128,11 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(["getWorkflowNameByInstanceId", "getWorkflowByInstanceId", "getStoredWorkflowIdByInstanceId"]),
|
||||
...mapState(useWorkflowStore, [
|
||||
"getWorkflowNameByInstanceId",
|
||||
"getWorkflowByInstanceId",
|
||||
"getStoredWorkflowIdByInstanceId",
|
||||
]),
|
||||
...mapGetters("history", ["getHistoryById", "getHistoryNameById"]),
|
||||
title() {
|
||||
let title = `Workflow Invocations`;
|
||||
@@ -160,7 +167,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapCacheActions(["fetchWorkflowForInstanceId"]),
|
||||
...mapActions(useWorkflowStore, ["fetchWorkflowForInstanceId"]),
|
||||
...mapCacheActions("history", ["loadHistoryById"]),
|
||||
async provider(ctx) {
|
||||
ctx.root = this.root;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user