Merge pull request #9809 from jmchilton/upload_from_form

Allow uploading directly from the tool form
This commit is contained in:
Björn Grüning
2020-08-09 11:27:56 +02:00
committed by GitHub
18 changed files with 243 additions and 35 deletions
@@ -23,6 +23,10 @@
<div class="fa fa-caret-left mr-1" />
Back
</b-btn>
<b-btn size="sm" class="float-left mr-1" @click="newUpload">
<div class="fa fa-upload ml-1" />
Upload
</b-btn>
<b-btn
v-if="multiple"
size="sm"
@@ -44,6 +48,7 @@ import SelectionDialogMixin from "components/SelectionDialog/SelectionDialogMixi
import { UrlTracker } from "./utilities";
import { Model } from "./model";
import { Services } from "./services";
import { mountUploadModal } from "components/Upload";
Vue.use(BootstrapVue);
@@ -125,6 +130,16 @@ export default {
this.load(record.url);
}
},
newUpload: function () {
const propsData = {
multiple: this.multiple,
format: this.format,
callback: this.callback,
modalShow: true,
};
mountUploadModal(propsData);
this.modalShow = false;
},
/** Called when selection is complete, values are formatted and parsed to external callback **/
finalize: function () {
const results = this.model.finalize();
@@ -20,7 +20,9 @@ describe("Datatypes/index.js", () => {
axiosMock.onGet(`/api/datatypes/types_and_mapping`).reply(200, typesAndMappingResponse);
await getDatatypesMapper().then((mapper) => {
expect(mapper.isSubType("txt", "data")).to.equals(true);
expect(mapper.isSubType("txt", "txt")).to.equals(true);
expect(mapper.isSubType("data", "txt")).to.equals(false);
expect(mapper.isSubTypeOfAny("data", ["txt", "data"])).to.equals(true);
});
});
});
@@ -10,4 +10,8 @@ export class DatatypesMapperModel {
parent = mapping.ext_to_class_name[parent];
return mapping.class_to_classes[child] && parent in mapping.class_to_classes[child];
}
isSubTypeOfAny(child, parents) {
return parents.some((parent) => this.isSubType(child, parent));
}
}
@@ -42,7 +42,7 @@
</select2>
</template>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.hide()">
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.dismiss()">
{{ btnCloseTitle }}
</b-button>
<b-button
@@ -139,6 +139,7 @@ export default {
listExtensions: [],
listGenomes: [],
running: false,
multiple: true, // needed for uploadbox stuff - always allow multiple uploads for collections
counterAnnounce: 0,
counterSuccess: 0,
counterError: 0,
@@ -158,7 +159,7 @@ export default {
btnBuildTitle: _l("Build"),
btnStopTitle: _l("Pause"),
btnResetTitle: _l("Reset"),
btnCloseTitle: _l("Close"),
btnCloseTitle: this.app.callback ? _l("Cancel") : _l("Close"),
};
},
created() {
@@ -34,7 +34,7 @@
</select2>
</template>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" @click="app.hide()">
<b-button ref="btnClose" class="ui-button-default" @click="app.dismiss()">
{{ btnCloseTitle }}
</b-button>
<b-button
@@ -74,7 +74,7 @@ export default {
showHelper: true,
btnResetTitle: _l("Reset"),
btnStartTitle: _l("Start"),
btnCloseTitle: _l("Close"),
btnCloseTitle: this.app.callback ? _l("Cancel") : _l("Close"),
readyStart: false,
};
},
@@ -34,7 +34,7 @@
</select2>
</template>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.hide()">
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.dismiss()">
{{ btnCloseTitle }}
</b-button>
<b-button
@@ -55,6 +55,16 @@
>
{{ btnStopTitle }}
</b-button>
<b-button
ref="btnBuild"
class="ui-button-default"
id="btn-build"
@click="_eventSelect"
:disabled="!enableBuild"
:variant="enableBuild ? 'primary' : ''"
>
{{ btnSelectTitle }}
</b-button>
<b-button
ref="btnStart"
class="ui-button-default"
@@ -111,6 +121,12 @@ Vue.use(BootstrapVue);
export default {
mixins: [UploadBoxMixin],
props: {
multiple: {
type: Boolean,
default: true,
},
},
data() {
return {
topInfo: "",
@@ -118,6 +134,7 @@ export default {
showHelper: true,
extension: this.app.defaultExtension,
genome: this.app.defaultGenome,
callback: this.app.callback,
listExtensions: [],
listGenomes: [],
running: false,
@@ -129,6 +146,7 @@ export default {
uploadSize: 0,
uploadCompleted: 0,
enableReset: false,
enableBuild: false,
enableStart: false,
enableSources: false,
btnLocalTitle: _l("Choose local files"),
@@ -136,7 +154,8 @@ export default {
btnStartTitle: _l("Start"),
btnStopTitle: _l("Pause"),
btnResetTitle: _l("Reset"),
btnCloseTitle: _l("Close"),
btnCloseTitle: this.app.callback ? _l("Cancel") : _l("Close"),
btnSelectTitle: _l("Select"),
};
},
created() {
@@ -149,6 +168,7 @@ export default {
// file upload
this.initUploadbox({
url: this.app.uploadPath,
multiple: this.multiple,
announce: (index, file) => {
this._eventAnnounce(index, file);
},
@@ -200,6 +220,17 @@ export default {
},
},
methods: {
_eventSelect: function () {
const models = this.getUploadedModels();
const asDict = models.map((model) => {
return {
id: model.attributes.id, // model.id has datatype prefix
src: model.src,
};
});
this.callback(asDict);
this.app.cancel();
},
_newUploadModelProps: function (index, file) {
return {
id: index,
@@ -213,8 +244,9 @@ export default {
/** Success */
_eventSuccess: function (index, message) {
var hids = _.pluck(message["outputs"], "hid");
var it = this.collection.get(index);
it.set({ percentage: 100, status: "success" });
it.set({ percentage: 100, status: "success", hids: hids });
this._updateStateForSuccess(it);
},
@@ -46,7 +46,7 @@
></textarea>
</span>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.hide()">
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.dismiss()">
{{ btnCloseTitle }}
</b-button>
<b-button
@@ -103,7 +103,7 @@ export default {
selectionType: "paste",
btnBuildTitle: _l("Build"),
btnResetTitle: _l("Reset"),
btnCloseTitle: _l("Close"),
btnCloseTitle: this.app.callback ? _l("Cancel") : _l("Close"),
};
},
created() {
@@ -102,15 +102,15 @@ export default {
},
_updateStateForCounters: function () {
this.setTopInfoBasedOnCounters();
this.enableReset =
this.counterRunning == 0 && this.counterAnnounce + this.counterSuccess + this.counterError > 0;
const counterNonRunning = this.counterAnnounce + this.counterSuccess + this.counterError;
this.enableReset = this.counterRunning == 0 && counterNonRunning > 0;
this.enableStart = this.counterRunning == 0 && this.counterAnnounce > 0;
this.enableBuild =
this.counterRunning == 0 &&
this.counterAnnounce == 0 &&
this.counterSuccess > 0 &&
this.counterError == 0;
this.enableSources = this.counterRunning == 0;
this.enableSources = this.counterRunning == 0 && (this.multiple || counterNonRunning == 0);
var show_table = this.counterAnnounce + this.counterSuccess + this.counterError > 0;
this.showHelper = !show_table;
},
@@ -259,7 +259,7 @@ export default {
return $(this.$refs.uploadTable);
},
extensionDetails(extension) {
return findExtension(this.listExtensions, extension);
return findExtension(this.effectiveExtensions, extension);
},
initExtensionInfo() {
$(this.$refs.footerExtensionInfo)
@@ -283,7 +283,7 @@ export default {
this.collection = new UploadModel.Collection();
},
initAppProperties() {
this.listExtensions = this.app.listExtensions;
this.listExtensions = this.app.effectiveExtensions;
this.listGenomes = this.app.listGenomes;
this.ftpUploadSite = this.app.currentFtp();
this.fileSourcesConfigured = this.app.fileSourcesConfigured;
@@ -6,23 +6,25 @@
modal-class="ui-modal"
dialog-class="upload-dialog"
body-class="upload-dialog-body"
ref="modal"
no-enforce-focus
hide-footer
:id="id"
>
<template v-slot:modal-header>
<h4 class="title" tabindex="0">{{ title }}</h4>
</template>
<b-tabs v-if="ready">
<b-tab title="Regular" id="regular" button-id="tab-title-link-regular">
<default :app="this" :lazy-load-max="50" />
<b-tab title="Regular" id="regular" button-id="tab-title-link-regular" v-if="showRegular">
<default :app="this" :lazy-load-max="50" :multiple="multiple" />
</b-tab>
<b-tab title="Composite" id="composite" button-id="tab-title-link-composite">
<b-tab title="Composite" id="composite" button-id="tab-title-link-composite" v-if="showComposite">
<composite :app="this" />
</b-tab>
<b-tab title="Collection" id="collection" button-id="tab-title-link-collection">
<b-tab title="Collection" id="collection" button-id="tab-title-link-collection" v-if="showCollection">
<collection :app="this" />
</b-tab>
<b-tab title="Rule-based" id="rule-based" button-id="tab-title-link-rule-based">
<b-tab title="Rule-based" id="rule-based" button-id="tab-title-link-rule-based" v-if="showRules">
<rules-input :app="this" />
</b-tab>
</b-tabs>
@@ -40,6 +42,7 @@ import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { getGalaxyInstance } from "app";
import UploadUtils from "mvc/upload/upload-utils";
import { getDatatypesMapper } from "components/Datatypes";
import Composite from "./Composite";
import Collection from "./Collection";
import Default from "./Default";
@@ -93,6 +96,20 @@ export default {
type: Boolean,
default: false,
},
formats: {
type: Array,
default: null,
},
multiple: {
// Restrict the forms to a single dataset upload if false
type: Boolean,
default: true,
},
callback: {
// Return uploads when done if supplied.
type: Function,
default: null,
},
auto: {
type: Object,
default: function () {
@@ -102,6 +119,7 @@ export default {
},
data: function () {
return {
id: "",
title: _l("Download from web or upload from disk"),
historyAvailable: false,
currentUser: null,
@@ -109,6 +127,8 @@ export default {
listExtensions: [],
genomesSet: false,
extensionsSet: false,
datatypesMapper: null,
datatypesMapperReady: true,
};
},
created: function () {
@@ -138,11 +158,67 @@ export default {
}, this.defaultGenome);
this.initStateWhenHistoryReady();
if (this.formats !== null) {
this.datatypesMapperReady = false;
getDatatypesMapper().then((datatypesMapper) => {
this.datatypesMapper = datatypesMapper;
this.datatypesMapperReady = true;
});
} else {
this.datatypesMapperReady = true;
}
},
computed: {
ready() {
return this.genomesSet && this.extensionsSet && this.historyAvailable;
return this.genomesSet && this.extensionsSet && this.historyAvailable && this.datatypesMapperReady;
},
unrestricted() {
return this.formats === null && this.multiple;
},
effectiveExtensions() {
if (this.formats === null || !this.datatypesMapperReady) {
return this.listExtensions;
}
const effectiveExtensions = [];
this.listExtensions.forEach((extension) => {
if (extension && extension.id == "auto") {
effectiveExtensions.push(extension);
} else if (this.datatypesMapper.isSubTypeOfAny(extension.id, this.formats)) {
effectiveExtensions.push(extension);
}
});
return effectiveExtensions;
},
formatRestricted() {
return this.formats !== null;
},
showComposite() {
if (!this.formatRestricted) {
return true;
}
return this.effectiveExtensions.some((extension) => !!extension.composite_files);
},
showRegular() {
if (!this.formatRestricted) {
return true;
}
return this.effectiveExtensions.some((extension) => !extension.composite_files);
},
showCollection() {
if (this.unrestricted) {
return true;
}
return false;
},
showRules() {
if (this.unrestricted) {
return true;
}
return this.multiple;
},
},
mounted() {
this.id = String(this._uid);
},
methods: {
show() {
@@ -152,6 +228,21 @@ export default {
hide() {
this.modalShow = false;
},
cancel() {
this.hide();
this.$nextTick(() => {
this.$bvModal.hide(this.id, "cancel");
this.$destroy();
});
},
dismiss() {
// hide or cancel based on whether this is a singleton
if (this.callback) {
this.cancel();
} else {
this.hide();
}
},
initStateWhenHistoryReady() {
const Galaxy = getGalaxyInstance();
if (!Galaxy.currHistoryPanel || !Galaxy.currHistoryPanel.model) {
@@ -245,12 +336,4 @@ export default {
};
</script>
<style>
.upload-dialog {
width: 900px;
}
.upload-dialog-body {
height: 500px;
overflow: hidden;
}
</style>
<style></style>
@@ -4,3 +4,5 @@
export { default as UploadModal } from "./UploadModal";
export { initializeUploadDefaults } from "./config";
export { mount as mountUploadModal } from "./mount";
@@ -0,0 +1,13 @@
import Vue from "vue";
import UploadModal from "./UploadModal";
import { initializeUploadDefaults } from "./config";
export function mount(propsData = {}) {
propsData = initializeUploadDefaults(propsData);
const instance = Vue.extend(UploadModal);
const vm = document.createElement("div");
document.getElementsByTagName("body")[0].appendChild(vm);
new instance({
propsData: propsData,
}).$mount(vm);
}
@@ -10,7 +10,7 @@ export function mountWithApp(component, options = {}, propsData_ = {}) {
return "ftp://localhost";
},
model: new Backbone.Model(),
listExtensions: [
effectiveExtensions: [
{ id: "ab1", text: "ab1", description: "A binary sequence file in 'ab1' format with a '.ab1'" },
{
id: "affybatch",
@@ -272,8 +272,10 @@ const View = Backbone.View.extend({
});
if (cnf.src == "hda") {
this.button_dialog.show();
this.upload_dialog.show();
} else {
this.button_dialog.hide();
this.upload_dialog.hide();
}
this.button_type.value(i);
} else {
@@ -306,7 +308,8 @@ const View = Backbone.View.extend({
// prepare extension component of error message
const data = self.model.get("data");
const extensions = Utils.textify(this.model.get("extensions"));
const formats = this.model.get("extensions");
const extensions = Utils.textify(formats);
const src_labels = this.model.get("src_labels");
// build radio button for data selectors
@@ -362,14 +365,38 @@ const View = Backbone.View.extend({
},
});
// build data dialog button
this.upload_dialog = new Ui.Button({
icon: "fa-upload",
tooltip: "Upload Dataset",
onclick: () => {
const current = this.model.get("current");
const cnf = this.config[current];
// model doesn't have format yet unfortunately, need to get through to here.
galaxy.data.dialog(
(response) => {
this._handleDropValues(response, false);
},
{
multiple: cnf.multiple,
formats: formats,
new: true,
}
);
},
});
// append views
const $fields = $("<div/>").addClass("overflow-auto w-100 py-2 py-lg-0 pr-lg-2 ");
this.$el
.empty()
.addClass("d-flex flex-row flex-wrap flex-lg-nowrap")
.append($("<div/>").append(this.button_type.$el))
.append($fields)
.append($("<div/>").append(this.button_dialog.$el));
.append($fields);
if (galaxy.config.upload_from_form_button == "always-on") {
this.$el.append($("<div style='margin-right: 5px;' />").append(this.upload_dialog.$el));
}
this.$el.append($("<div/>").append(this.button_dialog.$el));
_.each(this.fields, (field) => {
$fields.append(field.$el);
});
+7 -1
View File
@@ -5,6 +5,7 @@ import DataDialog from "components/DataDialog/DataDialog.vue";
import { FilesDialog } from "components/FilesDialog";
import WorkflowDialog from "components/SelectionDialog/WorkflowDialog.vue";
import DatasetCollectionDialog from "components/SelectionDialog/DatasetCollectionDialog.vue";
import { mountUploadModal } from "components/Upload";
import { getGalaxyInstance } from "app";
import { getAppRoot } from "onload/loadConfig";
@@ -44,7 +45,12 @@ export function dialog(callback, options = {}) {
root: galaxy.root,
host: host,
});
_mountSelectionDialog(DataDialog, options);
if (options.new) {
options.modalShow = true;
mountUploadModal(options);
} else {
_mountSelectionDialog(DataDialog, options);
}
});
}
+2 -1
View File
@@ -300,6 +300,7 @@ import { getAppRoot } from "onload/loadConfig";
alert(m);
},
complete: () => {},
multiple: true,
},
options
);
@@ -320,7 +321,7 @@ import { getAppRoot } from "onload/loadConfig";
// element
var uploadinput = $(this).uploadinput({
multiple: true,
multiple: opts.multiple,
onchange: (files) => {
_.each(files, (file) => {
file.chunk_mode = true;
+9
View File
@@ -316,6 +316,15 @@ body {
}
}
.upload-dialog {
width: 900px;
}
.upload-dialog-body {
height: 500px;
overflow: hidden;
}
// ==== Masthead ====
#masthead {
@extend .p-0;
+1
View File
@@ -105,6 +105,7 @@ class ConfigSerializer(base.ModelSerializer):
'python' : _defaults_to((sys.version_info.major, sys.version_info.minor)),
'select_type_workflow_threshold' : _use_config,
'file_sources_configured' : lambda config, key, **context: self.app.file_sources.custom_sources_configured,
'upload_from_form_button' : _use_config,
}
@@ -2158,6 +2158,18 @@ mapping:
When using LDAP for authentication, allow administrators to pre-populate users
using an additional form on 'Create new user'
upload_from_form_button:
type: str
default: 'always-off'
required: false
enum: ['always-on', 'always-off']
desc: |
If 'always-on', add another button to tool form data inputs that allow uploading
data from the tool form in fewer clicks (at the expense of making the form more
complicated). This applies to workflows as well.
Avoiding making this a boolean because we may add options such as 'in-single-form-view'
or 'in-simplified-workflow-views'. https://github.com/galaxyproject/galaxy/pull/9809/files#r461889109
allow_user_dataset_purge:
type: bool
default: true