mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge branch 'dev' of https://github.com/galaxyproject/galaxy into beacon-integration
This commit is contained in:
@@ -9,9 +9,7 @@ packages/venv
|
||||
node_modules
|
||||
database
|
||||
doc/build
|
||||
doc/source/conf.py
|
||||
eggs
|
||||
lib/galaxy/util/jstree.py
|
||||
lib/galaxy/web/proxy/js/node_modules
|
||||
static/maps
|
||||
static/scripts
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
hda
|
||||
@@ -3,8 +3,7 @@
|
||||
# B008 Do not perform function calls in argument defaults (for FastAPI Depends and Body)
|
||||
# E203 is whitespace before ':'; we follow black's formatting here. See https://black.readthedocs.io/en/stable/faq.html#why-are-flake8-s-e203-and-w503-violated
|
||||
# E402 module level import not at top of file # TODO, we would like to improve this.
|
||||
# E501 is line length
|
||||
# E501 is line length (delegated to black)
|
||||
# W503 is line breaks before binary operators, which has been reversed in PEP 8.
|
||||
# D** are docstring linting - which we mostly ignore except D302. (Hopefully we will solve more over time).
|
||||
ignore = B008,E203,E402,E501,W503,D100,D101,D102,D103,D104,D105,D106,D107,D200,D201,D202,D204,D205,D206,D207,D208,D209,D210,D211,D300,D301,D400,D401,D402,D403,D412,D413
|
||||
exclude = lib/galaxy/util/jstree.py
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: OpenAPI linting
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'client/**'
|
||||
- 'doc/**'
|
||||
- 'lib/galaxy_test/selenium/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'client/**'
|
||||
- 'doc/**'
|
||||
- 'lib/galaxy_test/selenium/**'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
validate-schema:
|
||||
name: Validate OpenAPI schema
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.7']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: 'galaxy root'
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Get full Python version
|
||||
id: full-python-version
|
||||
shell: bash
|
||||
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
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }}
|
||||
- name: Cache galaxy venv
|
||||
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') }}-api
|
||||
- name: Install dependencies
|
||||
run: ./scripts/common_startup.sh --skip-client-build
|
||||
working-directory: 'galaxy root'
|
||||
- name: Lint schema
|
||||
run: make lint-api-schema
|
||||
working-directory: 'galaxy root'
|
||||
- name: Build typescript schema
|
||||
run: make update-client-api-schema
|
||||
working-directory: 'galaxy root'
|
||||
- name: Check for changes
|
||||
run: |
|
||||
if [[ `git status --porcelain` ]]; then
|
||||
echo "Rebuilding client/src/schema/schema.ts resulted in changes, run 'make update-client-api-schema' and commit results"
|
||||
exit 1
|
||||
fi
|
||||
working-directory: 'galaxy root'
|
||||
@@ -1,6 +1,5 @@
|
||||
[settings]
|
||||
combine_as_imports=true
|
||||
extend_skip=doc/source/conf.py,lib/galaxy/util/jstree.py
|
||||
force_alphabetical_sort_within_sections=true
|
||||
# Override force_grid_wrap value from profile=black, but black is still happy
|
||||
force_grid_wrap=2
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# This file instructs Redocly's linter to ignore the rules contained for specific parts of your API.
|
||||
# See https://redoc.ly/docs/cli/ for more information.
|
||||
_schema.yaml:
|
||||
no-empty-servers:
|
||||
- '#/openapi'
|
||||
no-ambiguous-paths:
|
||||
- >-
|
||||
#/paths/~1api~1histories~1{history_id}~1contents~1{dataset_id}~1permissions
|
||||
- >-
|
||||
#/paths/~1api~1histories~1{history_id}~1contents~1{history_content_id}~1display
|
||||
- >-
|
||||
#/paths/~1api~1histories~1{history_id}~1contents~1{history_content_id}~1extra_files
|
||||
- >-
|
||||
#/paths/~1api~1histories~1{history_id}~1contents~1{history_content_id}~1metadata_file
|
||||
- '#/paths/~1api~1histories~1{history_id}~1contents~1{id}~1validate'
|
||||
- '#/paths/~1api~1histories~1{history_id}~1contents~1{type}s~1{id}'
|
||||
@@ -0,0 +1,5 @@
|
||||
organization: galaxyproject.org
|
||||
extends:
|
||||
- recommended
|
||||
rules:
|
||||
operation-4xx-response: off
|
||||
+2
-2
@@ -157,8 +157,8 @@ https://help.github.com/en/github/getting-started-with-github/git-and-github-lea
|
||||
- Galaxy follows [PEP-8](https://www.python.org/dev/peps/pep-0008/), with
|
||||
particular emphasis on readability being the ultimate goal:
|
||||
- 4 spaces (not tabs!) per indentation level
|
||||
- divergences from PEP-8 are listed in the `[flake8]` section of the
|
||||
`.flake8` file
|
||||
- divergences from PEP-8 are listed in the `[flake8]` section of the `.flake8`
|
||||
file and in the `[tool.ruff]` section of the `pyproject.toml` file.
|
||||
- The Python code base is automatically formatted using
|
||||
[isort](https://pycqa.github.io/isort/) (for imports) and
|
||||
[black](https://black.readthedocs.io). To easily format your Python code
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ The following individuals have contributed code to Galaxy:
|
||||
* Patrick Austin <patrick.austin@stfc.ac.uk>
|
||||
* Raj Ayyampalayam <raj76@uga.edu>
|
||||
* Abdulrahman Azab <eng.azab@gmail.com>
|
||||
* Finn Bacall <finn.bacall@cs.man.ac.uk>
|
||||
* Finn Bacall <finn.bacall@manchester.ac.uk>
|
||||
* Dannon Baker <dannon.baker@gmail.com>
|
||||
* balto <balto_59@hotmail.fr>
|
||||
* Christopher Bare <christopherbare@gmail.com>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Location of virtualenv used for development.
|
||||
VENV?=.venv
|
||||
# Source virtualenv to execute command (flake8, sphinx, twine, etc...)
|
||||
# Source virtualenv to execute command (darker, sphinx, twine, etc...)
|
||||
IN_VENV=if [ -f "$(VENV)/bin/activate" ]; then . "$(VENV)/bin/activate"; fi;
|
||||
RELEASE_CURR:=23.0
|
||||
RELEASE_UPSTREAM:=upstream
|
||||
@@ -170,8 +170,20 @@ else
|
||||
endif
|
||||
|
||||
|
||||
update-client-api-schema: node-deps
|
||||
$(IN_VENV) cd client && python ../scripts/dump_openapi_schema.py | yarn run openapi-typescript --output src/schema/schema.ts && npx prettier --write src/schema/schema.ts
|
||||
build-api-schema:
|
||||
$(IN_VENV) python scripts/dump_openapi_schema.py _schema.yaml
|
||||
|
||||
remove-api-schema:
|
||||
rm _schema.yaml
|
||||
|
||||
update-client-api-schema: node-deps build-api-schema
|
||||
$(IN_VENV) cd client && node openapi_to_schema.mjs ../_schema.yaml > src/schema/schema.ts && npx prettier --write src/schema/schema.ts
|
||||
$(MAKE) remove-api-schema
|
||||
|
||||
lint-api-schema: build-api-schema
|
||||
$(IN_VENV) npx --yes @redocly/cli lint _schema.yaml
|
||||
$(IN_VENV) codespell -I .ci/ignore-spelling.txt _schema.yaml
|
||||
$(MAKE) remove-api-schema
|
||||
|
||||
client: node-deps ## Rebuild client-side artifacts for local development.
|
||||
cd client && yarn run build
|
||||
|
||||
+2
-1
@@ -54,10 +54,11 @@ embargo, we will:
|
||||
- Patch the oldest release within the 12 month support window, and merge that fix forward.
|
||||
- Updates will be available on the `release_XX.YY` branches.
|
||||
- Update each release branch
|
||||
- Post a notice to the [galaxy-announce mailing list](https://lists.galaxyproject.org/listinfo/galaxy-announce) with:
|
||||
- Publish a repository security advisory on GitHub containing:
|
||||
- A description of the issue
|
||||
- List of supported versions that are affected
|
||||
- Steps to update or patch your Galaxy
|
||||
- A CVE identifier (if applicable)
|
||||
|
||||
If an issue is deemed to be time-sensitive – e.g. due to active and ongoing exploits in the wild – the embargo may be shortened considerably.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"rules": {
|
||||
// Standard rules
|
||||
"no-console": "off",
|
||||
"no-unused-vars": ["error", { "args": "none" }],
|
||||
"no-unused-vars": ["error", { "args": "none", "varsIgnorePattern": "[iI]gnoreUnused.*"}],
|
||||
"prefer-const": "error",
|
||||
"one-var": ["error", "never"],
|
||||
"curly": "error",
|
||||
|
||||
@@ -87,7 +87,7 @@ While simpler in this example, you may need to manually mock more return values
|
||||
|
||||
## 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).
|
||||
Composables can be of great use to extract any reactive code from your components. For an example of this, take a look at [useFilterObjectArray](https://github.com/galaxyproject/galaxy/blob/dev/client/src/composables/utils/filter.js).
|
||||
|
||||
Usage:
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ mandatory markup, just one big empty slot.
|
||||
// Testing a renderless component
|
||||
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue, waitForLifecyleEvent } from "jest/helpers";
|
||||
import { getLocalVue, waitForLifecyleEvent } from "tests/jest/helpers"
|
||||
import DoodadProvider from "./DoodadProvider";
|
||||
|
||||
describe("A renderless component", () => {
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
# Styleguide
|
||||
|
||||
Most of the client's code style is handled by Prettier. Prettier does a good job of keeping an overall consistent code style, however there are some cases it cannot account for.
|
||||
This document serves as a guide on how to style your code in such cases, with explanations as to why.
|
||||
Treat it more like a set of recommendations than hard rules.
|
||||
|
||||
## Naming
|
||||
|
||||
Do not abbreviate. This includes naming variables, functions and modules.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```js
|
||||
> function errorMessageTemplate(workflowfName, errorMessage) {
|
||||
> return `Failed to run ${workflowfName}. ${errorMessage}`;
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```js
|
||||
> function eMsgTmpl(wfName, msg) {
|
||||
> return `Failed to run ${wfName}. ${msg}`;
|
||||
> }
|
||||
> ```
|
||||
|
||||
> **Reason**
|
||||
>
|
||||
> While abbreviation may save a few keystrokes now, it will make the code harder to understand, and therefore maintain. Even when you think the abbreviations are obvious within this context, consider people looking at your code in the future might not have the same context you do when writing the code.
|
||||
|
||||
## Functions
|
||||
|
||||
There are several ways to define functions in JavaScript.
|
||||
|
||||
```js
|
||||
// named function
|
||||
function myFunction(param) {
|
||||
// do stuff
|
||||
}
|
||||
|
||||
// arrow functions
|
||||
const myFunction = (param) => {
|
||||
// do stuff
|
||||
};
|
||||
|
||||
// anonymous functions
|
||||
const myFunction = function(param) {
|
||||
//do stuff
|
||||
};
|
||||
```
|
||||
|
||||
Only use the first two.
|
||||
`anonymous functions` have mostly been superseded by `arrow functions`
|
||||
|
||||
### When to use the named functions
|
||||
|
||||
Use named functions in the top-level module scope and to declare class methods.
|
||||
|
||||
> **Reason**
|
||||
>
|
||||
> `function` is easy to process and understand at a glance. In module scope, arrow functions offer no benefit over regular functions, and they are not allowed as class methods.
|
||||
|
||||
### When to use arrow functions
|
||||
|
||||
Use arrow functions when declaring temporary functions within other scopes. This can be within another function, or inside a method that expects a callback function (eg. `array.forEach()`).
|
||||
|
||||
> **Reason**
|
||||
>
|
||||
> Arrow functions offer benefits about the ambiguity of the `this` keyword within other scopes, as they do not provide their own `this` context.
|
||||
>
|
||||
> Binding them to a variable, also makes it clear that this function only exists within said scope, just like any other scoped variable.
|
||||
|
||||
### When to use anonymous functions
|
||||
|
||||
When possible, use arrow functions instead.
|
||||
|
||||
### Examples
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```js
|
||||
> // in myModules.js
|
||||
>
|
||||
> export function myFunction(parameter) {
|
||||
> const addOne = (value) => {
|
||||
> return value + 1;
|
||||
> }
|
||||
> // do more stuff...
|
||||
> }
|
||||
>
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```js
|
||||
> // in myModules.js
|
||||
>
|
||||
> export const myFunction = (parameter) => {
|
||||
> const addOne = function(value) {
|
||||
> return value + 1;
|
||||
> }
|
||||
> // do more stuff...
|
||||
> }
|
||||
>
|
||||
> ```
|
||||
|
||||
## HTML Multi-Line Layout
|
||||
|
||||
Prettier tries to respect whitespace when formatting your HTML templates, even when it doesn't need to. So for example this code:
|
||||
|
||||
```vue
|
||||
<b-button class="danger-button mb-4" variant="danger" @click="onDangerButtonClick">A very Long Button Text</b-button>
|
||||
```
|
||||
|
||||
Might get turned into:
|
||||
|
||||
```vue
|
||||
<b-button class="danger-button mb-4" variant="danger" @click="onDangerButtonClick"
|
||||
>A very Long Button Text</b-button
|
||||
>
|
||||
```
|
||||
|
||||
Notice the strange positioning of the `>` brackets.
|
||||
|
||||
In this case the formatting is equivalent to the much more readable:
|
||||
|
||||
```vue
|
||||
<b-button class="danger-button mb-4" variant="danger" @click="onDangerButtonClick">
|
||||
A very Long Button Text
|
||||
</b-button>
|
||||
```
|
||||
|
||||
Prettier does not know if our element has significant whitespace, or not. Check if your element has significant whitespace, and if it does not, reformat the HTML to avoid disjointed brackets.
|
||||
|
||||
[Further reading about significant whitespace](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace)
|
||||
|
||||
## Spacing
|
||||
|
||||
Prettier doesn't add empty lines to your code, but they can help in making it more readable.
|
||||
|
||||
### Javascript
|
||||
|
||||
Add an empty line between a block of variable definitions and other code.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```js
|
||||
> let a = 5;
|
||||
> let b = 6;
|
||||
>
|
||||
> console.log(a + b);
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```js
|
||||
> let a = 5;
|
||||
> let b = 6;
|
||||
> console.log(a + b);
|
||||
> ```
|
||||
|
||||
Add space between scopes.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```js
|
||||
> function myFunction(parameter) {
|
||||
> if (condition) {
|
||||
> // do stuff...
|
||||
> }
|
||||
>
|
||||
> if (otherCondition) {
|
||||
> // do more stuff...
|
||||
> }
|
||||
> }
|
||||
>
|
||||
> function otherFunction() {
|
||||
> // do other stuff...
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```js
|
||||
> function myFunction(parameter) {
|
||||
> if (condition) {
|
||||
> // do stuff...
|
||||
> }
|
||||
> if (otherCondition) {
|
||||
> // do more stuff...
|
||||
> }
|
||||
> }
|
||||
> function otherFunction() {
|
||||
> // do other stuff...
|
||||
> }
|
||||
> ```
|
||||
|
||||
Add space between scopes and other code.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```js
|
||||
> const myConstant = 5;
|
||||
>
|
||||
> if (myConstant === 5) {
|
||||
> // do stuff...
|
||||
> }
|
||||
>
|
||||
> console.log("log stuff");
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```js
|
||||
> const myConstant = 5;
|
||||
> if (myConstant === 5) {
|
||||
> // do stuff...
|
||||
> }
|
||||
> console.log("log stuff");
|
||||
> ```
|
||||
|
||||
### Vue Components
|
||||
|
||||
Add spaces between the `script`, `template` and `style` blocks.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```vue
|
||||
> <script setup>
|
||||
> // do stuff...
|
||||
> </script>
|
||||
>
|
||||
> <template>
|
||||
> <!--template stuff-->
|
||||
> </template>
|
||||
>
|
||||
> <style lang="scss" scoped>
|
||||
> // stlye stuff...
|
||||
> </style>
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```vue
|
||||
> <script setup>
|
||||
> // do stuff...
|
||||
> </script>
|
||||
> <template>
|
||||
> <!--template stuff-->
|
||||
> </template>
|
||||
> <style lang="scss" scoped>
|
||||
> // stlye stuff...
|
||||
> </style>
|
||||
> ```
|
||||
|
||||
### Vue Templates
|
||||
|
||||
Do not add space between elements connected by conditionals.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```vue
|
||||
> <div>
|
||||
> <span v-if="conditional">
|
||||
> condition met
|
||||
> </span>
|
||||
> <span v-else>
|
||||
> condition not met
|
||||
> </span>
|
||||
> </div>
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```vue
|
||||
> <div>
|
||||
> <span v-if="conditional">
|
||||
> condition met
|
||||
> </span>
|
||||
>
|
||||
> <span v-else>
|
||||
> condition not met
|
||||
> </span>
|
||||
> </div>
|
||||
> ```
|
||||
|
||||
Add space between non-connected elements.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```vue
|
||||
> <div>
|
||||
> <span>
|
||||
> First span.
|
||||
> </span>
|
||||
>
|
||||
> <span>
|
||||
> Second span.
|
||||
> </span>
|
||||
> </div>
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```vue
|
||||
> <div>
|
||||
> <span>
|
||||
> First span.
|
||||
> </span>
|
||||
> <span>
|
||||
> Second span.
|
||||
> </span>
|
||||
> </div>
|
||||
> ```
|
||||
|
||||
Add space between logical blocks of elements.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```vue
|
||||
> <div>
|
||||
> <span v-if="conditional">
|
||||
> condition 1 met
|
||||
> </span>
|
||||
> <span v-else>
|
||||
> condition 1 not met
|
||||
> </span>
|
||||
>
|
||||
> <span v-if="otherConditional">
|
||||
> condition 2 met
|
||||
> </span>
|
||||
> <span v-else>
|
||||
> condition 2 not met
|
||||
> </span>
|
||||
> </div>
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```vue
|
||||
> <div>
|
||||
> <span v-if="conditional">
|
||||
> condition 1 met
|
||||
> </span>
|
||||
> <span v-else>
|
||||
> condition 1 not met
|
||||
> </span>
|
||||
> <span v-if="otherConditional">
|
||||
> condition 2 met
|
||||
> </span>
|
||||
> <span v-else>
|
||||
> condition 2 not met
|
||||
> </span>
|
||||
> </div>
|
||||
> ```
|
||||
|
||||
## Casting
|
||||
|
||||
Use explicit boolean casting.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```js
|
||||
> const condition = Boolean(value);
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```js
|
||||
> const condition = !!value;
|
||||
> ```
|
||||
|
||||
> **Reason**
|
||||
>
|
||||
> `!!` is not an intentional casting operator. The casting is a side-effect of the `!` operator. Using explicit casting is easier to understand at a glance.
|
||||
|
||||
## Equality
|
||||
|
||||
When possible, use strict equality.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```js
|
||||
> if (value === 5) {
|
||||
> // do stuff...
|
||||
> }
|
||||
>
|
||||
> if (otherValue !== 6) {
|
||||
> // do more stuff...
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```js
|
||||
> if (value == 5) {
|
||||
> // do stuff...
|
||||
> }
|
||||
>
|
||||
> if (otherValue != 6) {
|
||||
> // do more stuff...
|
||||
> }
|
||||
> ```
|
||||
|
||||
> **Reason**
|
||||
>
|
||||
> Strict equality simply checks if two values are equal. Loose equality does type conversion under the hood and is a lot more complicated. While in most cases these do the same, due to the added complexity of loose equality, there are some edge cases where loose equality (`==`) can lead to unexpected problems.
|
||||
>
|
||||
> For consistency, treat `===` as the default for equality checking.
|
||||
>
|
||||
> [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Strict_equality)
|
||||
|
||||
## Default value assignment
|
||||
|
||||
Use `??` to assign default values.
|
||||
|
||||
> **Do**
|
||||
>
|
||||
> ```js
|
||||
> const value = parameter ?? 0;
|
||||
> ```
|
||||
>
|
||||
> **Don't**
|
||||
>
|
||||
> ```js
|
||||
> const value = parameter || 0;
|
||||
> ```
|
||||
|
||||
> **Reason**
|
||||
>
|
||||
> The `??` operator uses the right hand value, when the left hand one is unassigned (`undefined` or `null`), while `||` does this on all falsely values (eg. `false` or `0`). This can lead to unexpected bugs in edge cases.
|
||||
>
|
||||
> [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing#assigning_a_default_value_to_a_variable)
|
||||
|
||||
### Working with unused variables
|
||||
|
||||
Unused variables are marked as errors in eslint, however when destructuring objects to remove
|
||||
certain keys you may be left with a variable you will not use further. You can
|
||||
name the unused variable `ignoredUnused`. The `no-unused-vars` config setting in `client/.eslintrc`
|
||||
contains `"varsIgnorePattern": "[iI]gnoreUnused.*"}` to ignore unused variables if they
|
||||
start with `ignoredUnused` or `IgnoredUnused`.
|
||||
@@ -0,0 +1,14 @@
|
||||
// this is a helper script that fixes const values
|
||||
// upstream fix in https://github.com/drwpow/openapi-typescript/pull/1014
|
||||
import openapiTS from "openapi-typescript";
|
||||
|
||||
const inputFilePath = process.argv[2];
|
||||
|
||||
const localPath = new URL(inputFilePath, import.meta.url);
|
||||
openapiTS(localPath, {
|
||||
transform(schemaObject, metadata) {
|
||||
if ("const" in schemaObject && schemaObject.type === "string") {
|
||||
return `"${schemaObject.const}"`;
|
||||
}
|
||||
},
|
||||
}).then((output) => console.log(output));
|
||||
+23
-22
@@ -24,16 +24,19 @@
|
||||
"dependencies": {
|
||||
"@fontsource/atkinson-hyperlegible": "^4.5.10",
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.6",
|
||||
"@handsontable/vue": "^2.0.0-beta1",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.9",
|
||||
"@handsontable/vue": "^2.0.0",
|
||||
"@hirez_io/observer-spy": "^2.1.2",
|
||||
"@johmun/vue-tags-input": "^2.1.0",
|
||||
"@pinia/testing": "^0.0.14",
|
||||
"@popperjs/core": "^2.11.5",
|
||||
"@sentry/browser": "^6.17.4",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@vueuse/core": "^9.10.0",
|
||||
"assert": "^2.0.0",
|
||||
"axios": "^0.25.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
@@ -43,7 +46,8 @@
|
||||
"citation-js": "^0.6.4",
|
||||
"core-js": "^3.21.0",
|
||||
"csv-parse": "^5.3.0",
|
||||
"d3": "3",
|
||||
"d3": "^7.8.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"date-fns": "^2.28.0",
|
||||
"date-fns-tz": "^1.3.3",
|
||||
"decode-uri-component": "^0.2.1",
|
||||
@@ -53,7 +57,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"flush-promises": "^1.0.2",
|
||||
"glob": "^7.2.0",
|
||||
"handsontable": "^2.0.0",
|
||||
"handsontable": "^4.0.0",
|
||||
"hsluv": "^0.1.0",
|
||||
"imask": "^6.4.0",
|
||||
"jquery": "2",
|
||||
@@ -64,6 +68,7 @@
|
||||
"linkify-html": "^3.0.5",
|
||||
"linkifyjs": "^3.0.5",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-regexp": "^0.4.0",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -96,7 +101,7 @@
|
||||
"vue-prismjs": "^1.2.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-rx": "^6.2.0",
|
||||
"vue-virtual-scroll-list": "^2.3.3",
|
||||
"vue-virtual-scroll-list": "^2.3.4",
|
||||
"vuedraggable": "2.24.3",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-cache": "^3.4.0",
|
||||
@@ -128,28 +133,23 @@
|
||||
"eslint": "eslint -c .eslintrc.json --ext .js,.vue,.ts src tests/jest/standalone"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.9",
|
||||
"@babel/eslint-parser": "^7.18.9",
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"@babel/core": "^7.20.5",
|
||||
"@babel/helper-validator-identifier": "^7.19.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.18.9",
|
||||
"@babel/preset-env": "^7.18.9",
|
||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.1",
|
||||
"@typescript-eslint/parser": "^5.45.1",
|
||||
"@vue/test-utils": "^1.3.3",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"@vue/vue2-jest": "^29.1.0",
|
||||
"@vue/vue2-jest": "^29.2.1",
|
||||
"amdi18n-loader": "^0.9.3",
|
||||
"autoprefixer": "10.4.5",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^28.1.3",
|
||||
"babel-loader": "^8.2.5",
|
||||
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
|
||||
"babel-plugin-transform-vue-template": "^0.4.2",
|
||||
"babel-jest": "^29.3.1",
|
||||
"buffer": "^6.0.3",
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^4.0.0",
|
||||
@@ -162,9 +162,9 @@
|
||||
"gulp": "^4.0.2",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"imports-loader": "^4.0.0",
|
||||
"jest": "^27.4.7",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"jest-location-mock": "^1.0.9",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"json-loader": "^0.5.7",
|
||||
"karma": "^6.3.16",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
@@ -184,6 +184,7 @@
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-loader": "^9.4.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"typescript-eslint": "^0.0.1-alpha.0",
|
||||
"vue-loader": "^15.10.0",
|
||||
|
||||
@@ -5,7 +5,7 @@ import BASE_MVC from "./base-mvc";
|
||||
import userModel from "./user-model";
|
||||
import metricsLogger from "utils/metrics-logger";
|
||||
import addLogging from "utils/add-logging";
|
||||
import localize from "utils/localization";
|
||||
import { localize, _setUserLocale, _getUserLocale } from "utils/localization";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { create, dialog } from "utils/data";
|
||||
|
||||
@@ -77,8 +77,8 @@ GalaxyApp.prototype._init = function (options, bootstrapped) {
|
||||
this._initUser(options.user || {});
|
||||
this.debug("GalaxyApp.user: ", this.user);
|
||||
|
||||
this.localize._setUserLocale(this.user, this.config);
|
||||
this.localize._getUserLocale();
|
||||
_setUserLocale(this.user, this.config);
|
||||
_getUserLocale();
|
||||
this.debug("currentLocale: ", sessionStorage.getItem("currentLocale"));
|
||||
|
||||
this._setUpListeners();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -20,7 +20,6 @@ export { SweepsterVisualization, SweepsterVisualizationView } from "viz/sweepste
|
||||
export { createTabularDatasetChunkedView } from "mvc/dataset/data";
|
||||
export { default as LegacyGridView } from "legacy/grid/grid-view";
|
||||
export { create_chart, create_histogram } from "reports/run_stats";
|
||||
export { openGlobalUploadModal } from "components/Upload";
|
||||
export { runTour } from "components/Tour/runTour";
|
||||
export { Toast } from "ui/toast"; // TODO: remove when external consumers are updated/gone (IES right now)
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
import { computed } from "vue";
|
||||
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { useConfig } from "composables/config";
|
||||
import UtcDate from "components/UtcDate.vue";
|
||||
import License from "components/License/License.vue";
|
||||
import ExternalLink from "components/ExternalLink.vue";
|
||||
import { getAppRoot } from "@/onload/loadConfig";
|
||||
import { useConfig } from "@/composables/config";
|
||||
import UtcDate from "@/components/UtcDate.vue";
|
||||
import License from "@/components/License/License.vue";
|
||||
import ExternalLink from "@/components/ExternalLink.vue";
|
||||
|
||||
const { config, isLoaded } = useConfig();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useDetailedDatatypes } from "composables/datatypes";
|
||||
import { useFilterObjectArray } from "composables/utils/filter";
|
||||
import DelayedInput from "components/Common/DelayedInput.vue";
|
||||
import { useDetailedDatatypes, type DetailedDatatypes } from "@/composables/datatypes";
|
||||
import { useFilterObjectArray } from "@/composables/utils/filter";
|
||||
import DelayedInput from "@/components/Common/DelayedInput.vue";
|
||||
|
||||
const filter = ref("");
|
||||
const filterFields = ["extension"];
|
||||
const filterFields: Array<keyof DetailedDatatypes> = ["extension"];
|
||||
|
||||
const { datatypes } = useDetailedDatatypes();
|
||||
const filteredDatatypes = useFilterObjectArray(datatypes, filter, filterFields);
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<div>
|
||||
{{ prefix }}
|
||||
<span v-html="citationHtml" />
|
||||
<a v-if="link" :href="link" target="_blank">
|
||||
<font-awesome-icon v-b-tooltip.hover title="View Citation" icon="external-link-alt" />
|
||||
</a>
|
||||
<a v-if="link" :href="link" target="_blank">Visit Citation <font-awesome-icon icon="external-link-alt" /> </a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import ExportForm from "./ExportForm.vue";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
@@ -35,4 +35,35 @@ describe("ExportForm.vue", () => {
|
||||
it("should localize button text", async () => {
|
||||
expect(wrapper.find(".export-button").text()).toBeLocalizationOf("Export");
|
||||
});
|
||||
|
||||
it("should emit 'export' event with correct inputs on export button click", async () => {
|
||||
await wrapper.setData({
|
||||
name: "export.tar.gz",
|
||||
directory: "gxfiles://",
|
||||
});
|
||||
expect(wrapper.emitted()).not.toHaveProperty("export");
|
||||
|
||||
await wrapper.find(".export-button").trigger("click");
|
||||
|
||||
expect(wrapper.emitted()).toHaveProperty("export");
|
||||
expect(wrapper.emitted()["export"][0][0]).toBe("gxfiles://");
|
||||
expect(wrapper.emitted()["export"][0][1]).toBe("export.tar.gz");
|
||||
});
|
||||
|
||||
it("should clear the inputs after export when clearInputAfterExport is enabled", async () => {
|
||||
await wrapper.setProps({
|
||||
clearInputAfterExport: true,
|
||||
});
|
||||
await wrapper.setData({
|
||||
name: "export.tar.gz",
|
||||
directory: "gxfiles://",
|
||||
});
|
||||
expect(wrapper.vm.directory).toEqual("gxfiles://");
|
||||
expect(wrapper.vm.name).toEqual("export.tar.gz");
|
||||
|
||||
await wrapper.find(".export-button").trigger("click");
|
||||
|
||||
expect(wrapper.vm.directory).toBe(null);
|
||||
expect(wrapper.vm.name).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="export-to-remote-file">
|
||||
<b-form-group
|
||||
id="fieldset-directory"
|
||||
label-for="directory"
|
||||
@@ -32,6 +32,10 @@ export default {
|
||||
type: String,
|
||||
default: "archive",
|
||||
},
|
||||
clearInputAfterExport: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -59,6 +63,10 @@ export default {
|
||||
methods: {
|
||||
doExport() {
|
||||
this.$emit("export", this.directory, this.name);
|
||||
if (this.clearInputAfterExport) {
|
||||
this.directory = null;
|
||||
this.name = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { BAlert, BCard, BCardTitle } from "bootstrap-vue";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ExportRecordModel } from "./models/exportRecordModel";
|
||||
|
||||
library.add(faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock);
|
||||
|
||||
const props = defineProps({
|
||||
record: {
|
||||
type: ExportRecordModel,
|
||||
required: true,
|
||||
},
|
||||
objectType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
actionMessage: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
actionMessageVariant: {
|
||||
type: String,
|
||||
default: "info",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["onReimport", "onDownload", "onActionMessageDismissed"]);
|
||||
|
||||
const title = computed(() => (props.record.isReady ? `Exported` : `Export started`));
|
||||
const preparingMessage = computed(
|
||||
() => `Preparing export. This may take some time depending on the size of your ${props.objectType}`
|
||||
);
|
||||
|
||||
async function reimportObject() {
|
||||
emit("onReimport", props.record);
|
||||
}
|
||||
|
||||
function downloadObject() {
|
||||
emit("onDownload", props.record);
|
||||
}
|
||||
|
||||
function onMessageDismissed() {
|
||||
emit("onActionMessageDismissed");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-card class="export-record-details">
|
||||
<b-card-title>
|
||||
<b>{{ title }}</b> {{ props.record.elapsedTime }}
|
||||
</b-card-title>
|
||||
<p v-if="!props.record.isPreparing">
|
||||
Format: <b class="record-archive-format">{{ props.record.modelStoreFormat }}</b>
|
||||
</p>
|
||||
<span v-if="props.record.isPreparing">
|
||||
<loading-span :message="preparingMessage" />
|
||||
</span>
|
||||
<div v-else>
|
||||
<div v-if="props.record.hasFailed">
|
||||
<font-awesome-icon
|
||||
icon="exclamation-circle"
|
||||
class="text-danger record-failed-icon"
|
||||
title="Export failed" />
|
||||
<span>
|
||||
Something failed during this export. Please try again and if the problem persist contact your
|
||||
administrator.
|
||||
</span>
|
||||
<b-alert show variant="danger">{{ props.record.errorMessage }}</b-alert>
|
||||
</div>
|
||||
<div v-else-if="props.record.isUpToDate" title="Up to date">
|
||||
<font-awesome-icon icon="check-circle" class="text-success record-up-to-date-icon" />
|
||||
<span> This export record contains the latest changes of the {{ props.objectType }}. </span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<font-awesome-icon icon="exclamation-triangle" class="text-warning record-outdated-icon" />
|
||||
<span>
|
||||
This export is outdated and contains the changes of this {{ props.objectType }} from
|
||||
{{ props.record.elapsedTime }}.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="props.record.canExpire" class="mt-3">
|
||||
<span v-if="props.record.hasExpired">
|
||||
<font-awesome-icon icon="clock" class="text-danger record-expired-icon" /> This download link has
|
||||
expired.
|
||||
</span>
|
||||
<span v-else>
|
||||
<font-awesome-icon icon="clock" class="text-warning record-expiration-warning-icon" /> This download
|
||||
link expires {{ props.record.expirationElapsedTime }}.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div v-if="props.record.isReady">
|
||||
<p class="mt-3">You can do the following actions with this {{ props.objectType }} export:</p>
|
||||
<b-alert
|
||||
v-if="props.actionMessage !== null"
|
||||
:variant="props.actionMessageVariant"
|
||||
show
|
||||
fade
|
||||
dismissible
|
||||
@dismissed="onMessageDismissed">
|
||||
{{ props.actionMessage }}
|
||||
</b-alert>
|
||||
<div v-else class="actions">
|
||||
<b-button
|
||||
v-if="props.record.canDownload"
|
||||
class="record-download-btn"
|
||||
variant="primary"
|
||||
@click="downloadObject">
|
||||
Download
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="props.record.canReimport"
|
||||
class="record-reimport-btn"
|
||||
variant="primary"
|
||||
@click="reimportObject">
|
||||
Reimport
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-card>
|
||||
</template>
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { BCard, BButton, BButtonGroup, BButtonToolbar, BCollapse, BTable, BLink } from "bootstrap-vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faExclamationCircle,
|
||||
faCheckCircle,
|
||||
faDownload,
|
||||
faFileImport,
|
||||
faSpinner,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faExclamationCircle, faCheckCircle, faDownload, faFileImport, faSpinner);
|
||||
|
||||
const props = defineProps({
|
||||
records: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["onReimport", "onDownload"]);
|
||||
|
||||
const fields = [
|
||||
{ key: "elapsedTime", label: "Exported" },
|
||||
{ key: "format", label: "Format" },
|
||||
{ key: "expires", label: "Expires" },
|
||||
{ key: "isUpToDate", label: "Up to date", class: "text-center" },
|
||||
{ key: "isReady", label: "Ready", class: "text-center" },
|
||||
{ key: "actions", label: "Actions" },
|
||||
];
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const title = computed(() => (isExpanded.value ? `Hide export records` : `Show export records`));
|
||||
|
||||
async function reimportObject(record) {
|
||||
emit("onReimport", record);
|
||||
}
|
||||
|
||||
function downloadObject(record) {
|
||||
emit("onDownload", record);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-link
|
||||
:class="isExpanded ? null : 'collapsed'"
|
||||
:aria-expanded="isExpanded ? 'true' : 'false'"
|
||||
aria-controls="collapse-previous"
|
||||
@click="isExpanded = !isExpanded">
|
||||
{{ title }}
|
||||
</b-link>
|
||||
<b-collapse id="collapse-previous" v-model="isExpanded">
|
||||
<b-card>
|
||||
<b-table :items="props.records" :fields="fields">
|
||||
<template v-slot:cell(elapsedTime)="row">
|
||||
<span :title="row.item.date">{{ row.value }}</span>
|
||||
</template>
|
||||
<template v-slot:cell(format)="row">
|
||||
<span>{{ row.item.modelStoreFormat }}</span>
|
||||
</template>
|
||||
<template v-slot:cell(expires)="row">
|
||||
<span v-if="row.item.hasExpired">Expired</span>
|
||||
<span v-else-if="row.item.expirationDate" :title="row.item.expirationDate">{{
|
||||
row.item.expirationElapsedTime
|
||||
}}</span>
|
||||
<span v-else>No</span>
|
||||
</template>
|
||||
<template v-slot:cell(isUpToDate)="row">
|
||||
<font-awesome-icon
|
||||
v-if="row.item.isUpToDate"
|
||||
icon="check-circle"
|
||||
class="text-success"
|
||||
title="This export record contains the latest changes." />
|
||||
<font-awesome-icon
|
||||
v-else
|
||||
icon="exclamation-circle"
|
||||
class="text-danger"
|
||||
title="This export record is outdated. Please consider generating a new export if you need the latest changes." />
|
||||
</template>
|
||||
<template v-slot:cell(isReady)="row">
|
||||
<font-awesome-icon
|
||||
v-if="row.item.isReady"
|
||||
icon="check-circle"
|
||||
class="text-success"
|
||||
title="Ready to download or import." />
|
||||
<font-awesome-icon
|
||||
v-else-if="row.item.isPreparing"
|
||||
icon="spinner"
|
||||
spin
|
||||
class="text-info"
|
||||
title="Exporting in progress..." />
|
||||
<font-awesome-icon
|
||||
v-else-if="row.item.hasExpired"
|
||||
icon="exclamation-circle"
|
||||
class="text-danger"
|
||||
title="The export has expired." />
|
||||
<font-awesome-icon
|
||||
v-else
|
||||
icon="exclamation-circle"
|
||||
class="text-danger"
|
||||
title="The export failed." />
|
||||
</template>
|
||||
<template v-slot:cell(actions)="row">
|
||||
<b-button-toolbar aria-label="Actions">
|
||||
<b-button-group>
|
||||
<b-button
|
||||
v-b-tooltip.hover.bottom
|
||||
:disabled="!row.item.canDownload"
|
||||
title="Download"
|
||||
@click="downloadObject(row.item)">
|
||||
<font-awesome-icon icon="download" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-b-tooltip.hover.bottom
|
||||
:disabled="!row.item.canReimport"
|
||||
title="Reimport"
|
||||
@click="reimportObject(row.item)">
|
||||
<font-awesome-icon icon="file-import" />
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</b-button-toolbar>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import exportsMixin from "./exportsMixin";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ExportRecordModel } from "./exportRecordModel";
|
||||
import {
|
||||
EXPECTED_EXPIRATION_DATE,
|
||||
EXPIRED_STS_DOWNLOAD_RESPONSE,
|
||||
FAILED_DOWNLOAD_RESPONSE,
|
||||
FILE_SOURCE_STORE_RESPONSE,
|
||||
RECENT_STS_DOWNLOAD_RESPONSE,
|
||||
} from "./testData/exportData";
|
||||
|
||||
describe("ExportRecordModel", () => {
|
||||
describe("STS Download Record", () => {
|
||||
const stsDownloadRecord = new ExportRecordModel(RECENT_STS_DOWNLOAD_RESPONSE);
|
||||
|
||||
it("should be considered temporal (STS) when it has a short term storage ID defined", () => {
|
||||
expect(stsDownloadRecord.isStsDownload).toBe(true);
|
||||
expect(stsDownloadRecord.stsDownloadId).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should allow download when ready and not yet expired", () => {
|
||||
expect(stsDownloadRecord.isReady).toBe(true);
|
||||
expect(stsDownloadRecord.hasExpired).toBe(false);
|
||||
expect(stsDownloadRecord.canDownload).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Expired STS Download Record", () => {
|
||||
const expiredDownloadRecord = new ExportRecordModel(EXPIRED_STS_DOWNLOAD_RESPONSE);
|
||||
|
||||
it("should calculate the correct expiration date", () => {
|
||||
expect(expiredDownloadRecord.canExpire).toBe(true);
|
||||
expect(expiredDownloadRecord.expirationDate).toStrictEqual(EXPECTED_EXPIRATION_DATE());
|
||||
});
|
||||
|
||||
it("should not allow download when expired", () => {
|
||||
expect(expiredDownloadRecord.hasExpired).toBe(true);
|
||||
expect(expiredDownloadRecord.canDownload).toBe(false);
|
||||
expect(expiredDownloadRecord.isReady).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Failed STS Download Record", () => {
|
||||
const failedDownloadRecord = new ExportRecordModel(FAILED_DOWNLOAD_RESPONSE);
|
||||
|
||||
it("should not be downloadable", () => {
|
||||
expect(failedDownloadRecord.isReady).toBe(false);
|
||||
expect(failedDownloadRecord.isPreparing).toBe(false);
|
||||
expect(failedDownloadRecord.hasExpired).toBe(false);
|
||||
expect(failedDownloadRecord.canDownload).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("File Source Storage Record", () => {
|
||||
const failedDownloadRecord = new ExportRecordModel(FILE_SOURCE_STORE_RESPONSE);
|
||||
|
||||
it("should be importable", () => {
|
||||
expect(failedDownloadRecord.isReady).toBe(true);
|
||||
expect(failedDownloadRecord.canReimport).toBe(true);
|
||||
expect(failedDownloadRecord.importUri).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import { formatDistanceToNow, parseISO } from "date-fns";
|
||||
import type { components } from "@/schema";
|
||||
|
||||
type ExportObjectRequestMetadata = components["schemas"]["ExportObjectRequestMetadata"];
|
||||
|
||||
export type StoreExportPayload = components["schemas"]["StoreExportPayload"];
|
||||
export type ObjectExportTaskResponse = components["schemas"]["ObjectExportTaskResponse"];
|
||||
|
||||
export class ExportParamsModel {
|
||||
private _params: StoreExportPayload;
|
||||
constructor(data: StoreExportPayload = {}) {
|
||||
this._params = data;
|
||||
}
|
||||
|
||||
get modelStoreFormat() {
|
||||
return this._params?.model_store_format;
|
||||
}
|
||||
|
||||
get includeFiles() {
|
||||
return this._params?.include_files;
|
||||
}
|
||||
|
||||
get includeDeleted() {
|
||||
return this._params?.include_deleted;
|
||||
}
|
||||
|
||||
get includeHidden() {
|
||||
return this._params?.include_hidden;
|
||||
}
|
||||
|
||||
public equals(otherExportParams?: ExportParamsModel) {
|
||||
if (!otherExportParams) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
this.modelStoreFormat === otherExportParams.modelStoreFormat &&
|
||||
this.includeFiles === otherExportParams.includeFiles &&
|
||||
this.includeDeleted === otherExportParams.includeDeleted &&
|
||||
this.includeHidden === otherExportParams.includeHidden
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExportRecordModel {
|
||||
private _data: ObjectExportTaskResponse;
|
||||
private _expirationDate?: Date | null;
|
||||
private _requestMetadata?: ExportObjectRequestMetadata;
|
||||
private _exportParameters?: ExportParamsModel;
|
||||
|
||||
constructor(data: ObjectExportTaskResponse) {
|
||||
this._data = data;
|
||||
this._expirationDate = undefined;
|
||||
this._requestMetadata = data.export_metadata?.request_data;
|
||||
this._exportParameters = this._requestMetadata?.payload
|
||||
? new ExportParamsModel(this._requestMetadata?.payload)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return (this._data.ready && !this.hasExpired) ?? false;
|
||||
}
|
||||
|
||||
get isPreparing() {
|
||||
return this._data.preparing ?? false;
|
||||
}
|
||||
|
||||
get isUpToDate() {
|
||||
return this._data.up_to_date ?? false;
|
||||
}
|
||||
|
||||
get hasFailed() {
|
||||
return !this.isReady && !this.isPreparing && !this.hasExpired;
|
||||
}
|
||||
|
||||
get date() {
|
||||
return parseISO(`${this._data.create_time}Z`);
|
||||
}
|
||||
|
||||
get elapsedTime() {
|
||||
return formatDistanceToNow(this.date, { addSuffix: true });
|
||||
}
|
||||
|
||||
get taskUUID() {
|
||||
return this._data.task_uuid;
|
||||
}
|
||||
|
||||
get importUri() {
|
||||
const payload = this._requestMetadata?.payload;
|
||||
return payload && "target_uri" in payload ? payload.target_uri : undefined;
|
||||
}
|
||||
|
||||
get canReimport() {
|
||||
return this.isReady && Boolean(this.importUri);
|
||||
}
|
||||
|
||||
get stsDownloadId() {
|
||||
const payload = this._requestMetadata?.payload;
|
||||
return payload && "short_term_storage_request_id" in payload
|
||||
? payload.short_term_storage_request_id
|
||||
: undefined;
|
||||
}
|
||||
|
||||
get isStsDownload() {
|
||||
return Boolean(this.stsDownloadId);
|
||||
}
|
||||
|
||||
get canDownload() {
|
||||
return this.isReady && this.isStsDownload && !this.hasExpired;
|
||||
}
|
||||
|
||||
get modelStoreFormat() {
|
||||
return this.exportParams?.modelStoreFormat;
|
||||
}
|
||||
|
||||
get exportParams() {
|
||||
return this._exportParameters;
|
||||
}
|
||||
|
||||
get duration() {
|
||||
const payload = this._requestMetadata?.payload;
|
||||
return payload && "duration" in payload ? payload.duration : undefined;
|
||||
}
|
||||
|
||||
get canExpire() {
|
||||
return this.isStsDownload && Boolean(this.duration);
|
||||
}
|
||||
|
||||
get expirationDate() {
|
||||
if (this._expirationDate === undefined) {
|
||||
this._expirationDate = this.duration ? new Date(this.date.getTime() + this.duration * 1000) : null;
|
||||
}
|
||||
return this._expirationDate;
|
||||
}
|
||||
|
||||
get expirationElapsedTime() {
|
||||
return this.canExpire && this.expirationDate
|
||||
? formatDistanceToNow(this.expirationDate, { addSuffix: true })
|
||||
: null;
|
||||
}
|
||||
|
||||
get hasExpired() {
|
||||
return this.canExpire && this.expirationDate && Date.now() > this.expirationDate.getTime();
|
||||
}
|
||||
|
||||
get errorMessage() {
|
||||
return this._data?.export_metadata?.result_data?.error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { ExportRecordModel } from "@/components/Common/models/exportRecordModel";
|
||||
import type { components } from "@/schema";
|
||||
|
||||
type ObjectExportTaskResponse = components["schemas"]["ObjectExportTaskResponse"];
|
||||
type ExportObjectRequestMetadata = components["schemas"]["ExportObjectRequestMetadata"];
|
||||
type ExportObjectResultMetadata = components["schemas"]["ExportObjectResultMetadata"];
|
||||
|
||||
const PAST_EXPORT_DATE = new Date("11 November 2022 14:48 UTC").toISOString();
|
||||
const RECENT_EXPORT_DATE = new Date().toISOString();
|
||||
const STS_EXPORT_DURATION_IN_SECONDS = 86400;
|
||||
export const EXPECTED_EXPIRATION_DATE = () => {
|
||||
const expectedDate = new Date(PAST_EXPORT_DATE);
|
||||
expectedDate.setSeconds(expectedDate.getSeconds() + STS_EXPORT_DURATION_IN_SECONDS);
|
||||
return expectedDate;
|
||||
};
|
||||
|
||||
const FAKE_STS_DOWNLOAD_REQUEST_DATA: ExportObjectRequestMetadata = {
|
||||
object_id: "3cc0effd29705aa3",
|
||||
object_type: "history",
|
||||
user_id: "f597429621d6eb2b",
|
||||
payload: {
|
||||
model_store_format: "rocrate.zip",
|
||||
include_files: true,
|
||||
include_deleted: false,
|
||||
include_hidden: false,
|
||||
short_term_storage_request_id: "08bf4cc3-758e-4a9d-9fe4-a89a0d0604c7",
|
||||
duration: STS_EXPORT_DURATION_IN_SECONDS,
|
||||
},
|
||||
};
|
||||
|
||||
const FAKE_FILE_SOURCE_REQUEST_DATA: ExportObjectRequestMetadata = {
|
||||
object_id: "3cc0effd29705aa3",
|
||||
object_type: "history",
|
||||
user_id: "f597429621d6eb2b",
|
||||
payload: {
|
||||
model_store_format: "tar.gz",
|
||||
include_files: true,
|
||||
include_deleted: false,
|
||||
include_hidden: false,
|
||||
target_uri: "gxfiles://fake-target-uri/test.tar.gz",
|
||||
},
|
||||
};
|
||||
|
||||
const SUCCESS_EXPORT_RESULT_DATA: ExportObjectResultMetadata = {
|
||||
success: true,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const FAILED_EXPORT_RESULT_DATA: ExportObjectResultMetadata = {
|
||||
success: false,
|
||||
error: "Fake Error Message",
|
||||
};
|
||||
|
||||
export const RECENT_STS_DOWNLOAD_RESPONSE: ObjectExportTaskResponse = {
|
||||
id: "FAKE_RECENT_DOWNLOAD_ID",
|
||||
ready: true,
|
||||
preparing: false,
|
||||
up_to_date: true,
|
||||
task_uuid: "35563335-e275-4520-80e8-885793279095",
|
||||
create_time: RECENT_EXPORT_DATE,
|
||||
export_metadata: {
|
||||
request_data: FAKE_STS_DOWNLOAD_REQUEST_DATA,
|
||||
result_data: SUCCESS_EXPORT_RESULT_DATA,
|
||||
},
|
||||
};
|
||||
|
||||
export const EXPIRED_STS_DOWNLOAD_RESPONSE: ObjectExportTaskResponse = {
|
||||
id: "FAKE_EXPIRED_DOWNLOAD_ID",
|
||||
ready: true,
|
||||
preparing: false,
|
||||
up_to_date: true,
|
||||
task_uuid: "35563335-e275-4520-80e8-885793279095",
|
||||
create_time: PAST_EXPORT_DATE,
|
||||
export_metadata: {
|
||||
request_data: FAKE_STS_DOWNLOAD_REQUEST_DATA,
|
||||
result_data: SUCCESS_EXPORT_RESULT_DATA,
|
||||
},
|
||||
};
|
||||
|
||||
export const FAILED_DOWNLOAD_RESPONSE: ObjectExportTaskResponse = {
|
||||
id: "FAKE_FAILED_DOWNLOAD_ID",
|
||||
ready: false,
|
||||
preparing: false,
|
||||
up_to_date: true,
|
||||
task_uuid: "35563335-e275-4520-80e8-885793279095",
|
||||
create_time: RECENT_EXPORT_DATE,
|
||||
export_metadata: {
|
||||
request_data: FAKE_STS_DOWNLOAD_REQUEST_DATA,
|
||||
result_data: FAILED_EXPORT_RESULT_DATA,
|
||||
},
|
||||
};
|
||||
|
||||
export const FILE_SOURCE_STORE_RESPONSE: ObjectExportTaskResponse = {
|
||||
id: "FAKE_RECENT_DOWNLOAD_ID",
|
||||
ready: true,
|
||||
preparing: false,
|
||||
up_to_date: true,
|
||||
task_uuid: "35563335-e275-4520-80e8-885793279095",
|
||||
create_time: RECENT_EXPORT_DATE,
|
||||
export_metadata: {
|
||||
request_data: FAKE_FILE_SOURCE_REQUEST_DATA,
|
||||
result_data: SUCCESS_EXPORT_RESULT_DATA,
|
||||
},
|
||||
};
|
||||
|
||||
export const EXPIRED_STS_DOWNLOAD_RECORD = new ExportRecordModel(EXPIRED_STS_DOWNLOAD_RESPONSE);
|
||||
export const FILE_SOURCE_STORE_RECORD = new ExportRecordModel(FILE_SOURCE_STORE_RESPONSE);
|
||||
export const RECENT_STS_DOWNLOAD_RECORD = new ExportRecordModel(RECENT_STS_DOWNLOAD_RESPONSE);
|
||||
@@ -46,7 +46,7 @@ import { UrlTracker } from "./utilities";
|
||||
import { Model } from "./model";
|
||||
import { Services } from "./services";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { mountUploadModal } from "components/Upload";
|
||||
import { useGlobalUploadModal } from "composables/globalUploadModal";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
@@ -78,6 +78,10 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { openGlobalUploadModal } = useGlobalUploadModal();
|
||||
return { openGlobalUploadModal };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: null,
|
||||
@@ -133,7 +137,7 @@ export default {
|
||||
modalShow: true,
|
||||
selectable: true,
|
||||
};
|
||||
mountUploadModal(propsData);
|
||||
this.openGlobalUploadModal(propsData);
|
||||
this.modalShow = false;
|
||||
},
|
||||
/** Called when selection is complete, values are formatted and parsed to external callback **/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import DatasetName from "./DatasetName";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import DatasetStorage from "./DatasetStorage";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import flushPromises from "flush-promises";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import axios from "axios";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fetcher } from "schema";
|
||||
import { safePath } from "utils/redirect";
|
||||
import type { FetchArgType } from "openapi-typescript-fetch";
|
||||
import { fetcher } from "@/schema";
|
||||
import { withPrefix } from "@/utils/redirect";
|
||||
|
||||
const _getDatasets = fetcher.path("/api/datasets").method("get").create();
|
||||
type GetDatasetsApiOptions = FetchArgType<typeof _getDatasets>;
|
||||
@@ -17,7 +17,7 @@ export async function getDatasets(options: GetDatasetsOptions = {}) {
|
||||
const params: GetDatasetsApiOptions = {};
|
||||
if (options.sortBy) {
|
||||
const sortPrefix = options.sortDesc ? "-dsc" : "-asc";
|
||||
params.order = `${options.sortBy}${sortPrefix}&`;
|
||||
params.order = `${options.sortBy}${sortPrefix}`;
|
||||
}
|
||||
if (options.limit) {
|
||||
params.limit = options.limit;
|
||||
@@ -67,7 +67,7 @@ export async function updateTags(
|
||||
|
||||
export function getCompositeDatasetLink(historyDatasetId: string, path: string) {
|
||||
// TODO: historyDatasetId is wrong here, we should expose the route without forcing to provide a history id
|
||||
return safePath(`/api/histories/${historyDatasetId}/contents/${historyDatasetId}/display?filename=${path}`);
|
||||
return withPrefix(`/api/histories/${historyDatasetId}/contents/${historyDatasetId}/display?filename=${path}`);
|
||||
}
|
||||
|
||||
const getDataset = fetcher.path("/api/histories/{history_id}/contents/{id}").method("get").create();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from "axios";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import DatasetAttributes from "./DatasetAttributes";
|
||||
import MockProvider from "../providers/MockProvider";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:id="datasetId"
|
||||
v-slot="{ result: dataset, loading: isDatasetLoading, error: datasetLoadingError }">
|
||||
<div aria-labelledby="dataset-details-heading">
|
||||
<h1 id="dataset-details-heading" class="hide-element">Dataset Details</h1>
|
||||
<h1 id="dataset-details-heading" class="sr-only">Dataset Details</h1>
|
||||
<LoadingSpan v-if="isDatasetLoading" />
|
||||
<Alert v-else-if="datasetLoadingError" :message="datasetLoadingError" variant="error" />
|
||||
<CurrentUser v-else v-slot="{ user }">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import DatasetError from "./DatasetError";
|
||||
import MockProvider from "../providers/MockProvider";
|
||||
import MockCurrentUser from "../providers/MockCurrentUser";
|
||||
|
||||
jest.mock("components/providers", () => {
|
||||
return {}; // stubbed below
|
||||
@@ -33,6 +34,7 @@ function buildWrapper(has_duplicate_inputs = true, has_empty_inputs = true, user
|
||||
}),
|
||||
FontAwesomeIcon: false,
|
||||
FormElement: false,
|
||||
CurrentUser: MockCurrentUser({ email: "email" }),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -48,7 +50,6 @@ describe("DatasetError", () => {
|
||||
expect(messages.at(1).text()).toBe("message_2");
|
||||
expect(wrapper.find("#dataset-error-has-empty-inputs")).toBeDefined();
|
||||
expect(wrapper.find("#dataset-error-has-duplicate-inputs")).toBeDefined();
|
||||
expect(wrapper.findAll("#dataset-error-email").length).toBe(1);
|
||||
});
|
||||
|
||||
it("check props without common problems", async () => {
|
||||
|
||||
@@ -61,25 +61,25 @@
|
||||
show
|
||||
>{{ resultMessage[0] }}</b-alert
|
||||
>
|
||||
<div v-if="showForm" id="fieldsAndButton">
|
||||
<FormElement
|
||||
v-if="!jobDetails.user_email"
|
||||
id="dataset-error-email"
|
||||
v-model="email"
|
||||
title="Please provide your email:" />
|
||||
<FormElement
|
||||
id="dataset-error-message"
|
||||
v-model="message"
|
||||
:area="true"
|
||||
title="Please provide detailed information on the activities leading to this issue:" />
|
||||
<b-button
|
||||
id="dataset-error-submit"
|
||||
variant="primary"
|
||||
class="mt-3"
|
||||
@click="submit(dataset, jobDetails.user_email)">
|
||||
<font-awesome-icon icon="bug" class="mr-1" />Report
|
||||
</b-button>
|
||||
</div>
|
||||
<CurrentUser v-slot="{ user }">
|
||||
<div v-if="showForm" id="fieldsAndButton">
|
||||
<span class="mr-2 font-weight-bold">{{ emailTitle }}</span>
|
||||
<span v-if="!!user.email">{{ user.email }}</span>
|
||||
<span v-else>{{ "You must be logged in to receive emails" | l }}</span>
|
||||
<FormElement
|
||||
id="dataset-error-message"
|
||||
v-model="message"
|
||||
:area="true"
|
||||
title="Please provide detailed information on the activities leading to this issue:" />
|
||||
<b-button
|
||||
id="dataset-error-submit"
|
||||
variant="primary"
|
||||
class="mt-3"
|
||||
@click="submit(dataset, jobDetails.user_email)">
|
||||
<font-awesome-icon icon="bug" class="mr-1" />Report
|
||||
</b-button>
|
||||
</div>
|
||||
</CurrentUser>
|
||||
</div>
|
||||
</JobDetailsProvider>
|
||||
</DatasetProvider>
|
||||
@@ -95,6 +95,7 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faBug } from "@fortawesome/free-solid-svg-icons";
|
||||
import { sendErrorReport } from "./services";
|
||||
import CurrentUser from "components/providers/CurrentUser";
|
||||
|
||||
library.add(faBug);
|
||||
|
||||
@@ -106,6 +107,7 @@ export default {
|
||||
FormElement,
|
||||
JobDetailsProvider,
|
||||
JobProblemProvider,
|
||||
CurrentUser,
|
||||
},
|
||||
props: {
|
||||
datasetId: {
|
||||
@@ -116,9 +118,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
message: null,
|
||||
email: null,
|
||||
errorMessage: null,
|
||||
resultMessages: [],
|
||||
emailTitle: this.l("Your email address"),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -132,8 +134,8 @@ export default {
|
||||
onError(err) {
|
||||
this.errorMessage = err;
|
||||
},
|
||||
submit(dataset, userEmail) {
|
||||
const email = this.email || userEmail;
|
||||
submit(dataset, userEmailJob) {
|
||||
const email = userEmailJob || this.currentUserEmail;
|
||||
const message = this.message;
|
||||
sendErrorReport(dataset, message, email).then(
|
||||
(resultMessages) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import DatasetInformation from "./DatasetInformation";
|
||||
import datasetResponse from "./testData/datasetResponse";
|
||||
import flushPromises from "flush-promises";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { components } from "schema";
|
||||
import type { components } from "@/schema";
|
||||
|
||||
export type DatatypesCombinedMap = components["schemas"]["DatatypesCombinedMap"];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetcher } from "schema/fetcher";
|
||||
import { fetcher } from "@/schema/fetcher";
|
||||
|
||||
const getTypesAndMappings = fetcher.path("/api/datatypes/types_and_mapping").method("get").create();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormBoolean from "./FormBoolean";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
@@ -18,13 +18,17 @@ describe("FormBoolean", () => {
|
||||
|
||||
it("check initial value and value change", async () => {
|
||||
const input = wrapper.find("input");
|
||||
expect(wrapper.vm.currentValue).toBe(false);
|
||||
await wrapper.setProps({ value: true });
|
||||
expect(wrapper.vm.currentValue).toBe(true);
|
||||
await input.trigger("click");
|
||||
expect(input.element.checked).toBe(false);
|
||||
await input.trigger("click");
|
||||
expect(input.element.checked).toBe(true);
|
||||
const switchComponent = wrapper.findComponent(".custom-switch");
|
||||
expect(switchComponent.props().value).toBe(false);
|
||||
await wrapper.setProps({ value: "true" });
|
||||
expect(wrapper.emitted().input[0][0]).toBe(true);
|
||||
await wrapper.setProps({ value: "false" });
|
||||
expect(wrapper.emitted().input[1][0]).toBe(false);
|
||||
await wrapper.setProps({ value: true });
|
||||
expect(wrapper.emitted().input[2][0]).toBe(true);
|
||||
await input.setChecked(false);
|
||||
expect(wrapper.emitted().input[3][0]).toBe(false);
|
||||
await input.setChecked(true);
|
||||
expect(wrapper.emitted().input[4][0]).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
export interface FormBooleanProps {
|
||||
value: boolean | string;
|
||||
}
|
||||
|
||||
const props = defineProps<FormBooleanProps>();
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: boolean): void;
|
||||
}>();
|
||||
|
||||
const currentValue = computed({
|
||||
get() {
|
||||
return String(props.value).toLowerCase() === "true";
|
||||
},
|
||||
set(newValue) {
|
||||
emit("input", newValue);
|
||||
},
|
||||
});
|
||||
|
||||
const label = computed(() => (currentValue.value ? "Yes" : "No"));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-form-checkbox v-model="currentValue" class="no-highlight" switch>
|
||||
{{ label }}
|
||||
</b-form-checkbox>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currentValue: {
|
||||
get() {
|
||||
return ["true", true].includes(this.value);
|
||||
},
|
||||
set(val) {
|
||||
this.$emit("input", val);
|
||||
},
|
||||
},
|
||||
label() {
|
||||
return this.currentValue ? "Yes" : "No";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import MountTarget from "./FormCheck";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
describe("FormCheck", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(MountTarget, {
|
||||
propsData: {
|
||||
value: null,
|
||||
options: [],
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
it("Confirm 'n + 1' checkboxes created (eg. includes the Select-All). Confirm labels and values match. Confirm correct values emitted.", async () => {
|
||||
const noInput = wrapper.find("[type='checkbox']");
|
||||
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='checkbox']");
|
||||
const labels = wrapper.findAll(".custom-control-label");
|
||||
expect(inputs.length).toBe(n + 1);
|
||||
const expectedValues = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
await inputs.at(i + 1).setChecked();
|
||||
expect(labels.at(i + 1).text()).toBe(`label_${i}`);
|
||||
expect(inputs.at(i + 1).attributes("value")).toBe(`value_${i}`);
|
||||
expectedValues.push(`value_${i}`);
|
||||
expect(wrapper.emitted()["input"][i][0]).toEqual(expectedValues);
|
||||
}
|
||||
});
|
||||
|
||||
it("Confirm checkboxes are created when various 'empty values' are passed.", async () => {
|
||||
const emptyValues = [0, null, false, true, undefined];
|
||||
const options = [];
|
||||
for (let i = 0; i < emptyValues.length; i++) {
|
||||
options.push([`label_${i}`, emptyValues[i]]);
|
||||
}
|
||||
await wrapper.setProps({ options });
|
||||
const inputs = wrapper.findAll("[type='checkbox']");
|
||||
expect(inputs.length).toBe(emptyValues.length + 1);
|
||||
const expectedValues = [];
|
||||
for (let i = 0; i < emptyValues; i++) {
|
||||
await inputs.at(i + 1).setChecked();
|
||||
expect(inputs.at(i + 1).attributes("value")).toBe(emptyValues[i]);
|
||||
expectedValues.push(expectedValues[i]);
|
||||
expect(wrapper.emitted()["input"][i][0]).toEqual(expectedValues);
|
||||
}
|
||||
});
|
||||
|
||||
it("Confirm Select-All checkbox works in various states: select-all, unselect-all, indeterminate/partial-list-selection.", async () => {
|
||||
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='checkbox']");
|
||||
/* confirm number of checkboxes requested matches number checkboxes created */
|
||||
expect(inputs.length).toBe(n + 1);
|
||||
/* confirm component loads unchecked */
|
||||
for (let i = 0; i < n + 1; i++) {
|
||||
expect(inputs.at(i).element.checked).toBeFalsy();
|
||||
}
|
||||
/* 1 - confirm select-all option checked */
|
||||
await inputs.at(0).setChecked();
|
||||
expect(inputs.at(0).element.checked).toBeTruthy();
|
||||
/* ...confirm corresponding options checked */
|
||||
const values = options.map((option) => option[1]);
|
||||
expect(wrapper.emitted()["input"][0][0]).toStrictEqual(values);
|
||||
/* 2 - confirm select-all option UNchecked */
|
||||
await inputs.at(0).setChecked(false);
|
||||
expect(inputs.at(0).element.checked).toBeFalsy();
|
||||
/* ...confirm corresponding options UNchecked */
|
||||
for (let i = 0; i < n; i++) {
|
||||
expect(inputs.at(i + 1).element.checked).toBeFalsy();
|
||||
}
|
||||
/* 3 - confirm corresponding options indeterminate-state */
|
||||
await inputs.at(1).setChecked(true);
|
||||
expect(wrapper.find("input:indeterminate").exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
export interface FormCheckProps {
|
||||
value?: string | string[];
|
||||
options: string[];
|
||||
}
|
||||
|
||||
const props = defineProps<FormCheckProps>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: string[]): void;
|
||||
}>();
|
||||
|
||||
const indeterminate = ref(false);
|
||||
|
||||
const currentValue = computed({
|
||||
get: () => {
|
||||
const val = props.value ?? [];
|
||||
return Array.isArray(val) ? val : [val];
|
||||
},
|
||||
set: (newValue) => {
|
||||
emit("input", newValue);
|
||||
|
||||
if (newValue.length === 0) {
|
||||
selectAll.value = false;
|
||||
indeterminate.value = false;
|
||||
} else if (newValue.length === props.options.length) {
|
||||
selectAll.value = true;
|
||||
indeterminate.value = false;
|
||||
} else {
|
||||
indeterminate.value = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const hasOptions = computed(() => {
|
||||
return props.options.length > 0;
|
||||
});
|
||||
|
||||
const selectAll = ref(false);
|
||||
|
||||
function onSelectAll() {
|
||||
if (selectAll.value) {
|
||||
const allValues = props.options.map((option) => option[1]);
|
||||
emit("input", allValues);
|
||||
} else {
|
||||
emit("input", []);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasOptions">
|
||||
<b-form-checkbox
|
||||
v-model="selectAll"
|
||||
v-localize
|
||||
class="mb-1"
|
||||
:indeterminate="indeterminate"
|
||||
@input="onSelectAll">
|
||||
Select / Deselect all
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox-group v-model="currentValue" stacked class="pl-3">
|
||||
<b-form-checkbox v-for="(option, index) in options" :key="index" :value="option[1]">
|
||||
{{ option[0] }}
|
||||
</b-form-checkbox>
|
||||
</b-form-checkbox-group>
|
||||
</div>
|
||||
<b-alert v-else v-localize variant="warning" show> No options available. </b-alert>
|
||||
</template>
|
||||
@@ -1,3 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
export interface FormColorProps {
|
||||
value?: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FormColorProps>(), {
|
||||
value: "",
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: string): void;
|
||||
}>();
|
||||
|
||||
const currentValue = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(newValue) {
|
||||
emit("input", newValue);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-row>
|
||||
<b-col class="form-color-input">
|
||||
@@ -9,30 +35,6 @@
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currentValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit("input", val);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.form-color-input {
|
||||
max-width: 3.6rem;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormDirectory from "./FormDirectory";
|
||||
import FilesDialog from "components/FilesDialog/FilesDialog";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormHidden from "./FormHidden";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormInput from "./FormInput";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import flushPromises from "flush-promises";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormNumber from "./FormNumber";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormOptionalText from "./FormOptionalText";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("FormOptionalText", () => {
|
||||
const mountFormOptionalText = async (props) =>
|
||||
await mount(FormOptionalText, {
|
||||
propsData: props,
|
||||
localVue,
|
||||
});
|
||||
|
||||
it("should display existing values", async () => {
|
||||
const v = "somevalue";
|
||||
const wrapper = await mountFormOptionalText({ value: v });
|
||||
const el = wrapper.find("input");
|
||||
expect(el.element.checked).toEqual(true);
|
||||
const elText = wrapper.find("input[type='text']");
|
||||
expect(elText.element.value).toEqual(v);
|
||||
await el.setChecked(false);
|
||||
expect(wrapper.emitted().input[0][0]).toEqual(null);
|
||||
await el.setChecked(true);
|
||||
expect(wrapper.emitted().input[1][0]).toEqual("");
|
||||
});
|
||||
|
||||
it("should initialize with null if value does not exist", async () => {
|
||||
const v = null;
|
||||
const wrapper = await mountFormOptionalText({ value: v });
|
||||
const el = wrapper.find("input");
|
||||
expect(el.element.checked).toEqual(false);
|
||||
await wrapper.setProps({ value: "" });
|
||||
expect(el.element.checked).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-checkbox v-model="currentStatus" class="ui-switch" switch>
|
||||
Set value for this optional select field?
|
||||
</b-form-checkbox>
|
||||
<FormText
|
||||
v-if="currentStatus"
|
||||
:id="id"
|
||||
v-model="currentValue"
|
||||
:readonly="readonly"
|
||||
:value="value"
|
||||
:area="area"
|
||||
:placeholder="placeholder"
|
||||
:multiple="multiple"
|
||||
:datalist="datalist"
|
||||
:type="type" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormText from "./FormText";
|
||||
export default {
|
||||
components: {
|
||||
FormText,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
default: "",
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
},
|
||||
area: {
|
||||
// <textarea> instead of <input> element
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
// Allow multiple entries to be created
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
datalist: {
|
||||
// Display list of suggestions in autocomplete dialog
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit("input", val);
|
||||
},
|
||||
},
|
||||
currentStatus: {
|
||||
get() {
|
||||
return this.status || this.value !== null;
|
||||
},
|
||||
set(val) {
|
||||
this.status = Boolean(val);
|
||||
if (this.status) {
|
||||
this.currentValue = "";
|
||||
} else {
|
||||
this.currentValue = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -56,5 +56,8 @@ export default {
|
||||
this.$emit("input", this.parameter.field.value());
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.parameter.remove();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import MountTarget from "./FormRadio";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const $emit = defineEmits(["input"]);
|
||||
const emit = defineEmits(["input"]);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
default: null,
|
||||
@@ -17,15 +17,20 @@ const currentValue = computed({
|
||||
return props.value;
|
||||
},
|
||||
set: (val) => {
|
||||
$emit("input", val);
|
||||
emit("input", val);
|
||||
},
|
||||
});
|
||||
|
||||
const hasOptions = computed(() => {
|
||||
return props.options.length > 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-form-radio-group v-model="currentValue" stacked>
|
||||
<b-form-radio-group v-if="hasOptions" 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>
|
||||
<b-alert v-else v-localize variant="warning" show> No options available. </b-alert>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import RuleCollectionBuilder from "components/RuleCollectionBuilder";
|
||||
import RulesDisplay from "components/RulesDisplay/RulesDisplay";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { ref, computed } from "vue";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object,
|
||||
},
|
||||
target: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const modal = ref(null);
|
||||
const elements = ref(null);
|
||||
|
||||
const initialRules = {
|
||||
rules: [],
|
||||
mapping: [],
|
||||
};
|
||||
|
||||
const displayRules = computed(() => props.value ?? initialRules);
|
||||
|
||||
async function onEdit() {
|
||||
if (props.target) {
|
||||
const url = `${getAppRoot()}api/dataset_collections/${props.target.id}?instance_type=history`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(url);
|
||||
elements.value = response.data;
|
||||
modal.value.show();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.log("problem fetching collection");
|
||||
}
|
||||
} else {
|
||||
modal.value.show();
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(["input"]);
|
||||
|
||||
function onSaveRules(rules) {
|
||||
modal.value.hide();
|
||||
emit("input", rules);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
modal.value.hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faEdit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-rules-edit">
|
||||
<RulesDisplay :input-rules="displayRules" />
|
||||
<b-button title="Edit Rules" @click="onEdit">
|
||||
<FontAwesomeIcon icon="fa-edit" />
|
||||
<span>Edit</span>
|
||||
</b-button>
|
||||
|
||||
<b-modal ref="modal" modal-class="ui-form-rules-edit-modal" hide-footer>
|
||||
<template v-slot:modal-title>
|
||||
<h2 class="mb-0">Build Rules for Applying to Existing Collection</h2>
|
||||
</template>
|
||||
<RuleCollectionBuilder
|
||||
elements-type="collection_contents"
|
||||
import-type="collections"
|
||||
:initial-elements="elements"
|
||||
:initial-rules="props.value"
|
||||
:save-rules-fn="onSaveRules"
|
||||
:oncancel="onCancel"
|
||||
:oncreate="() => {}" />
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-form-rules-edit-modal {
|
||||
.modal-dialog {
|
||||
width: 100%;
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import FormCheck from "./FormCheck";
|
||||
import FormRadio from "./FormRadio";
|
||||
|
||||
const $emit = defineEmits(["input"]);
|
||||
@@ -50,5 +51,6 @@ const currentOptions = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form-radio v-if="display == 'radio'" v-model="currentValue" :options="currentOptions" />
|
||||
<form-check v-if="display === 'checkboxes'" v-model="currentValue" :options="currentOptions" />
|
||||
<form-radio v-else-if="display === 'radio'" v-model="currentValue" :options="currentOptions" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormText from "./FormText";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("FormText", () => {
|
||||
const mountFormText = async (props) =>
|
||||
await mount(FormText, {
|
||||
propsData: props,
|
||||
localVue,
|
||||
});
|
||||
|
||||
it("should render the appropriate input type", async () => {
|
||||
const wrapper = await mountFormText({});
|
||||
const el = wrapper.find("input[type='text']");
|
||||
expect(el.exists()).toBe(true);
|
||||
await wrapper.setProps({ type: "password" });
|
||||
const elPassword = wrapper.find("input[type='password']");
|
||||
expect(elPassword.exists()).toBe(true);
|
||||
await wrapper.setProps({ type: "anyothertype" });
|
||||
const elOtherType = wrapper.find("input[type='text']");
|
||||
expect(elOtherType.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should render the appropriate component", async () => {
|
||||
const wrapper = await mountFormText({ area: true });
|
||||
const el = wrapper.find("textarea");
|
||||
expect(el.exists()).toBe(true);
|
||||
await wrapper.setProps({ area: false, multiple: true });
|
||||
const elMultiple = wrapper.find("textarea");
|
||||
expect(elMultiple.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be able to render a datalist", async () => {
|
||||
const wrapper = await mountFormText({ id: "text-input", datalist: ["one", "two", "three"] });
|
||||
let el = wrapper.find("datalist");
|
||||
expect(el.exists()).toBe(true);
|
||||
el = wrapper.find("option");
|
||||
expect(el.exists()).toBe(true);
|
||||
el = wrapper.find("[list='text-input-datalist']");
|
||||
expect(el.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be able to render border and text color from props", async () => {
|
||||
const wrapper = await mountFormText({});
|
||||
const el = wrapper.find("input");
|
||||
expect(el.element.style).toMatchObject({});
|
||||
await wrapper.setProps({ color: "green" });
|
||||
expect(el.element.style).toMatchObject({ color: "green", "border-color": "green" });
|
||||
await wrapper.setProps({ cls: "my-custom-class" });
|
||||
expect(el.element.classList).toContain("my-custom-class");
|
||||
});
|
||||
|
||||
it("should be able to accept a default value", async () => {
|
||||
const v = "something";
|
||||
const wrapper = await mountFormText({ value: v });
|
||||
const el = wrapper.find("input");
|
||||
expect(el.props("value")).toEqual(v);
|
||||
});
|
||||
|
||||
it("should be able to accept an array as value", async () => {
|
||||
const v = ["field_1", "field_2", "field_3"];
|
||||
const wrapper = await mountFormText({ value: v });
|
||||
const el = wrapper.find("input");
|
||||
expect(el.props("value")).toEqual("field_1");
|
||||
await wrapper.setProps({ multiple: true });
|
||||
const elMultiple = wrapper.find("textarea");
|
||||
expect(elMultiple.props("value")).toEqual("field_1\nfield_2\nfield_3\n");
|
||||
});
|
||||
|
||||
it("should be able to accept an empty array as value", async () => {
|
||||
const v = [];
|
||||
const wrapper = await mountFormText({ value: v });
|
||||
const el = wrapper.find("input");
|
||||
expect(el.props("value")).toEqual("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<b-row align-v="center">
|
||||
<b-col>
|
||||
<b-form-textarea
|
||||
v-if="inputArea"
|
||||
:id="id"
|
||||
v-model="currentValue"
|
||||
:class="['ui-text-area', cls]"
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
:style="style" />
|
||||
<b-form-input
|
||||
v-else
|
||||
:id="id"
|
||||
v-model="currentValue"
|
||||
:class="['ui-input', cls]"
|
||||
:readonly="readonly"
|
||||
:placeholder="placeholder"
|
||||
:style="style"
|
||||
:type="acceptedTypes"
|
||||
:list="`${id}-datalist`" />
|
||||
<datalist v-if="datalist && !inputArea" :id="`${id}-datalist`">
|
||||
<option v-for="data in datalist" :key="data.value" :label="data.label" :value="data.value" />
|
||||
</datalist>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
// String; Array for multiple
|
||||
default: "",
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
},
|
||||
area: {
|
||||
// <textarea> instead of <input> element
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
// Allow multiple entries to be created
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
cls: {
|
||||
// Refers to an optional custom css class name
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
datalist: {
|
||||
// Display list of suggestions in autocomplete dialog
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
acceptedTypes() {
|
||||
return ["text", "password"].includes(this.type) ? this.type : "text";
|
||||
},
|
||||
currentValue: {
|
||||
get() {
|
||||
const v = this.value ?? "";
|
||||
if (Array.isArray(v)) {
|
||||
if (v.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return this.multiple
|
||||
? this.value.reduce((str_value, v) => str_value + String(v) + "\n", "")
|
||||
: String(this.value[0]);
|
||||
}
|
||||
return String(v);
|
||||
},
|
||||
set(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$emit("input", newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
inputArea() {
|
||||
return this.area || this.multiple;
|
||||
},
|
||||
style() {
|
||||
return this.color
|
||||
? {
|
||||
color: this.color,
|
||||
"border-color": this.color,
|
||||
}
|
||||
: null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.ui-input-linked {
|
||||
border-left-width: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,6 @@ import Ui from "mvc/ui/ui-misc";
|
||||
import SelectContent from "mvc/ui/ui-select-content";
|
||||
import SelectLibrary from "mvc/ui/ui-select-library";
|
||||
import SelectFtp from "mvc/ui/ui-select-ftp";
|
||||
import RulesEdit from "mvc/ui/ui-rules-edit";
|
||||
import DataPicker from "mvc/ui/ui-data-picker";
|
||||
|
||||
// create form view
|
||||
@@ -31,6 +30,11 @@ export default Backbone.View.extend({
|
||||
data_dialog: "_fieldDialog",
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
this.field.remove();
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/** Returns an input field for a given field type */
|
||||
create: function (input_def) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
@@ -197,14 +201,6 @@ export default Backbone.View.extend({
|
||||
});
|
||||
},
|
||||
|
||||
_fieldRulesEdit: function (input_def) {
|
||||
return new RulesEdit.View({
|
||||
id: input_def.id,
|
||||
onchange: input_def.onchange,
|
||||
target: input_def.target,
|
||||
});
|
||||
},
|
||||
|
||||
/** Upload file field */
|
||||
_fieldUpload: function (input_def) {
|
||||
return new Ui.Upload({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormCard from "./FormCard";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormDisplay from "./FormDisplay";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormElement from "./FormElement";
|
||||
import FormHidden from "./Elements/FormHidden";
|
||||
import FormInput from "./Elements/FormInput";
|
||||
import FormText from "./Elements/FormText";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -70,12 +70,17 @@ describe("FormElement", () => {
|
||||
|
||||
it("check type matching", async () => {
|
||||
await wrapper.setProps({ type: "text" });
|
||||
expect(wrapper.findComponent(FormInput).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(FormText).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(FormHidden).exists()).toBe(false);
|
||||
|
||||
await wrapper.setProps({ attributes: { titleonly: true } });
|
||||
expect(wrapper.findComponent(FormHidden).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(FormInput).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(FormText).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("displays as the correct type if is_workflow is true", async () => {
|
||||
await wrapper.setProps({ type: "data_column", attributes: { is_workflow: true } });
|
||||
expect(wrapper.findComponent(FormText).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("marks required values", async () => {
|
||||
|
||||
@@ -1,110 +1,85 @@
|
||||
<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";
|
||||
<script setup lang="ts">
|
||||
import FormBoolean from "./Elements/FormBoolean.vue";
|
||||
import FormHidden from "./Elements/FormHidden.vue";
|
||||
import FormInput from "./Elements/FormInput.vue";
|
||||
import FormParameter from "./Elements/FormParameter.vue";
|
||||
import FormSelection from "./Elements/FormSelection.vue";
|
||||
import FormColor from "./Elements/FormColor.vue";
|
||||
import FormDirectory from "./Elements/FormDirectory.vue";
|
||||
import FormNumber from "./Elements/FormNumber.vue";
|
||||
import FormText from "./Elements/FormText.vue";
|
||||
import FormOptionalText from "./Elements/FormOptionalText.vue";
|
||||
import FormRulesEdit from "./Elements/FormRulesEdit.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { ref, computed, useAttrs } from "vue";
|
||||
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";
|
||||
|
||||
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,
|
||||
},
|
||||
import type { ComputedRef } from "vue";
|
||||
import type { FormParameterTypes, FormParameterAttributes, FormParameterValue } from "./parameterTypes";
|
||||
|
||||
interface FormElementProps {
|
||||
id?: string;
|
||||
type?: FormParameterTypes;
|
||||
value?: FormParameterValue;
|
||||
title?: string;
|
||||
refreshOnChange?: boolean;
|
||||
help?: string;
|
||||
error?: string;
|
||||
backbonejs?: boolean;
|
||||
disabled?: boolean;
|
||||
attributes?: FormParameterAttributes;
|
||||
collapsedEnableText?: string;
|
||||
collapsedDisableText?: string;
|
||||
collapsedEnableIcon?: string;
|
||||
collapsedDisableIcon?: string;
|
||||
connectedEnableText?: string;
|
||||
connectedDisableText?: string;
|
||||
connectedEnableIcon?: string;
|
||||
connectedDisableIcon?: string;
|
||||
workflowBuildingMode?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FormElementProps>(), {
|
||||
id: "identifier",
|
||||
refreshOnChange: false,
|
||||
backbonejs: false,
|
||||
disabled: false,
|
||||
collapsedEnableText: "Enable",
|
||||
collapsedDisableText: "Disable",
|
||||
collapsedEnableIcon: "far fa-caret-square-down",
|
||||
collapsedDisableIcon: "far fa-caret-square-up",
|
||||
connectedEnableText: "Remove connection from module.",
|
||||
connectedDisableText: "Add connection to module.",
|
||||
connectedEnableIcon: "fa fa-times",
|
||||
connectedDisableIcon: "fa fa-arrows-alt-h",
|
||||
workflowBuildingMode: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["input", "change"]);
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: FormParameterValue, id: string): void;
|
||||
(e: "change", shouldRefresh: boolean): void;
|
||||
}>();
|
||||
|
||||
//@ts-ignore bad library types
|
||||
library.add(faExclamation, faTimes, faArrowsAltH, faCaretSquareDown, faCaretSquareUp);
|
||||
|
||||
/** 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 attrs: ComputedRef<FormParameterAttributes> = computed(() => props.attributes || useAttrs());
|
||||
const collapsibleValue: ComputedRef<FormParameterValue> = computed(() => attrs.value["collapsible_value"]);
|
||||
const defaultValue: ComputedRef<FormParameterValue> = computed(() => attrs.value["default_value"]);
|
||||
const connectedValue: FormParameterValue = { __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"]);
|
||||
const connectable = computed(() => collapsible.value && Boolean(attrs.value["connectable"]));
|
||||
|
||||
// Determines to wether expand or collapse the input
|
||||
{
|
||||
@@ -117,7 +92,7 @@ const connectable = computed(() => collapsible.value && attrs.value["connectable
|
||||
}
|
||||
|
||||
/** Submits a changed value. */
|
||||
function setValue(value) {
|
||||
function setValue(value: FormParameterValue) {
|
||||
emit("input", value, props.id);
|
||||
emit("change", props.refreshOnChange);
|
||||
}
|
||||
@@ -153,7 +128,7 @@ 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})`)) {
|
||||
if (helpArgument && !props.help?.includes(`(${helpArgument})`)) {
|
||||
return `${props.help} (${helpArgument})`;
|
||||
} else {
|
||||
return props.help;
|
||||
@@ -170,7 +145,9 @@ const currentValue = computed({
|
||||
});
|
||||
|
||||
const isHiddenType = computed(
|
||||
() => ["hidden", "hidden_data", "baseurl"].includes(props.type) || (props.attributes && props.attributes.titleonly)
|
||||
() =>
|
||||
["hidden", "hidden_data", "baseurl"].includes(props.type ?? "") ||
|
||||
(props.attributes && props.attributes.titleonly)
|
||||
);
|
||||
|
||||
const collapseText = computed(() => (collapsed.value ? props.collapsedEnableText : props.collapsedDisableText));
|
||||
@@ -181,7 +158,7 @@ const isEmpty = computed(() => {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (["text", "integer", "float", "password"].includes(props.type) && currentValue.value === "") {
|
||||
if (["text", "integer", "float", "password"].includes(props.type ?? "") && currentValue.value === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -193,14 +170,6 @@ const isRequiredType = computed(() => props.type !== "boolean");
|
||||
const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !== undefined);
|
||||
</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"
|
||||
@@ -247,7 +216,6 @@ library.add(faExclamation, faTimes, faArrowsAltH, faCaretSquareDown, faCaretSqua
|
||||
- optional
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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']" />
|
||||
@@ -257,10 +225,37 @@ library.add(faExclamation, faTimes, faArrowsAltH, faCaretSquareDown, faCaretSqua
|
||||
v-model="currentValue"
|
||||
:max="attrs.max"
|
||||
:min="attrs.min"
|
||||
:type="type"
|
||||
:type="props.type ?? 'float'"
|
||||
:workflow-building-mode="workflowBuildingMode" />
|
||||
<FormOptionalText
|
||||
v-else-if="props.type === 'select' && attrs.is_workflow && attrs.optional"
|
||||
:id="id"
|
||||
v-model="currentValue"
|
||||
:readonly="attrs.readonly"
|
||||
:value="attrs.value"
|
||||
:area="attrs.area"
|
||||
:placeholder="attrs.placeholder"
|
||||
:multiple="attrs.multiple"
|
||||
:datalist="attrs.datalist"
|
||||
:type="props.type" />
|
||||
<FormText
|
||||
v-else-if="
|
||||
['text', 'password'].includes(props.type) ||
|
||||
(attrs.is_workflow && ['select', 'genomebuild', 'data_column', 'group_tag'].includes(props.type))
|
||||
"
|
||||
:id="id"
|
||||
v-model="currentValue"
|
||||
:readonly="attrs.readonly"
|
||||
:value="attrs.value"
|
||||
:area="attrs.area"
|
||||
:placeholder="attrs.placeholder"
|
||||
:color="attrs.color"
|
||||
:multiple="attrs.multiple"
|
||||
:cls="attrs.cls"
|
||||
:datalist="attrs.datalist"
|
||||
:type="props.type" />
|
||||
<FormSelection
|
||||
v-else-if="props.type == 'select' && attrs.display == 'radio'"
|
||||
v-else-if="props.type === 'select' && ['radio', 'checkboxes'].includes(attrs.display)"
|
||||
:id="id"
|
||||
v-model="currentValue"
|
||||
:data="attrs.data"
|
||||
@@ -270,12 +265,13 @@ library.add(faExclamation, faTimes, faArrowsAltH, faCaretSquareDown, faCaretSqua
|
||||
: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" />
|
||||
<FormRulesEdit v-else-if="props.type == 'rules'" v-model="currentValue" :target="attrs.target" />
|
||||
<FormParameter
|
||||
v-else-if="backbonejs"
|
||||
:id="props.id"
|
||||
v-model="currentValue"
|
||||
:data-label="props.title"
|
||||
:type="props.type"
|
||||
:type="props.type ?? 'text'"
|
||||
:attributes="attrs" />
|
||||
<FormInput v-else :id="props.id" v-model="currentValue" :area="attrs['area']" />
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { safePath } from "utils/redirect";
|
||||
import { withPrefix } from "utils/redirect";
|
||||
import { submitData } from "./services";
|
||||
import { UrlDataProvider } from "components/providers/UrlDataProvider";
|
||||
import { visitInputs } from "components/Form/utilities";
|
||||
@@ -99,7 +99,7 @@ export default {
|
||||
this.formData = formData;
|
||||
},
|
||||
onCancel() {
|
||||
window.location = safePath(this.cancelRedirect);
|
||||
window.location = withPrefix(this.cancelRedirect);
|
||||
},
|
||||
onSubmit() {
|
||||
submitData(this.url, this.formData).then((response) => {
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
}
|
||||
if (this.redirect) {
|
||||
const urlParams = new URLSearchParams(params);
|
||||
window.location = safePath(`${this.redirect}?${urlParams.toString()}`);
|
||||
window.location = withPrefix(`${this.redirect}?${urlParams.toString()}`);
|
||||
} else {
|
||||
const replaceParams = {};
|
||||
visitInputs(response.inputs, (input, name) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import FormMessage from "./FormMessage";
|
||||
|
||||
jest.mock("app");
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// TODO: stricter types
|
||||
export type FormParameterValue = any;
|
||||
export type FormParameterAttributes = {
|
||||
[attribute: string]: any;
|
||||
};
|
||||
|
||||
export type FormParameterTypes =
|
||||
| "boolean"
|
||||
| "hidden"
|
||||
| "hidden_data"
|
||||
| "baseurl"
|
||||
| "integer"
|
||||
| "float"
|
||||
| "radio"
|
||||
| "color"
|
||||
| "directory_uri"
|
||||
| "text"
|
||||
| "password"
|
||||
| "select"
|
||||
| "data_column"
|
||||
| "genomebuild"
|
||||
| "data"
|
||||
| "data_collection"
|
||||
| "drill_down"
|
||||
| "group_tag"
|
||||
| "library_data"
|
||||
| "ftpfile"
|
||||
| "upload"
|
||||
| "rules"
|
||||
| "data_dialog";
|
||||
@@ -1,10 +1,10 @@
|
||||
import axios from "axios";
|
||||
import { rethrowSimple } from "utils/simple-error";
|
||||
import { safePath } from "utils/redirect";
|
||||
import { withPrefix } from "utils/redirect";
|
||||
|
||||
export async function submitData(url, payload) {
|
||||
try {
|
||||
const { data } = await axios.put(safePath(url), payload);
|
||||
const { data } = await axios.put(withPrefix(url), payload);
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import CollectionDescription from "./CollectionDescription";
|
||||
import { JobStateSummary } from "./JobStateSummary";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import CollectionProgress from "./CollectionProgress";
|
||||
import { JobStateSummary } from "./JobStateSummary";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import ContentItem from "./ContentItem";
|
||||
import { updateContentFields } from "components/History/model/queries";
|
||||
|
||||
@@ -30,50 +30,70 @@ describe("ContentItem", () => {
|
||||
name: "name",
|
||||
selected: false,
|
||||
selectable: false,
|
||||
filterable: true,
|
||||
},
|
||||
localVue,
|
||||
stubs: {
|
||||
DatasetDetails: true,
|
||||
vueTagsInput: false,
|
||||
},
|
||||
provide: {
|
||||
store: {
|
||||
dispatch: jest.fn,
|
||||
getters: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("check basics", async () => {
|
||||
expect(wrapper.attributes("data-hid")).toBe("1");
|
||||
expect(wrapper.find(".content-title").text()).toBe("name");
|
||||
const tags = wrapper.find(".alltags").findAll(".ti-tag");
|
||||
const tags = wrapper.find(".stateless-tags").findAll(".tag");
|
||||
|
||||
// verify tags
|
||||
expect(tags.length).toBe(3);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(tags.at(i).text()).toBe(`tag${i + 1}`);
|
||||
await tags.at(i).find(".tag-name").trigger("click");
|
||||
|
||||
await tags.at(i).trigger("click");
|
||||
expect(wrapper.emitted()["tag-click"][i][0]).toBe(`tag${i + 1}`);
|
||||
}
|
||||
|
||||
// close all tags
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const tagRemover = wrapper.find(".ti-icon-close");
|
||||
const tagRemover = wrapper.find(`.tag[data-option=tag${i + 1}] button`);
|
||||
|
||||
await tagRemover.trigger("click");
|
||||
expect(wrapper.emitted()["tag-change"][i][1]).not.toContain(`tag${i + 1}`);
|
||||
}
|
||||
|
||||
await wrapper.setProps({ isHistoryItem: false, item: { tags: [] } });
|
||||
expect(wrapper.find(".alltags").exists()).toBe(false);
|
||||
expect(wrapper.find(".stateless-tags").exists()).toBe(false);
|
||||
|
||||
// expansion button
|
||||
const $el = wrapper.find(".cursor-pointer");
|
||||
$el.trigger("click");
|
||||
expect(wrapper.emitted()["update:expand-dataset"]).toBeDefined();
|
||||
|
||||
// select and unselect
|
||||
const noSelector = wrapper.find(".selector > svg");
|
||||
expect(noSelector.exists()).toBe(false);
|
||||
|
||||
await wrapper.setProps({ selectable: true });
|
||||
expect(wrapper.classes()).toEqual(expect.arrayContaining(["alert-success"]));
|
||||
|
||||
const selector = wrapper.find(".selector > svg");
|
||||
expect(selector.attributes("data-icon")).toBe("square");
|
||||
selector.trigger("click");
|
||||
|
||||
await localVue.nextTick();
|
||||
expect(wrapper.emitted()["update:selected"][0][0]).toBe(true);
|
||||
|
||||
await wrapper.setProps({ selected: true });
|
||||
selector.trigger("click");
|
||||
|
||||
await localVue.nextTick();
|
||||
expect(wrapper.emitted()["update:selected"][1][0]).toBe(false);
|
||||
expect(wrapper.classes()).toEqual(expect.arrayContaining(["alert-info"]));
|
||||
|
||||
@@ -3,57 +3,45 @@
|
||||
:id="contentId"
|
||||
:class="['content-item m-1 p-0 rounded btn-transparent-background', contentCls]"
|
||||
:data-hid="id"
|
||||
:data-state="state">
|
||||
<div
|
||||
class="p-1 cursor-pointer"
|
||||
draggable
|
||||
tabindex="0"
|
||||
@dragstart="onDragStart"
|
||||
@click.stop="onClick"
|
||||
@keypress="onClick">
|
||||
:data-state="state"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown="onKeyDown">
|
||||
<div class="p-1 cursor-pointer" draggable @dragstart="onDragStart" @click.stop="onClick">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="p-1 font-weight-bold">
|
||||
<span v-if="selectable" class="selector">
|
||||
<icon
|
||||
v-if="selected"
|
||||
fixed-width
|
||||
size="lg"
|
||||
:icon="['far', 'check-square']"
|
||||
@click.stop="$emit('update:selected', false)" />
|
||||
<icon
|
||||
v-else
|
||||
fixed-width
|
||||
size="lg"
|
||||
:icon="['far', 'square']"
|
||||
@click.stop="$emit('update:selected', true)" />
|
||||
</span>
|
||||
<span
|
||||
<b-button v-if="selectable" class="selector p-0" @click.stop="$emit('update:selected', !selected)">
|
||||
<icon v-if="selected" fixed-width size="lg" :icon="['far', 'check-square']" />
|
||||
<icon v-else fixed-width size="lg" :icon="['far', 'square']" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="highlight == 'input'"
|
||||
v-b-tooltip.hover
|
||||
variant="link"
|
||||
class="p-0"
|
||||
title="Input"
|
||||
tabindex="0"
|
||||
@click.stop="toggleHighlights"
|
||||
@keypress="toggleHighlights">
|
||||
@click.stop="toggleHighlights">
|
||||
<font-awesome-icon class="text-info" icon="arrow-circle-up" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="highlight == 'noInputs'"
|
||||
</b-button>
|
||||
<b-button
|
||||
v-else-if="highlight == 'active'"
|
||||
v-b-tooltip.hover
|
||||
title="No Inputs for this item"
|
||||
tabindex="0"
|
||||
@click.stop="toggleHighlights"
|
||||
@keypress="toggleHighlights">
|
||||
<font-awesome-icon icon="minus-circle" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="highlight == 'output'"
|
||||
v-b-tooltip.hover
|
||||
title="Inputs highlighted for this item"
|
||||
tabindex="0"
|
||||
variant="link"
|
||||
class="p-0"
|
||||
title="Inputs/Outputs highlighted for this item"
|
||||
@click.stop="toggleHighlights"
|
||||
@keypress="toggleHighlights">
|
||||
<font-awesome-icon icon="check-circle" />
|
||||
</span>
|
||||
</b-button>
|
||||
<b-button
|
||||
v-else-if="highlight == 'output'"
|
||||
v-b-tooltip.hover
|
||||
variant="link"
|
||||
class="p-0"
|
||||
title="Output"
|
||||
@click.stop="toggleHighlights">
|
||||
<font-awesome-icon class="text-info" icon="arrow-circle-down" />
|
||||
</b-button>
|
||||
<span v-if="hasStateIcon" class="state-icon">
|
||||
<icon fixed-width :icon="contentState.icon" :spin="contentState.spin" />
|
||||
</span>
|
||||
@@ -75,6 +63,7 @@
|
||||
:is-visible="item.visible"
|
||||
:state="state"
|
||||
:item-urls="itemUrls"
|
||||
:keyboard-selectable="expandDataset"
|
||||
@delete="$emit('delete')"
|
||||
@display="onDisplay"
|
||||
@showCollectionInfo="onShowCollectionInfo"
|
||||
@@ -92,19 +81,19 @@
|
||||
:elements-datatypes="item.elements_datatypes" />
|
||||
<StatelessTags
|
||||
v-if="!tagsDisabled || hasTags"
|
||||
class="alltags p-1"
|
||||
:value="tags"
|
||||
:use-toggle-link="false"
|
||||
:disabled="tagsDisabled"
|
||||
@tag-click="onTagClick"
|
||||
@input="onTags" />
|
||||
:clickable="filterable"
|
||||
:use-toggle-link="false"
|
||||
@input="onTags"
|
||||
@tag-click="onTagClick" />
|
||||
<!-- collections are not expandable, so we only need the DatasetDetails component here -->
|
||||
<b-collapse :visible="expandDataset">
|
||||
<DatasetDetails
|
||||
v-if="expandDataset"
|
||||
:dataset="item"
|
||||
:writable="writable"
|
||||
:show-highlight="isHistoryItem"
|
||||
:show-highlight="isHistoryItem && filterable"
|
||||
:item-urls="itemUrls"
|
||||
@edit="onEdit"
|
||||
@toggleHighlights="toggleHighlights" />
|
||||
@@ -113,7 +102,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { StatelessTags } from "components/Tags";
|
||||
import StatelessTags from "components/TagsMultiselect/StatelessTags";
|
||||
import { STATES, HIERARCHICAL_COLLECTION_JOB_STATES } from "./model/states";
|
||||
import CollectionDescription from "./Collection/CollectionDescription";
|
||||
import ContentOptions from "./ContentOptions";
|
||||
@@ -122,10 +111,10 @@ import { updateContentFields } from "components/History/model/queries";
|
||||
import { JobStateSummary } from "./Collection/JobStateSummary";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faArrowCircleUp, faMinusCircle, faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faArrowCircleUp, faMinusCircle, faCheckCircle);
|
||||
import { faArrowCircleUp, faArrowCircleDown, faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useEntryPointStore } from "stores/entryPointStore";
|
||||
|
||||
library.add(faArrowCircleUp, faArrowCircleDown, faCheckCircle);
|
||||
export default {
|
||||
components: {
|
||||
CollectionDescription,
|
||||
@@ -145,6 +134,7 @@ export default {
|
||||
name: { type: String, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
selectable: { type: Boolean, default: false },
|
||||
filterable: { type: Boolean, default: false },
|
||||
},
|
||||
computed: {
|
||||
jobState() {
|
||||
@@ -216,6 +206,15 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onKeyDown(event) {
|
||||
if (!event.target.classList.contains("content-item")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
this.onClick();
|
||||
}
|
||||
},
|
||||
onClick() {
|
||||
if (this.isDataset) {
|
||||
this.$emit("update:expand-dataset", !this.expandDataset);
|
||||
@@ -224,7 +223,15 @@ export default {
|
||||
}
|
||||
},
|
||||
onDisplay() {
|
||||
this.$router.push(this.itemUrls.display, { title: this.name });
|
||||
const entryPointStore = useEntryPointStore();
|
||||
const entryPointsForHda = entryPointStore.entryPointsForHda(this.item.id);
|
||||
if (entryPointsForHda && entryPointsForHda.length > 0) {
|
||||
// there can be more than one entry point, choose the first
|
||||
const url = entryPointsForHda[0].target;
|
||||
window.open(url, "_blank");
|
||||
} else {
|
||||
this.$router.push(this.itemUrls.display, { title: this.name });
|
||||
}
|
||||
},
|
||||
onDragStart(evt) {
|
||||
evt.dataTransfer.dropEffect = "move";
|
||||
@@ -242,7 +249,9 @@ export default {
|
||||
updateContentFields(this.item, { tags: newTags });
|
||||
},
|
||||
onTagClick(tag) {
|
||||
this.$emit("tag-click", tag.label);
|
||||
if (this.filterable) {
|
||||
this.$emit("tag-click", tag);
|
||||
}
|
||||
},
|
||||
toggleHighlights() {
|
||||
this.$emit("toggleHighlights", this.item);
|
||||
@@ -250,10 +259,21 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "~bootstrap/scss/_functions.scss";
|
||||
@import "theme/blue.scss";
|
||||
|
||||
.content-item {
|
||||
cursor: default;
|
||||
|
||||
.name {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// improve focus visibility
|
||||
&:deep(.btn:focus) {
|
||||
box-shadow: 0 0 0 0.2rem transparentize($brand-primary, 0.75);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
v-if="isDataset"
|
||||
:disabled="displayDisabled"
|
||||
:title="displayButtonTitle"
|
||||
:tabindex="tabindex"
|
||||
class="display-btn px-1"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@@ -28,6 +29,7 @@
|
||||
v-if="writable && isHistoryItem"
|
||||
:disabled="editDisabled"
|
||||
:title="editButtonTitle"
|
||||
:tabindex="tabindex"
|
||||
class="edit-btn px-1"
|
||||
size="sm"
|
||||
variant="link"
|
||||
@@ -37,6 +39,7 @@
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="writable && isHistoryItem && !isDeleted"
|
||||
:tabindex="tabindex"
|
||||
class="delete-btn px-1"
|
||||
title="Delete"
|
||||
size="sm"
|
||||
@@ -46,6 +49,7 @@
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="writable && isHistoryItem && isDeleted"
|
||||
:tabindex="tabindex"
|
||||
class="undelete-btn px-1"
|
||||
title="Undelete"
|
||||
size="sm"
|
||||
@@ -55,6 +59,7 @@
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="writable && isHistoryItem && !isVisible"
|
||||
:tabindex="tabindex"
|
||||
class="unhide-btn px-1"
|
||||
title="Unhide"
|
||||
size="sm"
|
||||
@@ -66,7 +71,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { prependPath } from "utils/redirect.js";
|
||||
import { prependPath } from "@/utils/redirect";
|
||||
export default {
|
||||
props: {
|
||||
writable: { type: Boolean, default: true },
|
||||
@@ -76,6 +81,7 @@ export default {
|
||||
isVisible: { type: Boolean, default: true },
|
||||
state: { type: String, default: "" },
|
||||
itemUrls: { type: Object, required: true },
|
||||
keyboardSelectable: { type: Boolean, default: true },
|
||||
},
|
||||
computed: {
|
||||
displayButtonTitle() {
|
||||
@@ -111,6 +117,9 @@ export default {
|
||||
showCollectionDetailsUrl() {
|
||||
return prependPath(this.itemUrls.showDetails);
|
||||
},
|
||||
tabindex() {
|
||||
return this.keyboardSelectable ? "0" : "-1";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<script>
|
||||
import { copy as sendToClipboard } from "utils/clipboard";
|
||||
import { absPath, prependPath } from "utils/redirect.js";
|
||||
import { absPath, prependPath } from "@/utils/redirect";
|
||||
import { downloadUrlMixin } from "./mixins.js";
|
||||
import DatasetDownload from "./DatasetDownload";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import DatasetDownload from "./DatasetDownload";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* It allows to select individual items or perform a query selection.
|
||||
*/
|
||||
|
||||
import { getFilters, testFilters } from "utils/filterConversion";
|
||||
import { HistoryFilters } from "../HistoryFilters";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
return this.allSelected && this.totalItemsInQuery !== this.items.size;
|
||||
},
|
||||
currentFilters() {
|
||||
return getFilters(this.filterText);
|
||||
return HistoryFilters.getFilters(this.filterText);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -40,7 +40,7 @@ export default {
|
||||
},
|
||||
isSelected(item) {
|
||||
if (this.isQuerySelection) {
|
||||
return testFilters(this.currentFilters, item);
|
||||
return HistoryFilters.testFilters(this.currentFilters, item);
|
||||
}
|
||||
const key = this.getItemKey(item);
|
||||
return this.items.has(key);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import flushPromises from "flush-promises";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import SelectedItems from "./SelectedItems";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Specifies highlighted items in the history listing. The `highlight` property is passed to
|
||||
* the content item component and can be used to modify its appearance.
|
||||
* TO DO: Consider case where parameter history is different and hence inputs cannot be seen
|
||||
* in the current panel.
|
||||
*/
|
||||
import axios from "axios";
|
||||
import { prependPath } from "utils/redirect";
|
||||
import { deepeach } from "utils/utils";
|
||||
import { LastQueue } from "utils/promise-queue";
|
||||
|
||||
// add promise queue
|
||||
const lastQueue = new LastQueue(300);
|
||||
|
||||
/** Local cache for parameter requests */
|
||||
const paramStash = new Map();
|
||||
|
||||
/** Performs request to obtain dataset parameters */
|
||||
async function getDatasetParameters(datasetId, jobId) {
|
||||
if (!paramStash.has(datasetId)) {
|
||||
const url = jobId
|
||||
? `api/jobs/${jobId}/parameters_display`
|
||||
: `api/datasets/${datasetId}/parameters_display?hda_ldda=hda`;
|
||||
const { data } = await lastQueue.enqueue(axios.get, prependPath(url));
|
||||
paramStash.set(datasetId, data);
|
||||
}
|
||||
return paramStash.get(datasetId);
|
||||
}
|
||||
|
||||
/** Returns item key */
|
||||
function getKey(details) {
|
||||
if (details.id && details.src) {
|
||||
const historyContentType = details.src == "hda" ? "dataset" : "dataset_collection";
|
||||
return `${details.id}-${historyContentType}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns highlighting details */
|
||||
export async function getHighlights(item, itemKey) {
|
||||
const highlights = {};
|
||||
const { outputs, parameters } = await getDatasetParameters(item.id, item.job_source_id);
|
||||
deepeach(parameters, (details) => {
|
||||
const key = getKey(details);
|
||||
if (key) {
|
||||
highlights[key] = "input";
|
||||
}
|
||||
});
|
||||
deepeach(outputs, (details) => {
|
||||
const key = getKey(details);
|
||||
if (key) {
|
||||
// some other item created this item (e.g.: inheritance)
|
||||
if (key != itemKey) {
|
||||
highlights[itemKey] = "output";
|
||||
highlights[key] = "input";
|
||||
} else {
|
||||
highlights[key] = "output";
|
||||
}
|
||||
}
|
||||
});
|
||||
// highlights only has item itself as an output (i.e.: no inputs)
|
||||
if (highlights[itemKey] === "output" && Object.keys(highlights).length == 1) {
|
||||
highlights[itemKey] = "noInputs";
|
||||
}
|
||||
// TO DO: Consider case where a job created multiple items (all highlights are outputs)
|
||||
return highlights;
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
:name="item.element_identifier"
|
||||
:expand-dataset="isExpanded(item)"
|
||||
:is-dataset="item.element_type == 'hda'"
|
||||
:filterable="filterable"
|
||||
@update:expand-dataset="setExpanded(item, $event)"
|
||||
@view-collection="onViewSubCollection" />
|
||||
</template>
|
||||
@@ -63,6 +64,7 @@ export default {
|
||||
history: { type: Object, required: true },
|
||||
selectedCollections: { type: Array, required: true },
|
||||
showControls: { type: Boolean, default: true },
|
||||
filterable: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -13,14 +13,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { openGlobalUploadModal } from "components/Upload";
|
||||
import { useGlobalUploadModal } from "composables/globalUploadModal";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
message: { type: String, default: "This history is empty." },
|
||||
},
|
||||
setup() {
|
||||
const { openGlobalUploadModal } = useGlobalUploadModal();
|
||||
return { openGlobalUploadModal };
|
||||
},
|
||||
methods: {
|
||||
openGlobalUploadModal,
|
||||
clickDataLink() {
|
||||
this.eventHub.$emit("openToolSection", "getext");
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import HistoryFilters from "./HistoryFilters";
|
||||
import { getQueryDict } from "utils/filterConversion";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import { HistoryFilters as HistoryFiltering } from "components/History/HistoryFilters";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -13,8 +13,8 @@ describe("HistoryFilters", () => {
|
||||
expect(wrapper.emitted()["update:show-advanced"][toggleEmit][0]).toEqual(showAdvanced);
|
||||
await wrapper.setProps({ showAdvanced: wrapper.emitted()["update:show-advanced"][toggleEmit][0] });
|
||||
const receivedText = wrapper.emitted()["update:filter-text"][filterEmit][0];
|
||||
const receivedDict = getQueryDict(receivedText);
|
||||
const parsedDict = getQueryDict(filterText);
|
||||
const receivedDict = HistoryFiltering.getQueryDict(receivedText);
|
||||
const parsedDict = HistoryFiltering.getQueryDict(filterText);
|
||||
expect(receivedDict).toEqual(parsedDict);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ describe("HistoryFilters", () => {
|
||||
expect(wrapper.find("[description='advanced filters']").exists()).toBe(false);
|
||||
await wrapper.setProps({ showAdvanced: true });
|
||||
expect(wrapper.find("[description='advanced filters']").exists()).toBe(true);
|
||||
expect(wrapper.find("[description='advanced filters']").exists()).toBe(true);
|
||||
const filterInputs = {
|
||||
"[placeholder='any name']": "name-filter",
|
||||
"[placeholder='any extension']": "ext-filter",
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
<small class="mt-1">Filter by state:</small>
|
||||
<b-form-input v-model="filterSettings['state:']" size="sm" placeholder="any state" list="stateSelect" />
|
||||
<b-form-datalist id="stateSelect" :options="states"></b-form-datalist>
|
||||
<small class="mt-1">Filter by related to item index:</small>
|
||||
<b-form-input v-model="filterSettings['related:']" size="sm" placeholder="index equals" />
|
||||
<small class="mt-1">Filter by item index:</small>
|
||||
<b-form-group class="m-0">
|
||||
<b-input-group>
|
||||
@@ -85,7 +87,7 @@
|
||||
import DebouncedInput from "components/DebouncedInput";
|
||||
import HistoryFiltersDefault from "./HistoryFiltersDefault";
|
||||
import { STATES } from "components/History/Content/model/states";
|
||||
import { getFilters, getFilterText, toAlias } from "utils/filterConversion";
|
||||
import { HistoryFilters } from "components/History/HistoryFilters";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -104,7 +106,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
filterSettings() {
|
||||
return toAlias(getFilters(this.filterText));
|
||||
return HistoryFilters.toAlias(HistoryFilters.getFilters(this.filterText));
|
||||
},
|
||||
localFilter: {
|
||||
get() {
|
||||
@@ -134,7 +136,7 @@ export default {
|
||||
this.onToggle();
|
||||
this.filterSettings["create_time>"] = this.create_time_gt;
|
||||
this.filterSettings["create_time<"] = this.create_time_lt;
|
||||
this.updateFilter(getFilterText(this.filterSettings));
|
||||
this.updateFilter(HistoryFilters.getFilterText(this.filterSettings));
|
||||
},
|
||||
onToggle() {
|
||||
this.$emit("update:show-advanced", !this.showAdvanced);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mockModule, getLocalVue } from "jest/helpers";
|
||||
import { mockModule, getLocalVue } from "tests/jest/helpers";
|
||||
import { userStore } from "store/userStore";
|
||||
import Vuex from "vuex";
|
||||
import HistoryNavigation from "./HistoryNavigation";
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { shallowMount } from "@vue/test-utils";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import flushPromises from "flush-promises";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import SelectionOperations from "./SelectionOperations.vue";
|
||||
import MockConfigProvider from "components/providers/MockConfigProvider";
|
||||
|
||||
|
||||
+4
-4
@@ -151,13 +151,13 @@ import {
|
||||
addTagsToSelectedContent,
|
||||
removeTagsFromSelectedContent,
|
||||
} from "components/History/model/crud";
|
||||
import { checkFilter, getQueryDict } from "utils/filterConversion";
|
||||
import { createDatasetCollection } from "components/History/model/queries";
|
||||
import { buildCollectionModal } from "components/History/adapters/buildCollectionModal";
|
||||
import { DbKeyProvider, DatatypesProvider } from "components/providers";
|
||||
import SingleItemSelector from "components/SingleItemSelector";
|
||||
import { StatelessTags } from "components/Tags";
|
||||
import ConfigProvider from "components/providers/ConfigProvider";
|
||||
import { HistoryFilters } from "components/History/HistoryFilters";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -185,11 +185,11 @@ export default {
|
||||
computed: {
|
||||
/** @returns {Boolean} */
|
||||
showHidden() {
|
||||
return checkFilter(this.filterText, "visible", false);
|
||||
return HistoryFilters.checkFilter(this.filterText, "visible", false);
|
||||
},
|
||||
/** @returns {Boolean} */
|
||||
showDeleted() {
|
||||
return checkFilter(this.filterText, "deleted", true);
|
||||
return HistoryFilters.checkFilter(this.filterText, "deleted", true);
|
||||
},
|
||||
/** @returns {Boolean} */
|
||||
showBuildOptions() {
|
||||
@@ -265,7 +265,7 @@ export default {
|
||||
async runOnSelection(operation, extraParams = null) {
|
||||
this.$emit("update:operation-running", this.history.update_time);
|
||||
const items = this.getExplicitlySelectedItems();
|
||||
const filters = getQueryDict(this.filterText);
|
||||
const filters = HistoryFilters.getQueryDict(this.filterText);
|
||||
this.$emit("update:show-selection", false);
|
||||
let expectHistoryUpdate = false;
|
||||
try {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import flushPromises from "flush-promises";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import HistorySelectionStatus from "./SelectionStatus.vue";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -1,156 +1,149 @@
|
||||
<template>
|
||||
<HistoryItemsProvider
|
||||
:key="historyId"
|
||||
v-slot="{ loading, result: itemsLoaded, count: totalItemsInQuery }"
|
||||
:history-id="historyId"
|
||||
:offset="offset"
|
||||
:update-time="history.update_time"
|
||||
:filter-text="filterText">
|
||||
<ExpandedItems
|
||||
v-slot="{ expandedCount, isExpanded, setExpanded, collapseAll }"
|
||||
:scope-key="historyId"
|
||||
:get-item-key="(item) => item.type_id">
|
||||
<SelectedItems
|
||||
v-slot="{
|
||||
selectedItems,
|
||||
showSelection,
|
||||
isQuerySelection,
|
||||
selectionSize,
|
||||
setShowSelection,
|
||||
selectAllInCurrentQuery,
|
||||
isSelected,
|
||||
setSelected,
|
||||
resetSelection,
|
||||
}"
|
||||
:scope-key="queryKey"
|
||||
:get-item-key="(item) => item.type_id"
|
||||
:filter-text="filterText"
|
||||
:total-items-in-query="totalItemsInQuery"
|
||||
@query-selection-break="querySelectionBreak = true">
|
||||
<section
|
||||
class="history-layout d-flex flex-column w-100"
|
||||
@drop.prevent="onDrop"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
@dragover.prevent
|
||||
@dragleave.prevent="onDragLeave">
|
||||
<slot name="navigation" :history="history" />
|
||||
<HistoryFilters
|
||||
<ExpandedItems
|
||||
v-slot="{ expandedCount, isExpanded, setExpanded, collapseAll }"
|
||||
:scope-key="historyId"
|
||||
:get-item-key="(item) => item.type_id">
|
||||
<SelectedItems
|
||||
v-slot="{
|
||||
selectedItems,
|
||||
showSelection,
|
||||
isQuerySelection,
|
||||
selectionSize,
|
||||
setShowSelection,
|
||||
selectAllInCurrentQuery,
|
||||
isSelected,
|
||||
setSelected,
|
||||
resetSelection,
|
||||
}"
|
||||
:scope-key="queryKey"
|
||||
:get-item-key="(item) => item.type_id"
|
||||
:filter-text="filterText"
|
||||
:total-items-in-query="totalItemsInQuery"
|
||||
@query-selection-break="querySelectionBreak = true">
|
||||
<section
|
||||
class="history-layout d-flex flex-column w-100"
|
||||
@drop.prevent="onDrop"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
@dragover.prevent
|
||||
@dragleave.prevent="onDragLeave">
|
||||
<slot name="navigation" :history="history" />
|
||||
<HistoryFilters
|
||||
v-if="showControls"
|
||||
class="content-operations-filters mx-3"
|
||||
:filter-text.sync="filterText"
|
||||
:show-advanced.sync="showAdvanced" />
|
||||
<section v-if="!showAdvanced">
|
||||
<HistoryDetails
|
||||
:history="history"
|
||||
:writeable="writable"
|
||||
@update:history="$emit('updateHistory', $event)" />
|
||||
<HistoryMessages :history="history" />
|
||||
<HistoryCounter
|
||||
v-if="showControls"
|
||||
class="content-operations-filters mx-3"
|
||||
:history="history"
|
||||
:is-watching="isWatching"
|
||||
:last-checked="lastChecked"
|
||||
:filter-text.sync="filterText"
|
||||
:show-advanced.sync="showAdvanced" />
|
||||
<section v-if="!showAdvanced">
|
||||
<HistoryDetails
|
||||
:history="history"
|
||||
:writeable="writable"
|
||||
@update:history="$emit('updateHistory', $event)" />
|
||||
<HistoryMessages :history="history" />
|
||||
<HistoryCounter
|
||||
v-if="showControls"
|
||||
:history="history"
|
||||
:is-watching="isWatching"
|
||||
:last-checked="lastChecked"
|
||||
:filter-text.sync="filterText"
|
||||
@reloadContents="reloadContents" />
|
||||
<HistoryOperations
|
||||
v-if="showControls"
|
||||
:history="history"
|
||||
:show-selection="showSelection"
|
||||
:expanded-count="expandedCount"
|
||||
:has-matches="hasMatches(itemsLoaded)"
|
||||
:operation-running.sync="operationRunning"
|
||||
@update:show-selection="setShowSelection"
|
||||
@collapse-all="collapseAll">
|
||||
<template v-slot:selection-operations>
|
||||
<HistorySelectionOperations
|
||||
:history="history"
|
||||
:filter-text="filterText"
|
||||
:content-selection="selectedItems"
|
||||
:selection-size="selectionSize"
|
||||
:is-query-selection="isQuerySelection"
|
||||
:total-items-in-query="totalItemsInQuery"
|
||||
:operation-running.sync="operationRunning"
|
||||
@update:show-selection="setShowSelection"
|
||||
@operation-error="onOperationError"
|
||||
@hide-selection="onHideSelection"
|
||||
@reset-selection="resetSelection" />
|
||||
<HistorySelectionStatus
|
||||
v-if="showSelection"
|
||||
:selection-size="selectionSize"
|
||||
@select-all="selectAllInCurrentQuery(itemsLoaded)"
|
||||
@reset-selection="resetSelection" />
|
||||
</template>
|
||||
</HistoryOperations>
|
||||
<SelectionChangeWarning :query-selection-break="querySelectionBreak" />
|
||||
<OperationErrorDialog
|
||||
v-if="operationError"
|
||||
:operation-error="operationError"
|
||||
@hide="operationError = null" />
|
||||
</section>
|
||||
<section v-if="!showAdvanced" class="position-relative flex-grow-1 scroller">
|
||||
<history-drop-zone v-if="showDropZone" />
|
||||
<div>
|
||||
<div v-if="loading && itemsLoaded && itemsLoaded.length === 0">
|
||||
<b-alert class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Loading History" />
|
||||
</b-alert>
|
||||
</div>
|
||||
<b-alert v-else-if="isProcessing" class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Processing operation" />
|
||||
</b-alert>
|
||||
<div v-else-if="itemsLoaded.length === 0">
|
||||
<HistoryEmpty v-if="queryDefault" class="m-2" />
|
||||
<b-alert v-else class="m-2" variant="info" show>
|
||||
No data found for selected filter.
|
||||
</b-alert>
|
||||
</div>
|
||||
<ListingLayout
|
||||
v-else
|
||||
:offset="listOffset"
|
||||
:items="itemsLoaded"
|
||||
:query-key="queryKey"
|
||||
@scroll="onScroll">
|
||||
<template v-slot:item="{ item, currentOffset }">
|
||||
<ContentItem
|
||||
v-if="!invisible[item.hid]"
|
||||
:id="item.hid"
|
||||
is-history-item
|
||||
:item="item"
|
||||
:name="item.name"
|
||||
:writable="writable"
|
||||
:expand-dataset="isExpanded(item)"
|
||||
:is-dataset="isDataset(item)"
|
||||
:highlight="getHighlight(item)"
|
||||
:selected="isSelected(item)"
|
||||
:selectable="showSelection"
|
||||
@tag-click="onTagClick"
|
||||
@tag-change="onTagChange"
|
||||
@toggleHighlights="toggleHighlights"
|
||||
@update:expand-dataset="setExpanded(item, $event)"
|
||||
@update:selected="setSelected(item, $event)"
|
||||
@view-collection="$emit('view-collection', item, currentOffset)"
|
||||
@delete="onDelete(item)"
|
||||
@undelete="onUndelete(item)"
|
||||
@unhide="onUnhide(item)" />
|
||||
</template>
|
||||
</ListingLayout>
|
||||
</div>
|
||||
</section>
|
||||
@reloadContents="reloadContents" />
|
||||
<HistoryOperations
|
||||
v-if="showControls"
|
||||
:history="history"
|
||||
:show-selection="showSelection"
|
||||
:expanded-count="expandedCount"
|
||||
:has-matches="hasMatches(itemsLoaded)"
|
||||
:operation-running.sync="operationRunning"
|
||||
@update:show-selection="setShowSelection"
|
||||
@collapse-all="collapseAll">
|
||||
<template v-slot:selection-operations>
|
||||
<HistorySelectionOperations
|
||||
:history="history"
|
||||
:filter-text="filterText"
|
||||
:content-selection="selectedItems"
|
||||
:selection-size="selectionSize"
|
||||
:is-query-selection="isQuerySelection"
|
||||
:total-items-in-query="totalItemsInQuery"
|
||||
:operation-running.sync="operationRunning"
|
||||
@update:show-selection="setShowSelection"
|
||||
@operation-error="onOperationError"
|
||||
@hide-selection="onHideSelection"
|
||||
@reset-selection="resetSelection" />
|
||||
<HistorySelectionStatus
|
||||
v-if="showSelection"
|
||||
:selection-size="selectionSize"
|
||||
@select-all="selectAllInCurrentQuery(itemsLoaded)"
|
||||
@reset-selection="resetSelection" />
|
||||
</template>
|
||||
</HistoryOperations>
|
||||
<SelectionChangeWarning :query-selection-break="querySelectionBreak" />
|
||||
<OperationErrorDialog
|
||||
v-if="operationError"
|
||||
:operation-error="operationError"
|
||||
@hide="operationError = null" />
|
||||
</section>
|
||||
</SelectedItems>
|
||||
</ExpandedItems>
|
||||
</HistoryItemsProvider>
|
||||
<section v-if="!showAdvanced" class="position-relative flex-grow-1 scroller">
|
||||
<history-drop-zone v-if="showDropZone" />
|
||||
<div>
|
||||
<div v-if="loading && itemsLoaded && itemsLoaded.length === 0">
|
||||
<b-alert class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Loading History" />
|
||||
</b-alert>
|
||||
</div>
|
||||
<b-alert v-else-if="isProcessing" class="m-2" variant="info" show>
|
||||
<LoadingSpan message="Processing operation" />
|
||||
</b-alert>
|
||||
<div v-else-if="itemsLoaded.length === 0">
|
||||
<HistoryEmpty v-if="queryDefault" class="m-2" />
|
||||
<b-alert v-else class="m-2" variant="info" show>
|
||||
No data found for selected filter.
|
||||
</b-alert>
|
||||
</div>
|
||||
<ListingLayout
|
||||
v-else
|
||||
:offset="listOffset"
|
||||
:items="itemsLoaded"
|
||||
:query-key="queryKey"
|
||||
@scroll="onScroll">
|
||||
<template v-slot:item="{ item, currentOffset }">
|
||||
<ContentItem
|
||||
v-if="!invisible[item.hid]"
|
||||
:id="item.hid"
|
||||
is-history-item
|
||||
:item="item"
|
||||
:name="item.name"
|
||||
:writable="writable"
|
||||
:expand-dataset="isExpanded(item)"
|
||||
:is-dataset="isDataset(item)"
|
||||
:highlight="getHighlight(item)"
|
||||
:selected="isSelected(item)"
|
||||
:selectable="showSelection"
|
||||
:filterable="filterable"
|
||||
@tag-click="onTagClick"
|
||||
@tag-change="onTagChange"
|
||||
@toggleHighlights="toggleHighlights"
|
||||
@update:expand-dataset="setExpanded(item, $event)"
|
||||
@update:selected="setSelected(item, $event)"
|
||||
@view-collection="$emit('view-collection', item, currentOffset)"
|
||||
@delete="onDelete(item)"
|
||||
@undelete="onUndelete(item)"
|
||||
@unhide="onUnhide(item)" />
|
||||
</template>
|
||||
</ListingLayout>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</SelectedItems>
|
||||
</ExpandedItems>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import { Toast } from "composables/toast";
|
||||
import { mapActions } from "vuex";
|
||||
import { HistoryItemsProvider } from "components/providers/storeProviders";
|
||||
import { mapActions as vuexMapActions } from "vuex";
|
||||
import { mapActions, mapState, storeToRefs } from "pinia";
|
||||
import { useHistoryItemsStore } from "stores/history/historyItemsStore";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import ContentItem from "components/History/Content/ContentItem";
|
||||
import { deleteContent, updateContentFields } from "components/History/model/queries";
|
||||
import { getHighlights } from "components/History/Content/model/highlights";
|
||||
import ExpandedItems from "components/History/Content/ExpandedItems";
|
||||
import SelectedItems from "components/History/Content/SelectedItems";
|
||||
import ListingLayout from "components/History/Layout/ListingLayout";
|
||||
@@ -178,7 +171,6 @@ export default {
|
||||
HistoryDropZone,
|
||||
HistoryEmpty,
|
||||
HistoryFilters,
|
||||
HistoryItemsProvider,
|
||||
HistoryOperations,
|
||||
HistorySelectionOperations,
|
||||
HistorySelectionStatus,
|
||||
@@ -194,13 +186,15 @@ export default {
|
||||
filter: { type: String, default: "" },
|
||||
writable: { type: Boolean, default: true },
|
||||
showControls: { type: Boolean, default: true },
|
||||
filterable: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
filterText: "",
|
||||
highlights: {},
|
||||
highlightsKey: null,
|
||||
invisible: {},
|
||||
loading: false,
|
||||
offset: 0,
|
||||
showAdvanced: false,
|
||||
showDropZone: false,
|
||||
@@ -210,10 +204,15 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useHistoryItemsStore, ["getHistoryItems"]),
|
||||
/** @returns {String} */
|
||||
historyId() {
|
||||
return this.history.id;
|
||||
},
|
||||
/** @returns {Date} */
|
||||
historyUpdateTime() {
|
||||
return this.history.update_time;
|
||||
},
|
||||
/** @returns {String} */
|
||||
queryKey() {
|
||||
return `${this.historyId}-${this.filterText}`;
|
||||
@@ -230,37 +229,67 @@ export default {
|
||||
isProcessing() {
|
||||
return this.operationRunning >= this.history.update_time;
|
||||
},
|
||||
/** @returns {Array} */
|
||||
itemsLoaded() {
|
||||
return this.getHistoryItems(this.historyId, this.filterText);
|
||||
},
|
||||
/** @returns {Date} */
|
||||
lastChecked() {
|
||||
return this.$store.getters.getLastCheckedTime();
|
||||
const { getLastCheckedTime } = storeToRefs(useHistoryItemsStore());
|
||||
return getLastCheckedTime.value;
|
||||
},
|
||||
/** @returns {Number} */
|
||||
totalItemsInQuery() {
|
||||
const { getTotalMatchesCount } = storeToRefs(useHistoryItemsStore());
|
||||
return getTotalMatchesCount.value;
|
||||
},
|
||||
/** @returns {Boolean} */
|
||||
isWatching() {
|
||||
return this.$store.getters.getWatchingVisibility();
|
||||
const { getWatchingVisibility } = storeToRefs(useHistoryItemsStore());
|
||||
return getWatchingVisibility.value;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
queryKey() {
|
||||
this.invisible = {};
|
||||
this.offset = 0;
|
||||
this.resetHighlights();
|
||||
this.loadHistoryItems();
|
||||
},
|
||||
historyId(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.operationRunning = null;
|
||||
this.resetHighlights();
|
||||
}
|
||||
},
|
||||
filter(newVal) {
|
||||
this.filterText = newVal;
|
||||
},
|
||||
offset() {
|
||||
this.loadHistoryItems();
|
||||
},
|
||||
historyUpdateTime() {
|
||||
this.loadHistoryItems();
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadHistoryItems();
|
||||
},
|
||||
methods: {
|
||||
...mapActions("history", ["loadHistoryById"]),
|
||||
...vuexMapActions("history", ["loadHistoryById"]),
|
||||
...mapActions(useHistoryItemsStore, ["fetchHistoryItems"]),
|
||||
getHighlight(item) {
|
||||
return this.highlights[this.getItemKey(item)];
|
||||
},
|
||||
getItemKey(item) {
|
||||
return `${item.id}-${item.history_content_type}`;
|
||||
if (this.filterText.includes("related:" + item.hid)) {
|
||||
this.highlightsKey = item.hid;
|
||||
return "active";
|
||||
} else if (this.filterText.includes("related:") && this.highlightsKey) {
|
||||
if (item.hid > this.highlightsKey) {
|
||||
return "output";
|
||||
} else {
|
||||
return "input";
|
||||
}
|
||||
} else {
|
||||
this.highlightsKey = null;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
hasMatches(items) {
|
||||
return !!items && items.length > 0;
|
||||
@@ -268,6 +297,18 @@ export default {
|
||||
isDataset(item) {
|
||||
return item.history_content_type == "dataset";
|
||||
},
|
||||
async loadHistoryItems() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.fetchHistoryItems(this.historyId, this.filterText, this.offset);
|
||||
this.error = null;
|
||||
this.loading = false;
|
||||
} catch (error) {
|
||||
console.debug("HistoryPanel - Load error.", error);
|
||||
this.error = error;
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
onDelete(item) {
|
||||
this.setInvisible(item);
|
||||
deleteContent(item);
|
||||
@@ -308,19 +349,13 @@ export default {
|
||||
console.debug("HistoryPanel - Operation error.", error);
|
||||
this.operationError = error;
|
||||
},
|
||||
async toggleHighlights(item) {
|
||||
const key = this.getItemKey(item);
|
||||
if (this.highlightsKey != key) {
|
||||
this.highlightsKey = key;
|
||||
this.highlights = await getHighlights(item, key);
|
||||
toggleHighlights(item) {
|
||||
if (this.filterText == "related:" + item.hid) {
|
||||
this.filterText = "";
|
||||
} else {
|
||||
this.resetHighlights();
|
||||
this.filterText = "related:" + item.hid;
|
||||
}
|
||||
},
|
||||
resetHighlights() {
|
||||
this.highlights = {};
|
||||
this.highlightsKey = null;
|
||||
},
|
||||
onDragEnter(e) {
|
||||
this.dragTarget = e.target;
|
||||
this.showDropZone = true;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { BCard, BFormSelect, BFormCheckbox, BFormGroup, BCollapse, BLink } from "bootstrap-vue";
|
||||
import { ExportParamsModel } from "components/Common/models/exportRecordModel";
|
||||
import { AVAILABLE_EXPORT_FORMATS } from "./services";
|
||||
|
||||
const props = defineProps({
|
||||
exportParams: {
|
||||
type: ExportParamsModel,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["onValueChanged"]);
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const title = computed(() => (isExpanded.value ? `Hide advanced export options` : `Show advanced export options`));
|
||||
const localOptions = reactive({
|
||||
modelStoreFormat: props.exportParams.modelStoreFormat,
|
||||
includeFiles: props.exportParams.includeFiles,
|
||||
includeDeleted: props.exportParams.includeDeleted,
|
||||
includeHidden: props.exportParams.includeHidden,
|
||||
});
|
||||
|
||||
function onValueChanged() {
|
||||
emit("onValueChanged", localOptions);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-link
|
||||
id="toggle-options-link"
|
||||
:class="isExpanded ? null : 'collapsed'"
|
||||
:aria-expanded="isExpanded ? 'true' : 'false'"
|
||||
aria-controls="collapse-options"
|
||||
@click="isExpanded = !isExpanded">
|
||||
{{ title }}
|
||||
</b-link>
|
||||
<b-collapse id="collapse-options" v-model="isExpanded">
|
||||
<b-card>
|
||||
<b-form-group label="Export Format:" label-for="format">
|
||||
<b-form-select
|
||||
id="format-selector"
|
||||
v-model="localOptions.modelStoreFormat"
|
||||
:options="AVAILABLE_EXPORT_FORMATS"
|
||||
value-field="id"
|
||||
text-field="name"
|
||||
@change="onValueChanged" />
|
||||
</b-form-group>
|
||||
|
||||
<b-form-group label="Dataset files included in the package:">
|
||||
<b-form-checkbox
|
||||
id="include-files-check"
|
||||
v-model="localOptions.includeFiles"
|
||||
switch
|
||||
@change="onValueChanged">
|
||||
Include Active
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
id="include-deleted-check"
|
||||
v-model="localOptions.includeDeleted"
|
||||
switch
|
||||
@change="onValueChanged">
|
||||
Include Deleted (not purged)
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox
|
||||
id="include-hidden-check"
|
||||
v-model="localOptions.includeHidden"
|
||||
switch
|
||||
@change="onValueChanged">
|
||||
Include Hidden
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,103 @@
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import {
|
||||
EXPIRED_STS_DOWNLOAD_RECORD,
|
||||
FILE_SOURCE_STORE_RECORD,
|
||||
RECENT_STS_DOWNLOAD_RECORD,
|
||||
} from "@/components/Common/models/testData/exportData";
|
||||
import flushPromises from "flush-promises";
|
||||
import type { components } from "@/schema";
|
||||
import { getLocalVue } from "../../../../tests/jest/helpers";
|
||||
import HistoryExport from "./HistoryExport.vue";
|
||||
import { getExportRecords } from "./services";
|
||||
|
||||
const localVue = getLocalVue(true);
|
||||
|
||||
jest.mock("./services");
|
||||
const mockGetExportRecords = getExportRecords as jest.MockedFunction<typeof getExportRecords>;
|
||||
mockGetExportRecords.mockResolvedValue([]);
|
||||
|
||||
const FAKE_HISTORY_ID = "fake-history-id";
|
||||
const REMOTE_FILES_API_ENDPOINT = new RegExp("/api/remote_files/plugins");
|
||||
|
||||
type FilesSourcePluginList = components["schemas"]["FilesSourcePlugin"][];
|
||||
const REMOTE_FILES_API_RESPONSE: FilesSourcePluginList = [
|
||||
{
|
||||
id: "test-posix-source",
|
||||
type: "posix",
|
||||
uri_root: "gxfiles://test-posix-source",
|
||||
label: "TestSource",
|
||||
doc: "For testing",
|
||||
writable: true,
|
||||
requires_roles: undefined,
|
||||
requires_groups: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
async function mountHistoryExport() {
|
||||
const wrapper = shallowMount(HistoryExport, {
|
||||
propsData: { historyId: FAKE_HISTORY_ID },
|
||||
localVue,
|
||||
});
|
||||
await flushPromises();
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
describe("HistoryExport.vue", () => {
|
||||
let axiosMock: MockAdapter;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
axiosMock.onGet(REMOTE_FILES_API_ENDPOINT).reply(200, []);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
it("should render export options", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#history-export-options").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should display a message indicating there are no exports where there are none", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#no-export-records-alert").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should render previous records when there is more than one record", async () => {
|
||||
mockGetExportRecords.mockResolvedValue([
|
||||
RECENT_STS_DOWNLOAD_RECORD,
|
||||
FILE_SOURCE_STORE_RECORD,
|
||||
EXPIRED_STS_DOWNLOAD_RECORD,
|
||||
]);
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#previous-export-records").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not render previous records when there is one or less records", async () => {
|
||||
mockGetExportRecords.mockResolvedValue([RECENT_STS_DOWNLOAD_RECORD]);
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#previous-export-records").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should display file sources tab if there are available", async () => {
|
||||
axiosMock.onGet(REMOTE_FILES_API_ENDPOINT).reply(200, REMOTE_FILES_API_RESPONSE);
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#direct-download-tab").exists()).toBe(true);
|
||||
expect(wrapper.find("#file-source-tab").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not display file sources tab if there are no file sources available", async () => {
|
||||
const wrapper = await mountHistoryExport();
|
||||
|
||||
expect(wrapper.find("#direct-download-tab").exists()).toBe(true);
|
||||
expect(wrapper.find("#file-source-tab").exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
<script setup>
|
||||
import { computed, ref, reactive, onMounted, watch } from "vue";
|
||||
import { BAlert, BCard, BButton, BTab, BTabs } from "bootstrap-vue";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import ExportRecordDetails from "components/Common/ExportRecordDetails.vue";
|
||||
import ExportRecordTable from "components/Common/ExportRecordTable.vue";
|
||||
import ExportOptions from "./ExportOptions.vue";
|
||||
import ExportToFileSourceForm from "components/Common/ExportForm.vue";
|
||||
import { getExportRecords, exportToFileSource, reimportHistoryFromRecord } from "./services";
|
||||
import { useTaskMonitor } from "composables/taskMonitor";
|
||||
import { useFileSources } from "composables/fileSources";
|
||||
import { useShortTermStorage, DEFAULT_EXPORT_PARAMS } from "composables/shortTermStorage";
|
||||
import { useConfirmDialog } from "composables/confirmDialog";
|
||||
|
||||
const { isRunning: isExportTaskRunning, waitForTask } = useTaskMonitor();
|
||||
const { hasWritable: hasWritableFileSources } = useFileSources();
|
||||
const { isPreparing: isPreparingDownload, downloadHistory, downloadObjectByRequestId } = useShortTermStorage();
|
||||
const { confirm } = useConfirmDialog();
|
||||
|
||||
const props = defineProps({
|
||||
historyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const exportParams = reactive(DEFAULT_EXPORT_PARAMS);
|
||||
const isLoadingRecords = ref(true);
|
||||
const exportRecords = ref(null);
|
||||
const latestExportRecord = computed(() => (exportRecords.value?.length ? exportRecords.value.at(0) : null));
|
||||
const previousExportRecords = computed(() => (exportRecords.value ? exportRecords.value.slice(1) : null));
|
||||
const hasPreviousExports = computed(() => previousExportRecords.value?.length > 0);
|
||||
const availableRecordsMessage = computed(() =>
|
||||
isLoadingRecords.value
|
||||
? "Loading export records..."
|
||||
: "This history has no export records yet. You can choose one of the export options above."
|
||||
);
|
||||
const errorMessage = ref(null);
|
||||
const actionMessage = ref(null);
|
||||
const actionMessageVariant = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
updateExports();
|
||||
});
|
||||
|
||||
watch(isExportTaskRunning, (newValue, oldValue) => {
|
||||
const hasFinished = oldValue && !newValue;
|
||||
if (hasFinished) {
|
||||
updateExports();
|
||||
}
|
||||
});
|
||||
|
||||
async function updateExports() {
|
||||
isLoadingRecords.value = true;
|
||||
try {
|
||||
errorMessage.value = null;
|
||||
exportRecords.value = await getExportRecords(props.historyId);
|
||||
const shouldWaitForTask = latestExportRecord.value?.isPreparing && !isExportTaskRunning.value;
|
||||
if (shouldWaitForTask) {
|
||||
waitForTask(latestExportRecord.value.taskUUID, 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage.value = error;
|
||||
}
|
||||
isLoadingRecords.value = false;
|
||||
}
|
||||
|
||||
async function doExportToFileSource(exportDirectory, fileName) {
|
||||
await exportToFileSource(props.historyId, exportDirectory, fileName, exportParams);
|
||||
updateExports();
|
||||
}
|
||||
|
||||
async function prepareDownload() {
|
||||
const upToDateDownloadRecord = findValidUpToDateDownloadRecord();
|
||||
if (upToDateDownloadRecord) {
|
||||
downloadObjectByRequestId(upToDateDownloadRecord.stsDownloadId);
|
||||
return;
|
||||
}
|
||||
await downloadHistory(props.historyId, { pollDelayInMs: 3000, exportParams: exportParams });
|
||||
updateExports();
|
||||
}
|
||||
|
||||
function downloadFromRecord(record) {
|
||||
if (record.canDownload) {
|
||||
downloadObjectByRequestId(record.stsDownloadId);
|
||||
}
|
||||
}
|
||||
|
||||
function findValidUpToDateDownloadRecord() {
|
||||
return exportRecords.value
|
||||
? exportRecords.value.find(
|
||||
(record) => record.canDownload && record.isUpToDate && record.exportParams?.equals(exportParams)
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
async function reimportFromRecord(record) {
|
||||
const confirmed = await confirm(
|
||||
`Do you really want to import a new copy of this history exported ${record.elapsedTime}?`
|
||||
);
|
||||
if (confirmed) {
|
||||
reimportHistoryFromRecord(record)
|
||||
.then(() => {
|
||||
actionMessageVariant.value = "info";
|
||||
actionMessage.value =
|
||||
"The history is being imported in the background. Check your histories after a while to find it.";
|
||||
})
|
||||
.catch((reason) => {
|
||||
actionMessageVariant.value = "danger";
|
||||
actionMessage.value = reason;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onActionMessageDismissedFromRecord() {
|
||||
actionMessage.value = null;
|
||||
actionMessageVariant.value = null;
|
||||
}
|
||||
|
||||
function updateExportParams(newParams) {
|
||||
exportParams.modelStoreFormat = newParams.modelStoreFormat;
|
||||
exportParams.includeFiles = newParams.includeFiles;
|
||||
exportParams.includeDeleted = newParams.includeDeleted;
|
||||
exportParams.includeHidden = newParams.includeHidden;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<span class="history-export-component">
|
||||
<h1 class="h-lg">Export history {{ props.historyId }}</h1>
|
||||
|
||||
<export-options
|
||||
id="history-export-options"
|
||||
:export-params="exportParams"
|
||||
@onValueChanged="updateExportParams" />
|
||||
|
||||
<b-card no-body class="mt-3">
|
||||
<b-tabs pills card>
|
||||
<b-tab id="direct-download-tab" title="to direct download" title-link-class="tab-export-to-link" active>
|
||||
<p>
|
||||
Here you can generate a temporal download for your history. When your download link expires or
|
||||
your history changes you can re-generate it again.
|
||||
</p>
|
||||
<b-alert show variant="warning">
|
||||
History archive downloads can expire and are removed at regular intervals. For permanent
|
||||
storage, export to a <b>remote file</b> or download and then import the archive on another
|
||||
Galaxy server.
|
||||
</b-alert>
|
||||
<b-button
|
||||
class="direct-download-btn"
|
||||
:disabled="isPreparingDownload"
|
||||
variant="primary"
|
||||
@click="prepareDownload">
|
||||
Download
|
||||
</b-button>
|
||||
<span v-if="isPreparingDownload">
|
||||
<loading-span message="Galaxy is preparing your download, this will likely take a while" />
|
||||
</span>
|
||||
</b-tab>
|
||||
<b-tab
|
||||
v-if="hasWritableFileSources"
|
||||
id="file-source-tab"
|
||||
title="to remote file"
|
||||
title-link-class="tab-export-to-file">
|
||||
<p>
|
||||
If you need a "more permanent" way of storing your history archive you can export it directly to
|
||||
one of the available remote file sources here. You will be able to re-import it later as long as
|
||||
it remains available on the remote server.
|
||||
</p>
|
||||
<export-to-file-source-form
|
||||
what="history"
|
||||
:clear-input-after-export="true"
|
||||
@export="doExportToFileSource" />
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</b-card>
|
||||
|
||||
<export-record-details
|
||||
v-if="latestExportRecord"
|
||||
:record="latestExportRecord"
|
||||
object-type="history"
|
||||
class="mt-3"
|
||||
:action-message="actionMessage"
|
||||
:action-message-variant="actionMessageVariant"
|
||||
@onDownload="downloadFromRecord"
|
||||
@onReimport="reimportFromRecord"
|
||||
@onActionMessageDismissed="onActionMessageDismissedFromRecord" />
|
||||
<b-alert v-else-if="errorMessage" id="last-export-record-error-alert" variant="danger" class="mt-3" show>
|
||||
{{ errorMessage }}
|
||||
</b-alert>
|
||||
<b-alert v-else id="no-export-records-alert" variant="info" class="mt-3" show>
|
||||
{{ availableRecordsMessage }}
|
||||
</b-alert>
|
||||
|
||||
<export-record-table
|
||||
v-if="hasPreviousExports"
|
||||
id="previous-export-records"
|
||||
:records="previousExportRecords"
|
||||
class="mt-3"
|
||||
@onDownload="downloadFromRecord"
|
||||
@onReimport="reimportFromRecord" />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,77 @@
|
||||
import { ExportRecordModel } from "@/components/Common/models/exportRecordModel";
|
||||
import type { ObjectExportTaskResponse } from "@/components/Common/models/exportRecordModel";
|
||||
import { DEFAULT_EXPORT_PARAMS } from "@/composables/shortTermStorage";
|
||||
import type { components } from "@/schema";
|
||||
import { fetcher } from "@/schema";
|
||||
|
||||
type ModelStoreFormat = components["schemas"]["ModelStoreFormat"];
|
||||
|
||||
const _getExportRecords = fetcher.path("/api/histories/{history_id}/exports").method("get").create();
|
||||
const _exportToFileSource = fetcher.path("/api/histories/{history_id}/write_store").method("post").create();
|
||||
const _importFromStoreAsync = fetcher.path("/api/histories/from_store_async").method("post").create();
|
||||
|
||||
/**
|
||||
* A list of objects with the available export formats IDs and display names.
|
||||
*/
|
||||
export const AVAILABLE_EXPORT_FORMATS: { id: ModelStoreFormat; name: string }[] = [
|
||||
{ id: "rocrate.zip", name: "RO-Crate" },
|
||||
{ id: "tar.gz", name: "Compressed TGZ" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets a list of export records for the given history.
|
||||
* @param historyId the encoded ID of the history
|
||||
* @param params query and pagination params
|
||||
* @returns a promise with a list of export records associated with the given history.
|
||||
*/
|
||||
export async function getExportRecords(historyId: string) {
|
||||
const response = await _getExportRecords(
|
||||
{
|
||||
history_id: historyId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.galaxy.task.export+json",
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data.map((item: unknown) => new ExportRecordModel(item as ObjectExportTaskResponse));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param historyId the encoded ID of the history
|
||||
* @param exportDirectory the output directory in the file source
|
||||
* @param fileName the name of the output archive
|
||||
* @param exportParams additional parameters to configure the export
|
||||
* @returns A promise with the request response
|
||||
*/
|
||||
export async function exportToFileSource(
|
||||
historyId: string,
|
||||
exportDirectory: string,
|
||||
fileName: string,
|
||||
exportParams = DEFAULT_EXPORT_PARAMS
|
||||
) {
|
||||
const exportDirectoryUri = `${exportDirectory}/${fileName}.${exportParams.modelStoreFormat}`;
|
||||
|
||||
return _exportToFileSource({
|
||||
history_id: historyId,
|
||||
target_uri: exportDirectoryUri,
|
||||
model_store_format: exportParams.modelStoreFormat as ModelStoreFormat,
|
||||
include_files: exportParams.includeFiles,
|
||||
include_deleted: exportParams.includeDeleted,
|
||||
include_hidden: exportParams.includeHidden,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a new history using the information stored in the given export record.
|
||||
* @param record The export record to be imported
|
||||
* @returns A promise with the request response
|
||||
*/
|
||||
export async function reimportHistoryFromRecord(record: ExportRecordModel) {
|
||||
return _importFromStoreAsync({
|
||||
store_content_uri: record.importUri,
|
||||
model_store_format: record.modelStoreFormat,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import Filtering, { compare, contains, equals, expandNameTag, toBool, toDate } from "utils/filtering";
|
||||
|
||||
export const validFilters = {
|
||||
hid: equals("hid"),
|
||||
state: equals("state"),
|
||||
name: contains("name"),
|
||||
extension: equals("extension"),
|
||||
hid_ge: compare("hid", "ge"),
|
||||
hid_gt: compare("hid", "gt"),
|
||||
hid_le: compare("hid", "le"),
|
||||
hid_lt: compare("hid", "lt"),
|
||||
related: equals("related"),
|
||||
tag: contains("tags", "tag", expandNameTag),
|
||||
visible: equals("visible", "visible", toBool),
|
||||
deleted: equals("deleted", "deleted", toBool),
|
||||
create_time: compare("create_time", "le", toDate),
|
||||
create_time_ge: compare("create_time", "ge", toDate),
|
||||
create_time_gt: compare("create_time", "gt", toDate),
|
||||
create_time_le: compare("create_time", "le", toDate),
|
||||
create_time_lt: compare("create_time", "lt", toDate),
|
||||
update_time: compare("update_time", "le", toDate),
|
||||
update_time_ge: compare("update_time", "ge", toDate),
|
||||
update_time_gt: compare("update_time", "gt", toDate),
|
||||
update_time_le: compare("update_time", "le", toDate),
|
||||
update_time_lt: compare("update_time", "lt", toDate),
|
||||
};
|
||||
|
||||
export const HistoryFilters = new Filtering(validFilters, true);
|
||||
@@ -1,13 +1,20 @@
|
||||
<script setup>
|
||||
import UtcDate from "components/UtcDate";
|
||||
import Tags from "components/Common/Tags";
|
||||
import { safePath } from "utils/redirect";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import Heading from "components/Common/Heading";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import DebouncedInput from "components/DebouncedInput";
|
||||
import { getPublishedHistories, updateTags } from "./services";
|
||||
import { getFilters, getFilterText, toAlias } from "utils/filterConversion";
|
||||
import Filtering, { contains, expandNameTag } from "utils/filtering";
|
||||
|
||||
const validFilters = {
|
||||
name: contains("name"),
|
||||
annotation: contains("annotation"),
|
||||
tag: contains("tags", "tag", expandNameTag),
|
||||
};
|
||||
|
||||
const filters = new Filtering(validFilters, false);
|
||||
|
||||
const limit = ref(50);
|
||||
const offset = ref(0);
|
||||
@@ -43,7 +50,7 @@ const localFilter = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const filterSettings = computed(() => toAlias(getFilters(filterText.value, false)));
|
||||
const filterSettings = computed(() => filters.toAlias(filters.getFilters(filterText.value)));
|
||||
|
||||
const updateFilter = (newVal, append = false) => {
|
||||
let oldValue = filterText.value;
|
||||
@@ -69,13 +76,17 @@ const onTagClick = (tag) => {
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true;
|
||||
getPublishedHistories({
|
||||
limit: limit.value,
|
||||
offset: offset.value,
|
||||
sortBy: sortBy.value,
|
||||
sortDesc: sortDesc.value,
|
||||
filterText: filterText.value,
|
||||
})
|
||||
|
||||
getPublishedHistories(
|
||||
{
|
||||
limit: limit.value,
|
||||
offset: offset.value,
|
||||
sortBy: sortBy.value,
|
||||
sortDesc: sortDesc.value,
|
||||
filterText: filterText.value,
|
||||
},
|
||||
filters
|
||||
)
|
||||
.then((data) => {
|
||||
items.value = data;
|
||||
})
|
||||
@@ -98,7 +109,7 @@ const onToggle = () => {
|
||||
|
||||
const onSearch = () => {
|
||||
onToggle();
|
||||
updateFilter(getFilterText(filterSettings.value));
|
||||
updateFilter(filters.getFilterText(filterSettings.value));
|
||||
};
|
||||
|
||||
load();
|
||||
@@ -204,7 +215,7 @@ watch([filterText, sortBy, sortDesc], () => {
|
||||
:sort-by.sync="sortBy"
|
||||
:sort-desc.sync="sortDesc">
|
||||
<template v-slot:cell(name)="row">
|
||||
<router-link :to="safePath(`/published/history?id=${row.item.id}`)">
|
||||
<router-link :to="`/published/history?id=${row.item.id}`">
|
||||
{{ row.item.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
v-if="!breadcrumbs.length"
|
||||
:list-offset="listOffset"
|
||||
:history="currentHistory"
|
||||
:filterable="true"
|
||||
v-on="handlers"
|
||||
@view-collection="onViewCollection">
|
||||
<template v-slot:navigation>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mockModule, getLocalVue } from "jest/helpers";
|
||||
import { mockModule, getLocalVue } from "tests/jest/helpers";
|
||||
import { userStore } from "store/userStore";
|
||||
import Vuex from "vuex";
|
||||
import DetailsLayout from "./DetailsLayout";
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
import short from "components/directives/v-short";
|
||||
import { StatelessTags } from "components/Tags";
|
||||
import StatelessTags from "components/TagsMultiselect/StatelessTags";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
</template>
|
||||
<script>
|
||||
import VirtualList from "vue-virtual-scroll-list";
|
||||
import { throttle } from "lodash";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
export default {
|
||||
@@ -36,8 +35,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
throttlePeriod: 20,
|
||||
deltaMax: 20,
|
||||
previousStart: undefined,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -45,37 +43,13 @@ export default {
|
||||
this.$refs.listing.scrollToOffset(0);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.onScrollThrottle = throttle((event) => {
|
||||
this.onScroll(event);
|
||||
}, this.throttlePeriod);
|
||||
},
|
||||
methods: {
|
||||
onScrollHandler(event) {
|
||||
/* CURRENTLY UNUSED
|
||||
// this avoids diagonal scrolling, we either scroll left/right or top/down
|
||||
// both events are throttled and the default handler has been prevented.
|
||||
if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
|
||||
// handle vertical scrolling with virtual scroller
|
||||
const listing = this.$refs.listing;
|
||||
const deltaMax = this.deltaMax;
|
||||
const deltaY = Math.max(Math.min(event.deltaY, deltaMax), -deltaMax);
|
||||
this.offset = Math.max(0, listing.getOffset() + deltaY);
|
||||
this.$refs.listing.scrollToOffset(this.offset);
|
||||
} else {
|
||||
// dispatch horizontal scrolling as regular event
|
||||
var wheelEvent = new WheelEvent("wheel", {
|
||||
deltaX: event.deltaX,
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
});
|
||||
event.target.dispatchEvent(wheelEvent);
|
||||
}
|
||||
*/
|
||||
},
|
||||
onScroll() {
|
||||
const rangeStart = this.$refs.listing.range.start;
|
||||
this.$emit("scroll", rangeStart);
|
||||
if (this.previousStart !== rangeStart) {
|
||||
this.previousStart = rangeStart;
|
||||
this.$emit("scroll", rangeStart);
|
||||
}
|
||||
},
|
||||
getOffset() {
|
||||
return this.$refs.listing?.getOffset() || 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import flushPromises from "flush-promises";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import SelectorModal from "./SelectorModal";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { createPinia } from "pinia";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import flushPromises from "flush-promises";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import { getLocalVue, mockModule } from "tests/jest/helpers";
|
||||
import MultipleView from "./MultipleView";
|
||||
import MockCurrentUser from "components/providers/MockCurrentUser";
|
||||
import MockUserHistories from "components/providers/MockUserHistories";
|
||||
import store from "store/index";
|
||||
import Vuex from "vuex";
|
||||
import { userStore } from "store/userStore";
|
||||
import { historyStore } from "store/historyStore";
|
||||
|
||||
const COUNT = 8;
|
||||
const USER_ID = "test-user-id";
|
||||
@@ -23,8 +25,8 @@ const getFakeHistorySummaries = (num, selectedIndex = 0) => {
|
||||
result[selectedIndex].id = CURRENT_HISTORY_ID;
|
||||
return result;
|
||||
};
|
||||
|
||||
const CurrentUserMock = MockCurrentUser({ id: USER_ID });
|
||||
const currentUser = { id: USER_ID };
|
||||
const CurrentUserMock = MockCurrentUser(currentUser);
|
||||
const UserHistoriesMock = MockUserHistories({ id: CURRENT_HISTORY_ID }, getFakeHistorySummaries(COUNT, 0), false);
|
||||
|
||||
const localVue = getLocalVue();
|
||||
@@ -33,6 +35,22 @@ describe("MultipleView", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
user: mockModule(userStore, { currentUser }),
|
||||
history: mockModule(historyStore, {
|
||||
currentHistoryId: CURRENT_HISTORY_ID,
|
||||
currentHistory: { id: CURRENT_HISTORY_ID },
|
||||
histories: getFakeHistorySummaries(COUNT, 0).reduce((acc, curr, index) => {
|
||||
acc[curr.id] = curr;
|
||||
return acc;
|
||||
}, {}),
|
||||
historiesLoading: false,
|
||||
handlers: {},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = mount(MultipleView, {
|
||||
store,
|
||||
pinia,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user