Files
galaxy/client/galaxy/scripts/components/RuleCollectionBuilder.vue
T

1901 lines
80 KiB
Vue

<template>
<state-div v-if="state == 'build'">
<!-- Different instructions if building up from individual datasets vs.
initial data import. -->
<rule-modal-header v-if="ruleView == 'source'">
Below is a raw JSON description of the rules to apply to the tabular data. This is an advanced setting.
</rule-modal-header>
<rule-modal-header v-else-if="elementsType == 'datasets' || elementsType == 'library_datasets'">
Use this form to describe rules for building collection(s) from the specified datasets. <b>Be sure to specify at least one column as a list identifier</b> - specify more to created nested list structures. Specify a column to serve as "collection name" to group datasets into multiple collections.
</rule-modal-header>
<!-- This modality allows importing individual datasets, multiple collections,
and requires a data source - note that. -->
<rule-modal-header v-else-if="importType == 'datasets'">
Use this form to describe rules for import datasets. At least one column should be defined to a source to fetch data from (URLs, FTP files, etc...).
</rule-modal-header>
<rule-modal-header v-else>
Use this form to describe rules for import datasets. At least one column should be defined to a source to fetch data from (URLs, FTP files, etc...). <b>Be sure to specify at least one column as a list identifier</b> - specify more to created nested list structures. Specify a column to serve as "collection name" to group datasets into multiple collections.
</rule-modal-header>
<rule-modal-middle v-if="ruleView == 'source'">
<p class="errormessagelarge" v-if="ruleSourceError">{{ ruleSourceError }}</p>
<textarea class="rule-source" v-model="ruleSource"></textarea>
</rule-modal-middle>
<rule-modal-middle v-else>
<!-- column-headers -->
<div class="rule-builder-body vertically-spaced"
v-bind:class="{ 'flex-column-container': vertical }" v-if="ruleView == 'normal'">
<!-- width: 30%; -->
<div class="rule-column" v-bind:class="orientation">
<div class="rules-container" v-bind:class="{'rules-container-vertical': initialElements && vertical, 'rules-container-horizontal': initialElements && horizontal, 'rules-container-full': initialElements == null}">
<rule-component rule-type="sort"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="addSortingTarget" :col-headers="activeRuleColHeaders" />
<label v-b-tooltip.hover :title="titleNumericSort">
<input type="checkbox" v-model="addSortingNumeric" />
{{ l("Numeric sorting.") }}
</label>
</rule-component>
<rule-component rule-type="add_column_basename"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="addColumnBasenameTarget" :col-headers="activeRuleColHeaders" />
</rule-component>
<rule-component rule-type="add_column_rownum"
:display-rule-type="displayRuleType"
:builder="this">
<label>
{{ l("Starting from") }}
<input type="number" v-model="addColumnRownumStart" min="0" />
</label>
</rule-component>
<rule-component rule-type="add_column_metadata"
:display-rule-type="displayRuleType"
:builder="this">
<label>
{{ l("For") }}
<select v-model="addColumnMetadataValue">
<option v-for="(col, index) in metadataOptions" :value="index">{{ col }}</option>
</select>
</label>
</rule-component>
<rule-component rule-type="add_column_regex"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="addColumnRegexTarget" :col-headers="activeRuleColHeaders" />
<input type="radio" v-model="addColumnRegexType" value="global">Create column matching expression.<br />
<input type="radio" v-model="addColumnRegexType" value="groups">Create columns matching expression groups.<br />
<input type="radio" v-model="addColumnRegexType" value="replacement">Create column from expression replacement.<br />
<regular-expression-input :target.sync="addColumnRegexExpression" />
<label v-if="addColumnRegexType=='groups'">
{{ l("Number of Groups") }}
<input type="number" v-model="addColumnRegexGroupCount" min="1" />
</label>
<label v-if="addColumnRegexType=='replacement'">
{{ l("Replacement Expression") }}
<input type="text" v-model="addColumnRegexReplacement" class="rule-replacement" />
</label>
</rule-component>
<rule-component rule-type="add_column_concatenate"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="addColumnConcatenateTarget0" :col-headers="activeRuleColHeaders" />
<column-selector :target.sync="addColumnConcatenateTarget1" :col-headers="activeRuleColHeaders" />
</rule-component>
<rule-component rule-type="add_column_substr"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="addColumnSubstrTarget" :col-headers="activeRuleColHeaders" />
<label>
<select v-model="addColumnSubstrType">
<option value="keep_prefix">Keep only prefix specified.</option>
<option value="drop_prefix">Strip off prefix specified.</option>
<option value="keep_suffix">Keep only suffix specified.</option>
<option value="drop_suffix">Strip off suffix specified.</option>
</select>
</label>
<label>
{{ l("Prefix or suffix length") }}
<input type="number" v-model="addColumnSubstrLength" min="0" />
</label>
</rule-component>
<rule-component rule-type="add_column_value"
:display-rule-type="displayRuleType"
:builder="this">
<label>
{{ l("Value") }}
<input type="text" v-model="addColumnValue" />
</label>
</rule-component>
<rule-component rule-type="remove_columns"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="removeColumnTargets" :col-headers="activeRuleColHeaders" :multiple="true" />
</rule-component>
<rule-component rule-type="split_columns"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="splitColumnsTargets0" label="Odd Row Column(s)" :col-headers="activeRuleColHeaders" :multiple="true" />
<column-selector :target.sync="splitColumnsTargets1" label="Even Row Column(s)" :col-headers="activeRuleColHeaders" :multiple="true" />
</rule-component>
<rule-component rule-type="swap_columns"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="swapColumnsTarget0" label="Swap Column" :col-headers="activeRuleColHeaders" />
<column-selector :target.sync="swapColumnsTarget1" label="With Column" :col-headers="activeRuleColHeaders" />
</rule-component>
<rule-component rule-type="add_filter_regex"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="addFilterRegexTarget" :col-headers="activeRuleColHeaders" />
<regular-expression-input :target.sync="addFilterRegexExpression" />
<label v-b-tooltip.hover :title="titleInvertFilterRegex">
<input type="checkbox" v-model="addFilterRegexInvert" />
{{ l("Invert filter.") }}
</label>
</rule-component>
<rule-component rule-type="add_filter_matches"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="addFilterMatchesTarget" :col-headers="activeRuleColHeaders" />
<input type="text" v-model="addFilterMatchesValue" />
<label v-b-tooltip.hover :title="titleInvertFilterMatches">
<input type="checkbox" v-model="addFilterMatchesInvert" />
{{ l("Invert filter.") }}
</label>
</rule-component>
<rule-component rule-type="add_filter_compare"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="addFilterCompareTarget" :col-headers="activeRuleColHeaders" />
<label>
Filter out rows
<select v-model="addFilterCompareType">
<option value="less_than">{{ l("less than") }} </option>
<option value="less_than_equal">{{ l("less than or equal to") }}</option>
<option value="greater_than">{{ l("greater than") }}</option>
<option value="greater_than_equal">{{ l("greater than or equal to") }}</option>
</select>
</label>
<input type="text" v-model="addFilterCompareValue" />
</rule-component>
<rule-component rule-type="add_filter_count"
:display-rule-type="displayRuleType"
:builder="this">
<label>
Filter which rows?
<select v-model="addFilterCountWhich">
<option value="first">first</option>
<option value="last">last</option>
</select>
</label>
<label>
Filter how many rows?
<input type="number" v-model="addFilterCountN" />
</label>
<label v-b-tooltip.hover :title="titleInvertFilterMatches">
<input type="checkbox" v-model="addFilterCountInvert" />
{{ l("Invert filter.") }}
</label>
</rule-component>
<rule-component rule-type="add_filter_empty"
:display-rule-type="displayRuleType"
:builder="this">
<column-selector :target.sync="addFilterEmptyTarget" :col-headers="activeRuleColHeaders" />
<label v-b-tooltip.hover :title="titleInvertFilterEmpty">
<input type="checkbox" v-model="addFilterEmptyInvert" />
{{ l("Invert filter.") }}
</label>
</rule-component>
<div v-if="displayRuleType == 'mapping'">
<div class="map"
v-for="(map, index) in mapping"
v-bind:index="index"
v-bind:key="map.type">
<column-selector
:class="'rule-map-' + map.type.replace(/_/g, '-')"
:label="mappingTargets()[map.type].label"
:help="mappingTargets()[map.type].help"
:target.sync="map.columns"
:ordered-edit.sync="map.editing"
:col-headers="colHeaders"
:multiple="mappingTargets()[map.type].multiple"
:ordered="true"
:value-as-list="true">
<span v-b-tooltip.hover :title="titleRemoveMapping" class="fa fa-times" @click="removeMapping(index)"></span>
</column-selector>
</div>
<div class="buttons rule-edit-buttons d-flex justify-content-end">
<button v-b-tooltip.hover :title="titleAddColumnDefinition" type="button" class="dropdown-toggle btn btn-primary mr-1" data-toggle="dropdown" v-if="unmappedTargets.length > 0">
<span class="fa fa-plus rule-add-mapping"></span> {{ "Add Definition" }}<span class="caret"></span>
</button>
<div class="dropdown-menu" role="menu">
<a v-for="target in unmappedTargets"
v-bind:index="target"
v-bind:key="target"
class="dropdown-item"
href="javascript:void(0)"
:class="'rule-add-mapping-' + target.replace(/_/g, '-')"
@click="addIdentifier(target)">
{{ mappingTargets()[target].label }}
</a>
</div>
<b-button v-b-tooltip.hover.bottom :title="titleApplyColumnDefinitions" class="rule-mapping-ok" v-if="!hasActiveMappingEdit" @click="displayRuleType = null">{{ l("Apply") }}</b-button>
</div>
</div>
<div class="rule-summary" v-if="displayRuleType == null">
<span class="title">
{{ l("Rules") }}
<span v-b-tooltip.hover class="fa fa-wrench rule-builder-view-source" :title="titleViewSource" @click="viewSource"></span>
</span>
<div v-if="jaggedData" class="rule-warning">
Rows contain differing numbers of columns, there was likely a problem parsing your data.
</div>
<ol class="rules">
<!-- Example at the end of https://vuejs.org/v2/guide/list.html -->
<rule-display
v-for="(rule, index) in rules"
v-bind:rule="rule"
v-bind:index="index"
v-bind:key="index"
@edit="editRule(rule, index)"
@remove="removeRule(index)"
:col-headers="colHeadersPerRule[index]" />
<identifier-display v-for="(map, index) in mapping"
v-bind="map"
v-bind:index="index"
v-bind:key="map.type"
@remove="removeMapping(index)"
@edit="displayRuleType = 'mapping'"
v-on:mouseover.native="map.columns.forEach((col) => highlightColumn(col))"
v-on:mouseout.native="map.columns.forEach((col) => unhighlightColumn(col))"
:col-headers="colHeaders" />
<div v-if="mapping.length == 0">
One or more column definitions must be specified. These are required to specify how to build collections and datasets from rows and columns of the table. <a href="javascript:void(0)" @click="displayRuleType = 'mapping'">Click here</a> to manage column definitions.
</div>
</ol>
<div class="rules-buttons">
<div class="btn-group dropup">
<button type="button" v-b-tooltip.hover.bottom :title="titleRulesMenu" class="rule-menu-rules-button primary-button dropdown-toggle" data-toggle="dropdown">
<span class="fa fa-plus"></span> {{ l("Rules") }}<span class="caret"></span>
</button>
<div class="dropdown-menu" role="menu">
<rule-target-component :builder="this" rule-type="sort" />
<rule-target-component :builder="this" rule-type="remove_columns" />
<rule-target-component :builder="this" rule-type="split_columns" />
<rule-target-component :builder="this" rule-type="swap_columns" />
<a href="javascript:void(0)" class="dropdown-item rule-link rule-link-mapping" @click="displayRuleType = 'mapping'">Add / Modify Column Definitions</a>
</div>
</div>
<div class="btn-group dropup">
<button type="button" v-b-tooltip.hover.bottom :title="titleFilterMenu" class="rule-menu-filter-button primary-button dropdown-toggle" data-toggle="dropdown">
<span class="fa fa-plus"></span> {{ l("Filter") }}<span class="caret"></span>
</button>
<div class="dropdown-menu" role="menu">
<rule-target-component :builder="this" rule-type="add_filter_regex" />
<rule-target-component :builder="this" rule-type="add_filter_matches" />
<rule-target-component :builder="this" rule-type="add_filter_compare" />
<rule-target-component :builder="this" rule-type="add_filter_empty" />
<rule-target-component :builder="this" rule-type="add_filter_count" />
</div>
</div>
<div class="btn-group dropup">
<button type="button" v-b-tooltip.hover.bottom :title="titleColumMenu" class="rule-menu-column-button primary-button dropdown-toggle" data-toggle="dropdown">
<span class="fa fa-plus"></span> {{ l("Column") }}<span class="caret"></span>
</button>
<div class="dropdown-menu" role="menu">
<rule-target-component :builder="this" rule-type="add_column_basename" />
<rule-target-component :builder="this" rule-type="add_column_metadata" v-if="elementsType == 'collection_contents'"/>
<rule-target-component :builder="this" rule-type="add_column_regex" />
<rule-target-component :builder="this" rule-type="add_column_concatenate" />
<rule-target-component :builder="this" rule-type="add_column_rownum" />
<rule-target-component :builder="this" rule-type="add_column_value" />
<rule-target-component :builder="this" rule-type="add_column_substr" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- flex-column column -->
<!-- style="width: 70%;" -->
<div class="table-column" v-bind:class="orientation" style="width: 100%;" v-if="initialElements !== null">
<hot-table id="hot-table"
ref="hotTable"
:data="hotData['data']"
:colHeaders="colHeadersDisplay"
:readOnly="true"
stretchH="all">
</hot-table>
</div>
</div>
</rule-modal-middle>
<rule-modal-footer v-if="ruleView == 'source'">
<b-button v-b-tooltip.hover :title="titleSourceCancel" @click="cancelSourceEdit" class="rule-btn-cancel">
{{ l("Cancel") }}
</b-button>
<b-button v-b-tooltip.hover :title="titleSourceReset" class="creator-reset-btn rule-btn-reset">
{{ l("Reset") }}
</b-button>
<b-button v-b-tooltip.hover :title="titleSourceApply" @click="attemptRulePreview" class="rule-btn-okay">
{{ l("Apply")}}
</b-button>
</rule-modal-footer>
<rule-modal-footer v-else-if="ruleView == 'normal'">
<div class="rule-footer-inputs" slot="inputs">
<label v-if="elementsType == 'datasets'">
{{ l("Hide original elements") }}:
</label>
<input type="checkbox" v-model="hideSourceItems" v-if="elementsType == 'datasets'"/>
<div class="rule-footer-extension-group" v-if="extension && showFileTypeSelector">
<label>
{{ l("Type") }}:
</label>
<select2 name="extension" class="extension-select" v-model="extension" >
<option v-for="(col, index) in extensions" :value="col['id']"">{{ col["text"] }}</option>
</select2>
</div>
<div class="rule-footer-genome-group" v-if="genome && showGenomeSelector">
<label>
{{ l("Genome") }}:
</label>
<select2 class="genome-select" v-model="genome">
<option v-for="(col, index) in genomes" :value="col['id']"">{{ col["text"] }}</option>
</select2>
</div>
<div class="rule-footer-name-group" v-if="showCollectionNameInput">
<b-input class="collection-name"
:placeholder="namePlaceholder" :title="namePlaceholder" v-b-tooltip.hover v-model="collectionName" />
<label>
{{ l("Name") }}:
</label>
</div>
</div>
<b-button v-b-tooltip.hover :help="titleCancel" @click="cancel" class="creator-cancel-btn rule-btn-cancel" tabindex="-1">
{{ l("Cancel") }}
</b-button>
<b-button v-b-tooltip.hover @click="resetRulesAndState" :title="titleReset" class="creator-reset-btn rule-btn-reset">
{{ l("Reset") }}
</b-button>
<b-button v-b-tooltip.hover @click="createCollection" :title="titleFinish" class="create-collection rule-btn-okay" variant="primary" :disabled="!validInput">
{{ finishButtonTitle }}
</b-button>
</rule-modal-footer>
</state-div>
<state-div v-else-if="state == 'wait'">
<rule-modal-header>
{{ l("Galaxy is waiting for collection creation, this dialog will close when this is complete.") }}
</rule-modal-header>
<rule-modal-footer>
<b-button @click="cancel" class="creator-cancel-btn" tabindex="-1">
{{ l("Close") }}
</b-button>
</rule-modal-footer>
</state-div>
<state-div v-else-if="state == 'error'">
<!-- TODO: steal styling from paired collection builder warning... -->
<rule-modal-header>
A problem was encountered.
</rule-modal-header>
<rule-modal-middle>
<p class="errormessagelarge">{{ errorMessage }}</p>
</rule-modal-middle>
<rule-modal-footer>
<b-button v-b-tooltip.hover :title="titleCancel" @click="cancel" class="creator-cancel-btn" tabindex="-1">
{{ l("Close") }}
</b-button>
<b-button v-b-tooltip.hover :title="titleErrorOkay" @click="state = 'build'" tabindex="-1">
{{ l("Okay") }}
</b-button>
</rule-modal-footer>
</state-div>
</template>
<script>
import AjaxQueue from "utils/ajax-queue";
import axios from "axios";
import _l from "utils/localization";
import HotTable from "@handsontable/vue";
import Popover from "mvc/ui/ui-popover";
import UploadUtils from "mvc/upload/upload-utils";
import JobStatesModel from "mvc/history/job-states-model";
import RuleDefs from "mvc/rules/rule-definitions";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
Vue.use(BootstrapVue);
const RULES = RuleDefs.RULES;
const MAPPING_TARGETS = RuleDefs.MAPPING_TARGETS;
// Local components...
// Based on https://vuejs.org/v2/examples/select2.html but adapted to handle list values
// with "multiple: true" set.
const Select2 = {
props: ["options", "value", "placeholder"],
template: `<select>
<slot></slot>
</select>`,
mounted: function() {
var vm = this;
$(this.$el)
// init select2
.select2({ data: this.options, placeholder: this.placeholder, allowClear: this.placeholder })
.val(this.value)
.trigger("change")
// emit event on change.
.on("change", function(event) {
vm.$emit("input", event.val);
});
},
watch: {
value: function(value) {
// update value
$(this.$el).val(value);
},
options: function(options) {
// update options
$(this.$el)
.empty()
.select2({ data: options });
}
},
destroyed: function() {
$(this.$el)
.off()
.select2("destroy");
}
};
const ColumnSelector = {
template: `
<div class="rule-column-selector" v-if="!multiple || !ordered">
<label class="d-flex justify-content-end align-items-center">
<span class="mr-auto" v-b-tooltip.hover :title="help">{{ label }}</span>
<div class="mr-1" v-b-tooltip.hover :title="title"><select2 :value="target" @input="handleInput" :multiple="multiple">
<option v-for="(col, index) in colHeaders" :value="index">{{ col }}</option>
</select2></div>
<slot></slot>
</label>
</div>
<div class="rule-column-selector" v-else>
<span>{{ label }}</span>
<slot></slot>
<ol>
<li v-for="(targetEl, index) in target"
v-bind:index="index"
v-bind:key="targetEl"
class="rule-column-selector-target">
{{ colHeaders[targetEl] }}
<span class="fa fa-times rule-column-selector-target-remove" @click="handleRemove(index)"></span>
<span class="fa fa-arrow-up rule-column-selector-up" v-if="index !== 0" @click="moveUp(index)"></span>
<span class="fa fa-arrow-down rule-column-selector-down" v-if="index < target.length - 1" @click="moveUp(index + 1)"></span>
</li>
<li v-if="this.target.length < this.colHeaders.length">
<span class="rule-column-selector-target-add" v-if="!orderedEdit">
<i @click="$emit('update:orderedEdit', true)">... {{ l("Assign Another Column") }}</i>
</span>
<span class="rule-column-selector-target-select" v-else>
<select2 @input="handleAdd" placeholder="Select a column">
<option /><!-- empty option selection for placeholder -->
<option v-for="(col, index) in remainingHeaders" :value="index">{{ col }}</option>
</select2>
</span>
</li>
</ol>
</div>
`,
data: function() {
return {
l: _l
};
},
props: {
target: {
required: true
},
label: {
required: false,
type: String,
default: _l("From Column")
},
help: {
required: false
},
colHeaders: {
type: Array,
required: true
},
multiple: {
type: Boolean,
required: false,
default: false
},
ordered: {
type: Boolean,
required: false,
default: false
},
valueAsList: {
type: Boolean,
required: false,
default: false
},
orderedEdit: {
type: Boolean,
required: false,
default: false
}
},
computed: {
remainingHeaders() {
const colHeaders = this.colHeaders;
if (!this.multiple) {
return colHeaders;
}
const remaining = {};
for (let key in colHeaders) {
if (this.target.indexOf(parseInt(key)) === -1) {
remaining[key] = colHeaders[key];
}
}
return remaining;
},
title() {
return _l("Select a column");
}
},
methods: {
handleInput(value) {
if (this.multiple) {
// https://stackoverflow.com/questions/262427/why-does-parseint-yield-nan-with-arraymap
let val = value.map(idx => parseInt(idx));
this.$emit("update:target", val);
} else {
let val = parseInt(value);
if (this.valueAsList) {
val = [val];
}
this.$emit("update:target", val);
}
},
handleAdd(value) {
this.target.push(parseInt(value));
this.$emit("update:orderedEdit", false);
},
handleRemove(index) {
this.target.splice(index, 1);
},
moveUp(value) {
const swapVal = this.target[value - 1];
Vue.set(this.target, value - 1, this.target[value]);
Vue.set(this.target, value, swapVal);
}
},
components: {
Select2
}
};
const RegularExpressionInput = {
template: `
<div>
<label for="regular_expression" v-b-tooltip.hover :title="title">{{ label }}</label>
<span v-b-popover.html="popoverContent" :title="popoverTitle" class="fa fa-question"></span>
<input v-b-tooltip.hover.left :title="title" name="regular_expression" class="rule-regular-expression" type="text" :value="target" @input="$emit('update:target', $event.target.value)" />
</div>
`,
props: {
target: {
required: true
}
},
computed: {
label() {
return _l("Regular Expression");
},
title() {
return _l("Enter a regular expression.");
},
popoverTitle() {
return _l("Regular Expressions");
},
popoverContent() {
return _l(
`Regular expressions are patterns used to match character combinations in strings. This input accepts Python-style regular expressions, find more information about these in <a href="https://pythonforbiologists.com/regular-expressions/">this Python for Biologists tutorial</a>.`
);
}
}
};
const RuleDisplay = {
template: `
<li class="rule">
<span class="rule-display">
<span class="mr-1">{{ title }}</span>
<span v-b-tooltip.hover :title="editTitle" class="fa fa-edit mr-1" @click="edit"></span>
<span v-b-tooltip.hover :title="removeTitle" class="fa fa-times map" @click="remove"></span>
</span>
<span class="rule-warning" v-if="rule.warn">
{{ rule.warn }}
</span>
<span class="rule-error" v-if="rule.error">
<span class="alert-message">{{ rule.error }}</span>
</span>
</li>
`,
props: {
rule: {
required: true,
type: Object
},
colHeaders: {
type: Array,
required: true
}
},
computed: {
title() {
const ruleType = this.rule.type;
return RULES[ruleType].display(this.rule, this.colHeaders);
},
editTitle() {
return _l("Edit this rule.");
},
removeTitle() {
return _l("Remove this rule.");
}
},
methods: {
edit() {
this.$emit("edit");
},
remove() {
this.$emit("remove");
}
}
};
const IdentifierDisplay = {
template: `
<li class="rule">
<span v-b-tooltip.hover :title="help">Set {{ columnsLabel }} as {{ typeDisplay }}</span>
<span v-b-tooltip.hover :title="titleEdit" class="fa fa-edit" @click="edit"></span>
<span v-b-tooltip.hover :title="titleRemove" class="fa fa-times" @click="remove"></span>
</li>
`,
props: {
type: {
type: String,
required: true
},
columns: {
required: true
},
colHeaders: {
type: Array,
required: true
}
},
methods: {
remove() {
this.$emit("remove");
},
edit() {
this.$emit("edit");
}
},
computed: {
typeDisplay() {
return MAPPING_TARGETS[this.type].label;
},
help() {
return MAPPING_TARGETS[this.type].help || "";
},
titleEdit() {
return _l("Edit column definition");
},
titleRemove() {
return _l("Remove this column definition");
},
columnsLabel() {
return RuleDefs.columnDisplay(this.columns, this.colHeaders);
}
}
};
const RuleTargetComponent = {
template: `<a class="rule-link dropdown-item" href="javascript:void(0)" :class="linkClassName" @click="builder.addNewRule(ruleType)">{{title}}</a>`,
props: {
ruleType: {
type: String,
required: true
},
builder: {
required: true
}
},
computed: {
linkClassName() {
return "rule-link-" + this.ruleType.replace(/_/g, "-");
},
title() {
return RULES[this.ruleType].title;
}
}
};
const RuleComponent = {
template: `
<div v-if="ruleType == displayRuleType" class="rule-editor" :class="typeToClass">
<slot></slot>
<div class="buttons rule-edit-buttons d-flex justify-content-end">
<button type="button" class="btn rule-editor-cancel mr-1" @click="cancel">{{ cancelLabel }}</button>
<button type="button" class="btn btn-primary rule-editor-ok" @click="okay">{{ applyLabel }}</button>
</div>
</div>`,
data: function() {
return {
applyLabel: _l("Apply"),
cancelLabel: _l("Cancel")
};
},
props: {
ruleType: {
type: String,
required: true
},
displayRuleType: {
required: true
},
builder: {
required: true
}
},
methods: {
cancel() {
this.builder.displayRuleType = null;
},
okay() {
this.builder.handleRuleSave(this.ruleType);
this.cancel();
}
},
computed: {
typeToClass() {
return "rule-edit-" + this.ruleType.replace(/_/g, "-");
}
}
};
const StateDiv = {
template: `
<div class="rule-collection-creator collection-creator flex-row-container">
<slot></slot>
</div>`
};
const RuleModalHeader = {
template: `<div class="header flex-row no-flex"><slot></slot></div>`
};
const RuleModalMiddle = {
template: `<div class="middle flex-row flex-row-container"><slot></slot></div>`
};
const RuleModalFooter = {
template: `
<div class="rule-footer footer flex-row no-flex">
<slot name="inputs"></slot>
<div class="actions clear vertically-spaced">
<div class="main-options float-right">
<slot></slot>
</div>
</div>
</div>`
};
export default {
data: function() {
let orientation = "vertical";
let mapping, rules;
if (this.initialRules) {
mapping = this.initialRules.mapping.slice();
rules = this.initialRules.rules.slice();
} else {
if (this.elementsType == "ftp") {
mapping = [{ type: "ftp_path", columns: [0] }];
} else if (this.elementsType == "datasets") {
mapping = [{ type: "list_identifiers", columns: [1] }];
} else {
mapping = [];
}
rules = [];
if (this.elementsType == "collection_contents") {
if (this.initialElements !== null) {
const collectionType = this.initialElements.collection_type;
const collectionTypeRanks = collectionType.split(":");
for (let index in collectionTypeRanks) {
rules.push({
type: "add_column_metadata",
value: "identifier" + index
});
}
} else {
orientation = "horizontal";
// just assume a list is given by default.
rules.push({
type: "add_column_metadata",
value: "identifier0"
});
}
}
}
return {
rules: rules,
colHeadersPerRule: [],
mapping: mapping,
state: "build", // 'build', 'error', 'wait',
ruleView: "normal", // 'normal' or 'source'
ruleSource: "",
ruleSourceJson: null,
ruleSourceError: null,
errorMessage: "",
jaggedData: false,
waitingJobState: "new",
titleReset: _l("Undo all reordering and discards"),
titleNumericSort: _l(
"By default columns will be sorted lexiographically, check this option if the columns are numeric values and should be sorted as numbers"
),
titleInvertFilterRegex: _l("Remove rows not matching the specified regular expression at specified column"),
titleInvertFilterEmpty: _l("Remove rows that have non-empty values at specified column"),
titleInvertFilterMatches: _l("Remove rows not matching supplied value"),
titleViewSource: _l(
"Advanced Option: View and or edit the JSON representation of the rules to apply to this tabular data"
),
titleSourceCancel: _l("Stop editing rules and dismiss changes"),
titleSourceReset: _l("Reset text area to current set of rules"),
titleSourceApply: _l("Apply changes to rule source and return to rule preview"),
titleRulesMenu: _l("General rules to apply"),
titleFilterMenu: _l("Rules that filter rows from the data"),
titleColumMenu: _l("Rules that generate new columns"),
titleRemoveMapping: _l("Remove column definition assignment"),
titleApplyColumnDefinitions: _l("Apply these column definitions and return to rules preview"),
titleAddColumnDefinition: _l("Assign a new piece of metadata as being derived from a column of the table"),
titleErrorOkay: _l("Dismiss this error and return to the rule builder to try again with new rules"),
namePlaceholder: _l("Enter a name for your new collection"),
activeRuleIndex: null,
addColumnRegexTarget: 0,
addColumnBasenameTarget: 0,
addColumnRegexExpression: "",
addColumnRegexReplacement: null,
addColumnRegexGroupCount: null,
addColumnRegexType: "global",
addColumnMetadataValue: 0,
addColumnConcatenateTarget0: 0,
addColumnConcatenateTarget1: 0,
addColumnRownumStart: 1,
addColumnSubstrTarget: 0,
addColumnSubstrType: "keep_prefix",
addColumnSubstrLength: 1,
addColumnValue: "",
removeColumnTargets: [],
addFilterRegexTarget: 0,
addFilterRegexExpression: "",
addFilterRegexInvert: false,
addFilterMatchesTarget: 0,
addFilterMatchesValue: "",
addFilterMatchesInvert: false,
addFilterEmptyTarget: 0,
addFilterEmptyInvert: false,
addFilterCompareTarget: 0,
addFilterCompareValue: 0,
addFilterCompareType: "less_than",
addFilterCountN: 1,
addFilterCountInvert: false,
addFilterCountWhich: "first",
addSortingTarget: 0,
addSortingNumeric: false,
splitColumnsTargets0: [],
splitColumnsTargets1: [],
swapColumnsTarget0: 0,
swapColumnsTarget1: 0,
collectionName: "",
displayRuleType: null,
extensions: [],
extension: null,
genomes: [],
genome: null,
hideSourceItems: this.defaultHideSourceItems,
orientation: orientation
};
},
props: {
initialElements: {
required: true
},
importType: {
type: String,
required: false,
default: "collections"
},
elementsType: {
type: String,
required: false,
default: "datasets"
},
// required if elementsType is "datasets" - hook into Backbone code for creating
// collections from HDAs, etc...
creationFn: {
required: false,
type: Function
},
// required if elementsType is "collection_contents" - hook into tool form to update
// rule parameter
saveRulesFn: {
required: false,
type: Function
},
initialRules: {
required: false,
type: Object
},
defaultHideSourceItems: {
type: Boolean,
required: false,
default: true
},
// Callbacks sent in by modal code.
oncancel: {
required: true,
type: Function
},
oncreate: {
required: true,
type: Function
},
ftpUploadSite: {
type: String,
required: false,
default: null
}
},
computed: {
exisistingDatasets() {
const elementsType = this.elementsType;
return (
elementsType === "datasets" ||
elementsType === "collection_contents" ||
elementsType === "library_datasets"
);
},
showFileTypeSelector() {
return !this.exisistingDatasets && !this.mappingAsDict.file_type;
},
showGenomeSelector() {
return !this.exisistingDatasets && !this.mappingAsDict.dbkey;
},
showCollectionNameInput() {
return (
this.importType == "collections" &&
this.elementsType != "collection_contents" &&
!this.mappingAsDict.collection_name
);
},
titleFinish() {
if (this.elementsType == "datasets" || this.elementsType == "library_datasets") {
return _l("Create new collection from specified rules and datasets");
} else if (this.elementsType == "collection_contents") {
return _l("Save rules and return to tool form");
} else {
return _l("Upload collection using specified rules");
}
},
titleCancel() {
if (this.importType == "datasets") {
return _l("Close this modal and do not upload any datasets");
} else {
return _l("Close this modal and do not create any collections");
}
},
finishButtonTitle() {
if (this.elementsType == "datasets" || this.elementsType == "library_datasets") {
return _l("Create");
} else if (this.elementsType == "collection_contents") {
return _l("Save");
} else {
return _l("Upload");
}
},
hasActiveMappingEdit() {
const has = _.any(_.values(this.mapping), mapping => mapping.editing);
return has;
},
activeRule() {
return this.activeRuleIndex !== null && this.rules[this.activeRuleIndex];
},
activeRuleColHeaders() {
const rulesHeaders = this.activeRuleIndex !== null && this.colHeadersPerRule[this.activeRuleIndex];
return rulesHeaders || this.colHeaders;
},
horizontal() {
return this.orientation == "horizontal";
},
vertical() {
return this.orientation == "vertical";
},
mappedTargets() {
const targets = [];
for (let mapping of this.mapping) {
targets.push(mapping.type);
}
return targets;
},
unmappedTargets() {
const targets = [];
const mappedTargets = this.mappedTargets;
for (let target in MAPPING_TARGETS) {
const targetModes = MAPPING_TARGETS[target].modes;
if (targetModes && targetModes.indexOf(this.elementsType) < 0) {
continue;
}
const targetDefinition = MAPPING_TARGETS[target];
const targetImportType = targetDefinition.importType;
if (targetImportType && this.importType != targetImportType) {
continue;
}
if (!this.ftpUploadSite && targetDefinition.requiresFtp) {
continue;
}
if (mappedTargets.indexOf(target) < 0) {
targets.push(target);
}
}
return targets;
},
hotData() {
let data, sources, columns;
if (this.elementsType == "datasets") {
data = this.initialElements.map(el => [el["hid"], el["name"]]);
sources = this.initialElements.slice();
columns = ["new", "new"];
} else if (this.elementsType == "library_datasets") {
data = this.initialElements.map(el => [el["name"]]);
sources = this.initialElements.slice();
columns = ["new"];
} else if (this.elementsType == "collection_contents") {
const collection = this.initialElements;
if (collection) {
const obj = this.populateElementsFromCollectionDescription(
collection.elements,
collection.collection_type
);
data = obj.data;
sources = obj.sources;
columns = [];
} else {
data = [];
sources = [];
columns = [];
}
} else {
data = this.initialElements.slice();
sources = data.map(el => null);
columns = [];
if (this.initialElements) {
for (var columnIndex in this.initialElements[0]) {
columns.push("new");
}
}
}
this.colHeadersPerRule = [];
return RuleDefs.applyRules(data, sources, columns, this.rules, this.colHeadersPerRule);
},
colHeaders() {
const data = this.hotData["data"];
const columns = this.hotData["columns"];
return RuleDefs.colHeadersFor(data, columns);
},
colHeadersDisplay() {
const formattedHeaders = [];
for (let colIndex in this.colHeaders) {
const colHeader = this.colHeaders[colIndex];
formattedHeaders[colIndex] = `<b>${_.escape(colHeader)}</b>`;
const mappingDisplay = [];
for (let mapping of this.mapping) {
if (mapping.columns.indexOf(parseInt(colIndex)) !== -1) {
const mappingDef = MAPPING_TARGETS[mapping.type];
mappingDisplay.push(`<i>${_.escape(mappingDef.columnHeader || mappingDef.label)}</i>`);
}
}
if (mappingDisplay.length == 1) {
formattedHeaders[colIndex] += ` (${mappingDisplay[0]})`;
} else if (mappingDisplay.length > 1) {
formattedHeaders[colIndex] += ` (${[
mappingDisplay.slice(0, -1).join(", "),
mappingDisplay.slice(-1)[0]
].join(" & ")})`;
}
}
return formattedHeaders;
},
mappingAsDict() {
const asDict = {};
for (let mapping of this.mapping) {
asDict[mapping.type] = mapping;
}
return asDict;
},
metadataOptions() {
const metadataOptions = {};
if (this.elementsType == "collection_contents") {
let collectionType;
if (this.initialElements) {
collectionType = this.initialElements.collection_type;
} else {
// give a bunch of different options if not constrained with given input
collectionType = "list:list:list:paired";
}
const collectionTypeRanks = collectionType.split(":");
for (let index in collectionTypeRanks) {
const collectionTypeRank = collectionTypeRanks[index];
if (collectionTypeRank == "list") {
// TODO: drop the numeral at the end if only flat list
metadataOptions["identifier" + index] = _l("List Identifier ") + (parseInt(index) + 1);
} else {
metadataOptions["identifier" + index] = _l("Paired Identifier");
}
}
}
return metadataOptions;
},
collectionType() {
let identifierColumns = [];
if (this.mappingAsDict.list_identifiers) {
identifierColumns = this.mappingAsDict.list_identifiers.columns;
}
let collectionType = identifierColumns.map(col => "list").join(":");
if (this.mappingAsDict.paired_identifier) {
if (collectionType) {
collectionType += ":paired";
} else {
collectionType = "paired";
}
}
return collectionType;
},
validInput() {
const identifierColumns = this.identifierColumns();
const mappingAsDict = this.mappingAsDict;
const buildingCollection = identifierColumns.length > 0 && this.elementsType != "collection_contents";
let valid = true;
if (buildingCollection && !mappingAsDict.collection_name) {
valid = this.collectionName.length > 0;
}
if (mappingAsDict.ftp_path && mappingAsDict.url) {
// Can only specify one of these.
valid = false;
}
for (var rule of this.rules) {
if (rule.error) {
valid = false;
}
}
// raw tabular variant can build stand-alone datasets without identifier
// columns for the collection builder for existing datasets cannot do this.
const fromDatasets = this.elementsType == "datasets" || this.elementsType == "library_datasets";
if (fromDatasets && identifierColumns.length == 0) {
valid = false;
}
return valid;
}
},
methods: {
l(str) {
// _l conflicts private methods of Vue internals, expose as l instead
return _l(str);
},
cancel() {
this.oncancel();
},
mappingTargets() {
return MAPPING_TARGETS;
},
resetRules() {
this.rules.splice(0, this.rules.length);
this.mapping.splice(0, this.mapping.length);
},
resetRulesAndState() {
this.resetRules();
this.state = "build";
this.errorMessage = "";
this.collectionName = "";
},
addNewRule(ruleType) {
RULES[ruleType].init(this);
this.displayRuleType = ruleType;
this.activeRuleIndex = null;
},
handleRuleSave(ruleType) {
const rule = this.activeRule;
if (rule) {
RULES[ruleType].save(this, rule);
} else {
const rule = { type: ruleType };
RULES[ruleType].save(this, rule);
this.rules.push(rule);
}
},
viewSource() {
this.resetSource();
this.ruleView = "source";
},
resetSource() {
const replacer = function(key, value) {
if (key == "error" || key == "warn") {
return undefined;
}
return value;
};
const asJson = {
rules: this.rules,
mapping: this.mapping
};
if (!this.exisistingDatasets) {
if (this.extension !== UploadUtils.DEFAULT_EXTENSION) {
asJson.extension = this.extension;
}
if (this.genome !== UploadUtils.DEFAULT_GENOME) {
asJson.genome = this.genome;
}
}
this.ruleSourceJson = asJson;
this.ruleSource = JSON.stringify(asJson, replacer, " ");
this.ruleSourceError = null;
},
attemptRulePreview() {
// Leave source mode if rules are valid and view.
this.ruleSourceError = null;
let asJson;
try {
asJson = JSON.parse(this.ruleSource);
} catch (error) {
this.ruleSourceError = "Problem parsing your rules.";
return;
}
this.updateFromSource(asJson);
this.ruleView = "normal";
},
cancelSourceEdit() {
this.ruleSourceError = null;
this.ruleView = "normal";
},
updateFromSource(asJson) {
this.resetRules();
if (asJson.extension) {
this.extension = asJson.extension;
}
if (asJson.genome) {
this.genome = asJson.genome;
}
if (asJson.rules) {
this.rules = asJson.rules;
}
if (asJson.mapping) {
this.mapping = asJson.mapping;
}
},
addIdentifier(identifier) {
const multiple = this.mappingTargets()[identifier].multiple;
// If multiple selection, pop open a new column selector in edit mode.
const initialColumns = multiple ? [] : [0];
this.mapping.push({ type: identifier, columns: initialColumns, editing: multiple });
},
editRule(rule, index) {
const ruleType = rule.type;
this.activeRuleIndex = index;
RULES[ruleType].init(this, rule);
this.displayRuleType = ruleType;
},
removeRule(index) {
this.rules.splice(index, 1);
},
removeMapping(index) {
this.mapping.splice(index, 1);
},
refreshAndWait(response) {
if (Galaxy && Galaxy.currHistoryPanel) {
Galaxy.currHistoryPanel.refreshContents();
}
this.waitOnJob(response);
},
waitOnJob(response) {
const jobId = response.data.jobs[0].id;
const handleJobShow = jobResponse => {
const state = jobResponse.data.state;
this.waitingJobState = state;
if (JobStatesModel.NON_TERMINAL_STATES.indexOf(state) !== -1) {
setTimeout(doJobCheck, 1000);
} else if (JobStatesModel.ERROR_STATES.indexOf(state) !== -1) {
this.state = "error";
this.errorMessage =
"Unknown error encountered while running your upload job, this could be a server issue or a problem with the upload definition.";
} else {
const history =
parent.Galaxy && parent.Galaxy.currHistoryPanel && parent.Galaxy.currHistoryPanel.model;
history.refresh();
this.oncreate();
}
};
const doJobCheck = () => {
axios
.get(`${Galaxy.root}api/jobs/${jobId}`)
.then(handleJobShow)
.catch(this.renderFetchError);
};
setTimeout(doJobCheck, 1000);
},
renderFetchError(error) {
this.state = "error";
if (error.response) {
console.log(error.response);
this.errorMessage = error.response.data.err_msg;
} else {
console.log(error);
this.errorMessage = "Unknown error encountered: " + error;
}
},
swapOrientation() {
this.orientation = this.orientation == "horizontal" ? "vertical" : "horizontal";
const hotTable = this.$refs.hotTable.table;
if (this.orientation == "horizontal") {
this.$nextTick(function() {
const fullWidth = $(".rule-builder-body").width();
hotTable.updateSettings({
width: fullWidth
});
});
} else {
this.$nextTick(function() {
const fullWidth = $(".rule-builder-body").width();
hotTable.updateSettings({
width: fullWidth - 270
});
});
}
},
createCollection() {
this.state = "wait";
const name = this.collectionName;
const collectionType = this.collectionType;
if (this.elementsType == "datasets" || this.elementsType == "library_datasets") {
const elements = this.creationElementsFromDatasets();
if (this.state !== "error") {
new AjaxQueue.AjaxQueue(
_.map(elements, (elements, name) => {
return () => {
const response = this.creationFn(elements, collectionType, name, this.hideSourceItems);
return response;
};
})
)
.done(this.oncreate)
.fail(this.renderFetchError);
}
} else if (this.elementsType == "collection_contents") {
this.resetSource();
if (this.state !== "error") {
this.saveRulesFn(this.ruleSourceJson);
this.oncreate();
}
} else {
const historyId = Galaxy.currHistoryPanel.model.id;
let elements, targets;
if (collectionType) {
targets = [];
const elementsByCollectionName = this.creationElementsForFetch();
for (let collectionName in elementsByCollectionName) {
const target = {
destination: { type: "hdca" },
elements: elementsByCollectionName[collectionName],
collection_type: collectionType,
name: collectionName
};
targets.push(target);
}
} else {
elements = this.creationDatasetsForFetch();
targets = [
{
destination: { type: "hdas" },
elements: elements,
name: name
}
];
}
if (this.state !== "error") {
axios
.post(`${Galaxy.root}api/tools/fetch`, {
history_id: historyId,
targets: targets
})
.then(this.refreshAndWait)
.catch(this.renderFetchError);
}
}
},
identifierColumns() {
const mappingAsDict = this.mappingAsDict;
let identifierColumns = [];
if (mappingAsDict.list_identifiers) {
identifierColumns = mappingAsDict.list_identifiers.columns.slice();
}
if (this.mappingAsDict.paired_identifier) {
identifierColumns.push(this.mappingAsDict.paired_identifier.columns[0]);
}
return identifierColumns;
},
buildRequestElements(createDatasetDescription, createSubcollectionDescription, subElementProp) {
const data = this.hotData["data"];
const identifierColumns = this.identifierColumns();
if (identifierColumns.length < 1) {
console.log("Error but this shouldn't have happened, create button should have been disabled.");
return;
}
const numIdentifierColumns = identifierColumns.length;
const collectionType = this.collectionType;
const elementsByName = {};
let dataByCollection = {};
const collectionNameMap = this.mappingAsDict.collection_name;
if (collectionNameMap) {
const collectionNameTarget = collectionNameMap.columns[0];
for (let dataIndex in data) {
const row = data[dataIndex];
const name = row[collectionNameTarget];
if (!dataByCollection[name]) {
dataByCollection[name] = {};
}
dataByCollection[name][dataIndex] = row;
}
} else {
// use global collection name from the form.
dataByCollection[this.collectionName] = data;
}
for (let collectionName in dataByCollection) {
const elements = [];
const identifiers = [];
for (let dataIndex in dataByCollection[collectionName]) {
const rowData = data[dataIndex];
// For each row, find place in depth for this element.
let collectionTypeAtDepth = collectionType;
let elementsAtDepth = elements;
let identifiersAtDepth = identifiers;
for (
let identifierColumnIndex = 0;
identifierColumnIndex < numIdentifierColumns;
identifierColumnIndex++
) {
// typeof indicates identifier is a string, but the raw string value coming from this data
// structure sometimes does not work as expected with indexOf below, I don't understand why
// but as a result this cast here seems needed.
let identifier = String(rowData[identifierColumns[identifierColumnIndex]]);
if (identifierColumnIndex + 1 == numIdentifierColumns) {
// At correct final position in nested structure for this dataset.
if (collectionTypeAtDepth === "paired") {
if (["f", "1", "r1", "forward"].indexOf(identifier.toLowerCase()) > -1) {
identifier = "forward";
} else if (["r", "2", "r2", "reverse"].indexOf(identifier.toLowerCase()) > -1) {
identifier = "reverse";
} else {
this.state = "error";
this.errorMessage =
"Unknown indicator of paired status encountered - only values of F, R, 1, 2, R1, R2, forward, or reverse are allowed.";
return;
}
}
const element = createDatasetDescription(dataIndex, identifier);
elementsAtDepth.push(element);
if (identifiersAtDepth.indexOf(identifier) >= 0) {
this.state = "error";
this.errorMessage =
"Duplicate identifiers detected, collection identifiers must be unique.";
return;
}
identifiersAtDepth.push(identifier);
} else {
// Create nesting for this element.
collectionTypeAtDepth = collectionTypeAtDepth
.split(":")
.slice(1)
.join(":");
let found = false;
for (let element of elementsAtDepth) {
if (element["name"] == identifier) {
elementsAtDepth = element[subElementProp];
identifiersAtDepth = identifiersAtDepth[identifier];
found = true;
break;
}
}
if (!found) {
const subcollection = createSubcollectionDescription(identifier);
elementsAtDepth.push(subcollection);
identifiersAtDepth[identifier] = [];
const childCollectionElements = [];
subcollection[subElementProp] = childCollectionElements;
subcollection.collection_type = collectionTypeAtDepth;
elementsAtDepth = childCollectionElements;
identifiersAtDepth = identifiersAtDepth[identifier];
}
}
}
}
elementsByName[collectionName] = elements;
}
return elementsByName;
},
creationElementsFromDatasets() {
const sources = this.hotData["sources"];
const data = this.hotData["data"];
const elementsByCollectionName = this.buildRequestElements(
(dataIndex, identifier) => {
const source = sources[dataIndex];
const src = this.elementsType == "datasets" ? "hda" : "ldda";
return { id: source["id"], name: identifier, src: src };
},
identifier => {
return { name: identifier, src: "new_collection" };
},
"element_identifiers"
);
return elementsByCollectionName;
},
creationElementsForFetch() {
// fetch elements for HDCA
const data = this.hotData["data"];
const mappingAsDict = this.mappingAsDict;
const elementsByCollectionName = this.buildRequestElements(
(dataIndex, identifier) => {
const res = this._datasetFor(dataIndex, data, mappingAsDict);
res["name"] = identifier;
return res;
},
identifier => {
return { name: identifier };
},
"elements"
);
return elementsByCollectionName;
},
creationDatasetsForFetch() {
// fetch elements for HDAs if not collection information specified.
const data = this.hotData["data"];
const mappingAsDict = this.mappingAsDict;
const datasets = [];
for (let dataIndex in data) {
const rowData = data[dataIndex];
const res = this._datasetFor(dataIndex, data, mappingAsDict);
datasets.push(res);
}
return datasets;
},
populateElementsFromCollectionDescription(elements, collectionType, parentIdentifiers_) {
const parentIdentifiers = parentIdentifiers_ ? parentIdentifiers_ : [];
let data = [];
let sources = [];
for (let element of elements) {
const elementObject = element.object;
const identifiers = parentIdentifiers.concat([element.element_identifier]);
const collectionTypeLevelSepIndex = collectionType.indexOf(":");
if (collectionTypeLevelSepIndex === -1) {
// Flat collection at this depth.
// sources are the elements
// TOOD: right thing is probably this: data.push([]);
data.push([]);
sources.push({ identifiers: identifiers, dataset: elementObject });
} else {
const restCollectionType = collectionType.slice(collectionTypeLevelSepIndex + 1);
let elementObj = this.populateElementsFromCollectionDescription(
elementObject.elements,
restCollectionType,
identifiers
);
const elementData = elementObj.data;
const elementSources = elementObj.sources;
data = data.concat(elementData);
sources = sources.concat(elementSources);
}
}
return { data, sources };
},
highlightColumn(n) {
const headerSelection = $(`.htCore > thead > tr > th:nth-child(${n + 1})`);
headerSelection.addClass("ht__highlight");
const bodySelection = $(`.htCore > tbody > tr > td:nth-child(${n + 1})`);
bodySelection.addClass("rule-highlight");
},
unhighlightColumn(n) {
const headerSelection = $(`.htCore > thead > tr > th:nth-child(${n + 1})`);
headerSelection.removeClass("ht__highlight");
const bodySelection = $(`.htCore > tbody > tr > td:nth-child(${n + 1})`);
bodySelection.removeClass("rule-highlight");
},
_datasetFor(dataIndex, data, mappingAsDict) {
const res = {};
if (mappingAsDict.url) {
const urlColumn = mappingAsDict.url.columns[0];
let url = data[dataIndex][urlColumn];
if (url.indexOf("://") == -1) {
url = "http://" + url;
}
res["url"] = url;
res["src"] = "url";
} else {
const ftpPathColumn = mappingAsDict.ftp_path.columns[0];
const ftpPath = data[dataIndex][ftpPathColumn];
res["ftp_path"] = ftpPath;
res["src"] = "ftp_import";
}
if (mappingAsDict.dbkey) {
const dbkeyColumn = mappingAsDict.dbkey.columns[0];
const dbkey = data[dataIndex][dbkeyColumn];
res["dbkey"] = dbkey;
} else if (this.genome) {
res["dbkey"] = this.genome;
}
if (mappingAsDict.file_type) {
const fileTypeColumn = mappingAsDict.file_type.columns[0];
const fileType = data[dataIndex][fileTypeColumn];
res["ext"] = file_type;
} else if (this.extension) {
res["ext"] = this.extension;
}
if (mappingAsDict.name) {
const nameColumn = mappingAsDict.name.columns[0];
const name = data[dataIndex][nameColumn];
res["name"] = name;
}
if (mappingAsDict.info) {
const infoColumn = mappingAsDict.info.columns[0];
const info = data[dataIndex][infoColumn];
res["info"] = info;
}
return res;
}
},
created() {
if (this.elementsType !== "collection_contents") {
let columnCount = null;
if (this.elementsType == "datasets") {
for (let element of this.initialElements) {
if (element.history_content_type == "dataset_collection") {
this.errorMessage =
"This component can only be used with datasets, you have specified one or more collections.";
this.state = "error";
}
}
} else {
for (let row of this.initialElements) {
if (columnCount == null) {
columnCount = row.length;
} else {
if (columnCount != row.length) {
this.jaggedData = true;
break;
}
}
}
}
UploadUtils.getUploadDatatypes(
extensions => {
this.extensions = extensions;
this.extension = UploadUtils.DEFAULT_EXTENSION;
},
false,
UploadUtils.AUTO_EXTENSION
);
UploadUtils.getUploadGenomes(genomes => {
this.genomes = genomes;
this.genome = UploadUtils.DEFAULT_GENOME;
}, UploadUtils.DEFAULT_GENOME);
}
},
watch: {
addColumnRegexType: function(val) {
if (val == "groups") {
this.addColumnRegexGroupCount = 1;
}
if (val == "replacement") {
this.addColumnRegexReplacement = "\0";
}
}
},
components: {
HotTable,
RuleComponent,
RuleTargetComponent,
RuleDisplay,
IdentifierDisplay,
ColumnSelector,
RegularExpressionInput,
StateDiv,
RuleModalHeader,
RuleModalMiddle,
RuleModalFooter,
Select2
}
};
</script>
<style src="../../../node_modules/handsontable/dist/handsontable.full.css">
</style>
<style>
.table-column {
width: 100%;
/* overflow: scroll; */
}
.select2-container {
min-width: 60px;
}
.vertical #hot-table {
width: 100%;
overflow: scroll;
height: 400px;
}
.horizontal #hot-table {
width: 100%;
overflow: scroll;
height: 250px;
}
.rule-builder-body {
height: 400px;
}
.rule-column.vertical {
height: 400px;
}
.rule-column.horizontal {
height: 150px;
}
.rules-container-full {
width: 100%;
height: 400px;
}
.rules-container {
border: 1px dashed #ccc;
padding: 5px;
}
.rules-container-vertical {
width: 280px;
height: 400px;
}
.rules-container-horizontal {
width: 100%;
height: 150px;
}
.rules-container .title {
font-weight: bold;
}
.rule-summary {
height: 100%;
display: flex;
flex-direction: column;
}
.rule-edit-buttons {
margin: 5px;
}
.rules {
flex-grow: 1;
overflow-y: scroll;
}
.rule-source {
height: 400px;
}
.rules li {
list-style-type: circle;
list-style-position: inside;
padding: 5px;
padding-top: 0px;
padding-bottom: 0px;
}
.rule-column-selector li {
list-style-type: circle;
list-style-position: inside;
padding: 5px;
padding-top: 0px;
padding-bottom: 0px;
}
.rules .rule-error {
display: block;
margin-left: 10px;
font-style: italic;
color: red;
}
.rule-warning {
display: block;
margin-left: 10px;
font-style: italic;
color: #e28809;
}
.rule-summary .title {
font-size: 1.1em;
}
.rule-highlight {
font-style: italic;
font-weight: bold;
}
.rules-buttons {
}
.rule-footer-inputs label {
padding-left: 20px;
align-self: baseline;
}
.rule-footer-inputs .select2-container {
align-self: baseline;
}
.rule-footer-inputs {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
align-items: baseline;
}
.rule-footer-inputs input {
align-self: baseline;
}
.extension-select {
flex: 1;
max-width: 120px;
min-width: 60px;
}
.genome-select {
flex: 1;
max-width: 300px;
min-width: 120px;
}
.collection-name {
flex: 1;
min-width: 120px;
max-width: 500px;
}
.rule-footer-genome-group {
flex: 2;
display: flex;
}
.rule-footer-extension-group {
flex: 1;
display: flex;
}
.rule-footer-name-group {
flex: 3;
display: flex;
flex-direction: row-reverse;
}
.fa-edit,
.fa-times,
.fa-wrench {
cursor: pointer;
}
</style>